diff --git a/.changeset/13626-formview-section-style-keys-retired.md b/.changeset/13626-formview-section-style-keys-retired.md deleted file mode 100644 index 55a47dc1a8..0000000000 --- a/.changeset/13626-formview-section-style-keys-retired.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/plugin-form': minor ---- - -Retire the form-view section `className` / `gridClassName` reads (objectstack#13626, -maintainer ruling 2026-09-01, director decision batch C). - -**Breaking, deliberately.** A `className` or `gridClassName` authored on a form-view -section no longer has any effect. Before this change an authored `gridClassName` -reached the section's field-grid `
` and an authored `className` reached the -section wrapper / divider header; both are now dropped at the renderer. - -The two keys sit on the SDUI-only side of the authorable boundary: `@objectstack/spec` -deliberately does not declare either on the form-view/section surface (its -`component.zod.ts` says so in as many words) and the authorable-surface ledger carries -no entry for them. The renderer nevertheless reached them off the parsed view through -`as any` at seven sites — the boundary declared on one side and crossed on the other, -with the two repos each deliberate and in opposite directions. - -Declaring the keys instead was weighed and **not** adopted: it would formally invite -free Tailwind strings into authored metadata, the exact class the boundary exists to -keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime -metadata are never scanned by the build-time Tailwind, so they silently produce no CSS -anyway. Declaring them would have published a styling surface whose most obvious use -does nothing. If per-view styling becomes a real product need it gets an explicit -controlled token surface, not two leaked keys. - -**Migration.** Nothing in the measured corpora has to change. A census across the -objectstack corpus, this repo's corpus, and the hotcrm application found **zero** -authored uses of either key on a form-view section (201 authored section nodes reached, -0 carrying either key). If you author them in your own metadata, move the styling to -the host application's own CSS, or to the form ROOT `className` — which is a different -key on a different node and is **unaffected** by this change. - -Six sites in `ObjectForm` (the tabbed / wizard / split / drawer / modal section maps and -the stacked section-divider) and one in `DrawerForm` (its own divider) stop copying the -keys. The omission is pinned behaviourally across all seven arms rather than by a source -grep, because `ObjectFormSection` still declares both keys — so a later uncast -`className: s.className` would type-check and silently restore consumption. diff --git a/.changeset/14026-list-import-mappings-pin.md b/.changeset/14026-list-import-mappings-pin.md deleted file mode 100644 index 8dd77c188a..0000000000 --- a/.changeset/14026-list-import-mappings-pin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Pin `listImportMappings(objectName)` against the framework's measured -`GET /api/v1/meta/mapping` body (objectui#14026). Test only; no package is -released by this change. diff --git a/.changeset/1674-maker-single-composer.md b/.changeset/1674-maker-single-composer.md deleted file mode 100644 index bbc7d160f9..0000000000 --- a/.changeset/1674-maker-single-composer.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -One composer for makers — the built-in `ask` folds into `build` for authoring -principals (cloud#1674 maker convergence, Phase B). - -Ruling: `build` is the higher-privilege admin agent and, since cloud#1673, a -strict data superset of `ask` — it answers records/aggregation/chart questions -with the same tools. So Ask/Build stop being peer modes for a maker: - -- `surfaceAgent.ts` (the ONE ADR-0063 resolver) gains rule (3b): an `ask` want - upgrades to `build` for a principal with `manage_metadata` when the catalog - serves it — including over an app's explicit `defaultAgent: 'ask'` pin (the - pin serves the app's business users; the maker always gets the superset). - New `makerConvergedOnBuild` / `makerVisibleAgents` carry the same predicate - to the picker sites. -- ChatPane's agent launcher lists `makerVisibleAgents`: the built-in ask leaves - the list for makers (custom agents stay), computed inside ChatPane so every - host — full `/ai` page, Studio copilot, console dock — converges identically. -- A maker's bare `/ai/ask` redirects to `/ai/build`; `/ai/ask/:conversationId` - keeps rendering, so ask history stays readable. -- `ConversationsSidebar` gains `includeAskConversations`: the converged surface - lists BOTH built-in groups' threads whichever one is open (merging only ask - made the build history vanish the moment an ask thread was opened — measured - in-browser on the first pass). -- Console home shows the single Build entry for makers; non-authoring sessions - (the objectstack#8270 hosted posture) keep Ask AI, and every layer is a no-op - for them and for deployments without a build agent. -- `useCanAuthorMetadata` extracted from HomePage to `hooks/` so the CTAs and - the chat surfaces answer the same per-principal question. diff --git a/.changeset/1984-v1-honest-chips-and-ui-fragments.md b/.changeset/1984-v1-honest-chips-and-ui-fragments.md deleted file mode 100644 index e0266c4eaa..0000000000 --- a/.changeset/1984-v1-honest-chips-and-ui-fragments.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -'@object-ui/i18n': minor -'@object-ui/core': minor -'@object-ui/app-shell': minor -'@object-ui/plugin-chatbot': minor -'@object-ui/plugin-dashboard': minor -'@object-ui/plugin-report': minor -'@object-ui/console': minor ---- - -Six user-visible fixes across the maker surface, the assistant rail and the -dataset captions. - -**The maker's start chips now promise only what ADR-0112 v1 builds -(cloud#1984).** Two of the five asked for automation the first version has no -flows or actions for — the ticket chip said 「状态流转」, the inventory chip said -「低库存预警」 — and the measured behaviour was not a refusal but a silent -degrade: a status kanban and a low-stock view. The chip promised an alert and -delivered a page. All five are reworded in all ten packs (and in the call-site -`defaultValue` fallbacks, which are a second copy of the same strings) to ask -for objects, fields, views, pages, dashboards and sample data, keeping each a -real business scenario — the ticket chip now asks for a status field and a board -grouped by it, the inventory chip for a view that filters below the reorder -point. A note beside the keys says to revert when v2 re-adds flows. - -**Five newer AI tools get their step labels (objectui#7481).** A zh conversation -read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: -`get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, -`test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are -newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` -entry in any pack and fell through to the English title-caser. - -**The assistant rail follows the thread when you send (objectui#7480).** The -rail and the full-page maker are the same component; what differs is width. A -reply that still ends on screen in the wide column runs two or three times -taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the -user types and the new bubble, the tool steps and the streaming answer all land -below the fold. Every send path now re-arms the lock — including the plan-card -"Build it" and 确认修改 approvals, whose own code comments already named this -miss. Message APPENDS deliberately do not, so a user reading back through the -thread mid-answer is never yanked to the bottom. - -**Console toasts move off the assistant composer (objectui#7482).** 「客户更新 -成功」 sat on the ChatDock composer's send button and stayed there. One defect, -two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override -that predates ADR-0057 P3a — so a toast both covered the button and, because -sonner pauses a toast's dismiss timer while the pointer is inside the toaster -region, never got to run its 4s timer with a pointer resting on the composer -underneath. The override is gone; the console takes `ConsoleToaster`'s own -documented top-right anchor, and the 4s success duration is now pinned. - -**Built-in aggregate captions follow the locale everywhere (objectui#7534).** -objectui#7258 taught `buildChartSeries()` to resolve a server-minted default -measure through the locale map, so a chart legend read `计数` while the table -beneath it, the KPI caption, the pivot header and the dataset preview still -printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` -takes the same optional `builtinAggregateLabels`, resolving through the one -`resolveMeasureLabel` order, and the five call sites pass it. Omitting the -argument reproduces the previous output byte for byte, and an author-declared -measure still keeps its own label verbatim (objectui#4106). - -**The activity feed stops asking for an object the environment does not have -(objectui#7476).** A tenant environment has no `sys_activity`, so every page -load issued a request that 404'd. Everything downstream was already correct — -the adapter memoizes the missing collection, its logger demotes the failure, the -feed retires as an ANSWER and the panel renders its earned empty state — so what -is left is the request itself, and `data-objectstack` states the rule for it: -the cure for a doomed request is not issuing it. New `useObjectPresence` reads -the object registry the shell loads for the nav anyway; only a registry that has -ANSWERED and lists other objects without this one skips the read. Every -uncertainty — no provider, empty registry, still loading, errored — reads as -before, because a wrong skip would cost a real deployment its feed. diff --git a/.changeset/3162-types-ledger-batch8-verdicts.md b/.changeset/3162-types-ledger-batch8-verdicts.md deleted file mode 100644 index d578606c34..0000000000 --- a/.changeset/3162-types-ledger-batch8-verdicts.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Closes the last batch of the `@objectstack/spec` any-erasure ledger (objectstack#4115 batch -8/8) with zero re-exports, because measurement says none of the four symbols is burnable. - -The batch — `JoinNode`, `NavigationItem`, `NavigationItemSchema`, `JoinedReportBlock` — was -filed as "blocked on objectstack#4171; burn down when the upstream `any` erasure lifts". -That issue closed completed on 2026-07-30, which is the card's own start signal. Re-measuring -each symbol in the BUILT dist at `@objectstack/spec` 17.2.0 (source and issue state are both -insufficient — dist erasure is invisible from either) found three different reasons why -binding is still wrong, none of them the reason the ledger recorded: - -- `JoinNode` — the spec no longer exports the symbol at all; spec 17.0.0 retired the - `query.joins` cluster. Nothing upstream to bind to. -- `NavigationItem` — upstream is precise now, so "no longer `any`" is settled; the three - semantic blockers that actually prevent binding are untouched by it. -- `NavigationItemSchema` — upstream is precise now too, and that is exactly what makes the - burn-down dangerous: the live blocker is runtime shape, invisible to every type-level - probe. Referencing the spec's schema would make the published `objectui validate` reject - navigation metadata this renderer accepts today. -- `JoinedReportBlock` — still erased, to `unknown`, by a cause objectstack#4171 never - covered (a bare `z.ZodTypeAny`, not recursion). - -So the remaining debt is converted from a card pointing at an upstream issue into pins that -each name their own release condition, and the guard's machine-readable ledger -(`check:spec-symbols`) goes from "3 untriaged collisions" to zero, with the three symbols -recorded as declared dialects carrying their measured reasons. - -Two stale justifications were corrected in passing, both the same defect class this ledger -exists to catch — a comment asserting an upstream type "erases to `any`" when it no longer -does. Left alone, the next triage checks the claim, finds it false, and lands the regression. -No published behaviour changes; no runtime code was touched. diff --git a/.changeset/3719-settings-valuedomain-combobox.md b/.changeset/3719-settings-valuedomain-combobox.md deleted file mode 100644 index f39d01bbcc..0000000000 --- a/.changeset/3719-settings-valuedomain-combobox.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Setup's settings selects now follow the specifier's `valueDomain` declaration instead of -treating the curated `options` table as the domain (objectui#3719). - -Since objectstack#5712 / PR objectstack#6581 a settings specifier may declare -`valueDomain` (`iana_time_zone` | `iso_4217_currency` | `iso_3166_alpha2`), and when it -does the **standard's membership is the enforcement boundary** — the server accepts -`timezone: 'Europe/Zurich'` and `currency: 'CHF'`, neither of which is in the manifest's -list. The console kept drawing those keys as closed dropdowns, so an admin could author -only the 17 curated zones and 9 curated currencies while the contract took the whole -domain; every other legal value was reachable by API or `OS_LOCALIZATION_*` env only. The -keys' own descriptions had promised "IANA zone" / "ISO 4217 code" all along. - -`case 'select'` in `SettingsField` now keys the control off the declaration. Declared → -an editable combobox: the curated options stay on as suggestions (native ``, the -same suggest-but-allow-anything affordance `FlowReferenceField` uses — no new dependency), -free text is committed verbatim, and an out-of-domain value is refused by the server with -`invalid_value` + `constraint: { valueDomain }` into the field-error slot that already -exists. - -**Undeclared → the closed dropdown is untouched**, which is half the change rather than a -caveat. Those `options` are still exhaustive under objectstack#5131 (the sms/mail provider -selects), and `localization.locale` had its domain declaration deliberately **rejected** in -objectstack#6515 because its options *are* the shipped catalogs. Widening those to free -input would be a regression wearing this fix's clothes, so the two branches are pinned -against each other from the specifier data rather than from a list of key names — a key -that gains a domain server-side joins the right side of the pin with no edit here. - -Root cause, because it will recur: `Specifier` in `pages/settings/types.ts` is a -hand-written **local mirror** of the server's shape, not an import, so nothing tells it when -the schema grows — and TypeScript reports nothing, because a narrower mirror is a -structurally valid reading of a wider object. `valueDomain` is added there and the file -header now says to check the mirror first when a settings feature "doesn't render". diff --git a/.changeset/3880-es-done-listo.md b/.changeset/3880-es-done-listo.md deleted file mode 100644 index 032c5075e8..0000000000 --- a/.changeset/3880-es-done-listo.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The Spanish pack renders `Done` as `Listo` at every one of the four sites that say it -(objectui#3880, triage adjudication 2026-08-09). `grid.bulk.done` — the footer button that -dismisses the bulk-action result dialog — read `Hecho` while `common.done`, `view.done` and -`form.fullscreen.done` all read `Listo`, so the same English word rendered two ways in -Spanish across dialogs a user meets in one session. - -Adjudicated a typo rather than a deliberate contextual split, on three checks. All four -keys hold the byte-identical `en` value `Done`, and all four call sites are the same -control: a dialog-footer button whose click finishes or dismisses the surface -(`BulkActionDialog` `onClose(result)`, `ManageViewsDialog` `onOpenChange(false)`, -`fullscreen-editor` `commitFullscreen`, `InviteMemberDialog`'s invitation-created footer). -The nine other packs each render all four identically (de `Fertig`, fr `Terminé`, pt -`Concluído`, ru `Готово`, ja `完了`, ko `완료`, zh `完成`, ar `تم`), so no other translation -pass had found a context worth splitting on. And the neighbouring `Hecho`/`Deshecho` -pairing that could have justified it does not hold: `grid.bulk.undo` is the verb `Deshacer`, -and `Deshecho: ` is `undonePrefix`, a result-line status rather than a button. - -`Hecho` moved to the 3:1 majority `Listo`, which is the value objectui#3546 slice seven had -already chosen for `common.done`. `packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -pinned the old outlier as a recorded example of deliberate divergence; that pin now asserts -the four as one value instead, and its note keeps the history plus the `Pending`/zh row, -which remains a genuine deliberate split. - -No `en` value changes, so no other pack is asked to follow. This is the value half of -objectui#3880 only — the card's 281/164 shared-string census stays on the card as -documentation, and is explicitly not a gate: 164 of those groups diverge legitimately. diff --git a/.changeset/3909-query-params-filter-union.md b/.changeset/3909-query-params-filter-union.md deleted file mode 100644 index e17e77ec51..0000000000 --- a/.changeset/3909-query-params-filter-union.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/fields': patch ---- - -`QueryParams.$filter` now declares both shapes the data sources actually accept — the -MongoDB-style field-keyed record, or a `FilterArray`, the ObjectQL AST sugar bound from -`@objectstack/spec/data` (objectui#3909). - -**Nothing is narrowed and no accepted value changes.** `Record` already -accepted arrays structurally — they satisfy its string index — so the union documents -shapes that were always legal rather than admitting new ones. Measured both ways under -`tsc --strict`: all five inputs `translateFilterToAST` enumerates assign to the old and -new declarations alike, and both reject a bare number and a bare string identically. A -downstream `turbo run build` over all 43 dependent packages is green, which is the -evidence a published type change breaks no consumer. - -The harm was entirely on the type face, and it was two-sided. The declaration blocked -nothing while describing one legal shape as though it were the only one — objectui#3831 -is what that cost, a rule array accepted by a `Record` slot, object-spread -flattened to `{"0": {...}}`, types green, and the query filtering on a column literally -named `0`. And someone writing a new consumer would read the type and its record-only -`@example`, conclude the array path was illegal, and add a tolerant conversion for it — -the "widen the consumer to tolerate the producer" shape AGENTS.md #0.1 forbids. Two -producers have fed arrays through this slot all along: `plugin-list`'s -`buildEffectiveFilter` (grid and export) and `plugin-view`'s `ObjectView` (calendar / -kanban / gallery / timeline). The runtime was right; the declaration was narrow. - -The array half is **bound** to the spec's `FilterArray` rather than restated locally, so -it cannot fork from the vocabulary the servers parse — the same failure two hand-written -operator lists had in objectui#3948. The doc comment names `translateFilterToAST` as the -authoritative accepted set instead of carrying a second list to drift from. - -`@object-ui/fields` drops the local cast this defect forced. PR objectui#3908 wrote -`filter as Record` at one assignment in `useRecordQuery`, deliberately, as -debt rather than widening the shared type. `hasFilter` is now a type predicate narrowing -to the `$filter` slot's own type, so the assignment needs no cast and the guard cannot -drift from the declaration it guards. Type-only throughout; no runtime behaviour changes. diff --git a/.changeset/3917-retire-action-condition-branch.md b/.changeset/3917-retire-action-condition-branch.md deleted file mode 100644 index 717b205f5b..0000000000 --- a/.changeset/3917-retire-action-condition-branch.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire the `ActionCondition` `{ expression, then, else }` branch shape from -`ActionSchema.condition` (objectui#3917, maintainer ruling 2026-08-09 route B, -enforce-or-remove). - -`@object-ui/types` declared `condition` as a branch DSL — `expression` plus `then` / -`else` sub-actions — and shipped a zod mirror (`ActionConditionSchema`) that accepted it. -**Nothing ever read `expression`, `then` or `else`**: a repo-wide grep for -`condition.expression|then|else` has zero non-test hits. The only consumer of the key is -`ActionRunner.execute`, which reads it as a **predicate gate** (boolean / bare CEL / -`${...}` template / `{ dialect, source }` envelope). A branch object carries no `source`, -so the runner's normalizer read it as "no gate declared" and executed the action -unconditionally: the predicate was never evaluated, `then` / `else` were never dispatched, -and `os validate` / `os build` stayed green with zero diagnostics. Two docs pages taught -the shape with worked examples, so an author following the documentation -("amounts over 1000 go to manager approval") got unconditional execution. - -What changes: - -- `ActionCondition` is removed from `@object-ui/types` (and from the barrel export). -- `ActionSchema.condition` is retyped to the predicate the runtime actually honours: - `boolean | string | { dialect?: string; source: string }` — the same three arms - `ActionRunner`'s own `ActionDef.condition` carries, and the same vocabulary `visible` - and `disabled` use. -- `ActionConditionSchema` is removed from `@object-ui/types/zod` (and from the zod - barrel); the `condition` key now validates against that predicate union. -- The two teaching sites (`content/docs/core/enhanced-actions.mdx` Conditional Execution, - `content/docs/api/schema-reference.md` ActionSchema table) are rewritten to the live - vocabulary: `condition` is a gate; a branch is expressed as separate actions with - mutually exclusive `condition`s. - -**The zod parse verdict flips in both directions**, measured on `origin/main` @ `2aff580b5` -against this branch: the branch object went from **accepted** to **refused** -(`invalid_union` on `condition`), and every live predicate spelling — `false`, -`'data.amount > 1000'`, `'${data.amount > 1000}'`, `{ dialect: 'cel', source: ... }` — -went from **refused** to **accepted**. The old schema required `expression`, so the shape -the runtime honours was the one the schema rejected. - -**Breaking for TypeScript authors of `ActionCondition` and for metadata authoring -`condition: { expression, then, else }`** — marked `minor` per this repo's -version-alignment rule, which reserves `major` for following `@objectstack` across a major -(AGENTS.md 版本号策略; same classification as the `MobileOverrides` retirement, -objectui#4919). Runtime behaviour is unchanged: an authored branch object did nothing -before and does nothing now. What changes is that the contract no longer claims otherwise -— the mistake now surfaces at authoring time as a type error and a named zod refusal, -instead of a silent no-op that type-checks, validates and runs the action anyway. diff --git a/.changeset/3965-mint-box-neutral-container.md b/.changeset/3965-mint-box-neutral-container.md deleted file mode 100644 index 816615d608..0000000000 --- a/.changeset/3965-mint-box-neutral-container.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -Mint the `box` component type — the class-transparent neutral block container -(objectui#3965, maintainer ruling 2026-08-29 方案 A). - -The JSON authoring vocabulary had no neutral block box, which is why the -deprecated `div` could never actually retire: every replacement the deprecation -notice names injects layout of its own (`container` adds width/centering and a -responsive padding ramp, `flex`/`stack` add a display mode and gaps, `grid` adds -`grid-cols-*`, `card` adds border/shadow and wraps children in a `CardContent` -element — all measured through the real `SchemaRenderer`). `box` closes that -gap with a three-clause contract, pinned in -`packages/components/src/renderers/__tests__/box-neutral-container.test.tsx`: - -1. renders `children`; -2. authored `className` passes through **verbatim**; -3. **zero** injected classes. - -Deliberately unlike `div`, `box` reads `children` only — never `schema.body`. -The `div` renderer's `children || body` fallback is what made a mechanical -`div`→X swap silently drop content on `body`-authoring nodes while the element -count stayed unchanged; content moves into `children` at migration time (the -objectui#6771 B-ruling direction). - -Landed on both contract faces per the zod-mirror-parity pairing (objectui#6424 -family form): `BoxSchema` interface in `@object-ui/types`, its zod mirror in -`@object-ui/types/zod`, the `SchemaRegistry['box']` entry, and the registry -registration (`namespace: 'ui'`, `isContainer: true`). With `box` landed, the -catalog's 25 remaining `div`-authoring fixtures (80 nodes) migrate to it -mechanically with zero render difference, and the catalog ratchet closes to -zero tolerance for JSON-authored `div`. diff --git a/.changeset/4420-bulk-delete-visiblewhen.md b/.changeset/4420-bulk-delete-visiblewhen.md deleted file mode 100644 index eafb37e6e6..0000000000 --- a/.changeset/4420-bulk-delete-visiblewhen.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-list': patch ---- - -The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen` -per selected record (objectui#4420). It used to read that key as a bare boolean — the -object-level verdict only — so ticking a record the author's predicate excludes still -offered the red Delete, and pressing it deleted the record the predicate was written to -protect. The row kebab on the same screen hid its Delete correctly, so one declared key -meant two different things on two surfaces. - -Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the -operation and report the skipped**. The bar evaluates the predicate once per selected -record, the delete runs over the allowed subset, and the excluded records are reported -rather than silently dropped. The button itself is never hidden or disabled by the -predicate — a mixed selection is not punished for one stray tick — and a selection where -every row is excluded is a legible refusal rather than an unexplained absence. - -- `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of - `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once. - A bulk gate evaluates N records in a loop, which is why it can never be a hook. -- `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through - `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped - count; a selection with nothing excluded keeps the consumer's own delete flow - untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` — - the bulk half of the same predicates, gated on the object verdict rather than on the - row `onDelete` wiring. The dialog declines to run over zero records. -- `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters - the built-in `delete` to the eligible subset and states the skipped count inline. - -Custom bulk action ids are untouched: they route through the action runner carrying -their own gates. This is a UI affordance — server enforcement was never the leak. diff --git a/.changeset/4535-export-options-spec-parity.md b/.changeset/4535-export-options-spec-parity.md deleted file mode 100644 index a71a44a7ce..0000000000 --- a/.changeset/4535-export-options-spec-parity.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`exportOptions`: the alignment claim is now checked against the installed spec, -not asserted in prose - -`ListViewExportOptions` mirrors `@objectstack/spec`'s -`ListViewSchema.exportOptions` object branch. The comment above it explained -that the five keys were restated rather than derived "because objectui still -pins `@objectstack/spec@17.0.0-rc.6`, whose `ListView.exportOptions` is the -LEGACY bare format array". - -That is no longer true. objectui installs `@objectstack/spec@17.2.0`, which -carries the object form: the bare format array lifts to `{ formats: [...] }` at -parse and `'pdf'` has left the enum. Nobody edited the comment; the dependency -moved underneath it. This is the second time this particular comment has gone -false — objectui#4535 was filed for the first — and both times the mechanism was -the same: a prose claim about another package's shape, with nothing that fails -when that shape changes. - -So the reason is corrected and, more to the point, it stops being load-bearing. -A new `export-options-spec-parity.test.ts` reads the object branch out of the -spec package that is actually installed, at test time, and asserts against it: -the key set, the format enum (`'pdf'` absent from both sides), upstream -strictness, the migration prescription the `'pdf'` refusal carries, and the -parse-time array lift. The key set is projected from `keyof -ListViewExportOptions` through an exhaustive `Record`, so a local key added or -dropped fails to compile rather than passing a comparison against a hand-copied -list. - -The mirror itself is unchanged, and stays a mirror for a measured reason the -test now pins: `ListViewExportOptionsSchema` is internal to the spec bundle and -is not one of the package's public exports, so there is nothing to import. Only -the enclosing `ListViewSchema` is exported, and its `exportOptions` is a -two-branch union whose inferred type is not this interface. When upstream -exports the symbol, the test says so by failing, and the mirror can go. - -Types are unchanged — no export added or removed, no key or union altered — so -this is a patch. The shape change was the earlier minor that introduced -`ListViewExportOptions`. diff --git a/.changeset/4664-related-list-declared-filter.md b/.changeset/4664-related-list-declared-filter.md deleted file mode 100644 index f8fffb8af0..0000000000 --- a/.changeset/4664-related-list-declared-filter.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-detail': minor -'@object-ui/components': minor ---- - -Auto-derived related lists consume the field-level `relatedListFilter` -declaration — the list query AND-composed, the tab badge counting the same set -(objectui#4664). - -A `lookup` / `master_detail` field may now declare `relatedListFilter`, a -canonical Query-DSL `FilterCondition` such as `{ status: { $ne: 'deleted' } }` -(`@objectstack/spec` 17.1.0 — objectstack#8704 / PR #8955). Until now this repo -accepted that key at every gate and consumed it nowhere: a record page's derived -related lists answered wider than the metadata asked, and the driving scenario — -soft-deleted child rows on auto-derived record pages — had no way to be -expressed at all. - -What ships: - -- `deriveRelatedLists` reads the key off the FK and carries it on the derived - descriptor; `RecordDetailView` forwards it into the page synthesizer, which - emits it onto the `record:related_list` node's **existing** `filter` key. That - key already had a read site (objectstack#7118): `RelatedList` ANDs it with - `{ [referenceField]: parentId }`. The declared filter is therefore an authored - constraint that may only NARROW this parent's children — never a replacement, - which would leak other parents' rows — and no second filter dialect appears - for derived pages. -- The **tab badge honours the same composed filter**. `page:tabs` reads the - `filter` off the `record:related_list` node it is badging and the count store - composes it with the parent scope through the same `mergeFilterNodes` sink the - row query uses, so the badge and the rows send one `$filter`. Badge-count - parity is normative in the spec key's own contract text; without this half the - feature would ship the defect it exists to prevent — a badge saying 7 above a - list showing 3. -- Counts cache per (object, relationship, parent, **scope**), so a filtered and - an unfiltered probe over the same relationship are separate entries rather - than one wrong number. - -With no `relatedListFilter` declared, the synthesized node, the row query and -the badge probe are byte-identical to before. Consumption only — this change -adds no authoring UI for the filter. diff --git a/.changeset/4730-objectview-config-keys-retired.md b/.changeset/4730-objectview-config-keys-retired.md deleted file mode 100644 index 16c14f7c04..0000000000 --- a/.changeset/4730-objectview-config-keys-retired.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The `console.objectView.*` config-panel vocabulary is retired — 116 keys removed from each -of the ten packs, 1160 translated strings that nothing read (objectui#4730, maintainer -ruling 2026-08-19). - -The namespace held 209 keys per pack. 116 of them labelled a view-configuration settings -panel that does not exist: appearance and density toggles, accessibility attributes, -conditional-formatting rules, row-action and inline-edit switches, quick-filter builders, -an advanced-settings tier. `packages/app-shell/src/views/ViewConfigPanel.tsx` — the panel -they were written for — was migrated off the legacy `buildViewConfigSchema` engine onto -`ViewVariantInspector`, a spec-driven inspector whose field labels come from -`@objectstack/spec` metadata rather than from this namespace. The panel was replaced; the -keys were not cleaned up with it. - -Removed under objectui#4658's three-legged evidence standard, re-measured on this branch's -merge base rather than inherited from the card: zero `t()`/`tt()` call sites, zero textual -occurrence of the dotted key anywhere in the repo outside the packs that define it, and a -consumer spot-check confirming no i18n wiring. The 93 live keys stay — the create-view -dialog fields, the view-type catalogue, `new`/`save`/`cancel`, the object-not-found copy, -plus the 38 keys whose spelling still appears somewhere the AST pass cannot see, which are -out of scope here. - -Four of the retired keys name `ListViewSchema` properties that are still active — -`rowActions`, `inlineEdit`, `hiddenFields`, `filterableFields`. They are retired anyway, by -the ruling's own words: a live schema property is not a consumer of a locale string; only a -labelled UI control is. If such a panel is ever specified, its keys are re-authored -alongside it. - -`packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts` pins the removal -by name. Every i18n gate in this repo runs call site → key, so none of them can see a dead -key come back: the parity gate is fully satisfied by 116 dead keys present in all ten packs, -and the reverse sweep that found them (`scripts/check-i18n-dead-keys.mjs`) is report-only by -design. The pin is the only thing that would notice. diff --git a/.changeset/4730-retire-dead-locale-key-batch.md b/.changeset/4730-retire-dead-locale-key-batch.md deleted file mode 100644 index c1b2ec6af5..0000000000 --- a/.changeset/4730-retire-dead-locale-key-batch.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/i18n': minor ---- - -Retire 25 confirmed-dead locale keys from all ten packs — 250 translated strings -with no reader anywhere in the repo (objectui#4730's key-level trim round; -`calendar.agenda` closes objectui#5783). - -Every key was confirmed individually, not swept from a tool's output. The -inventory comes from `scripts/check-i18n-dead-keys.mjs`, which stays report-only -by design, and each candidate then had to clear the objectui#4658 evidence -standard on its own: zero `t()` call sites, zero textual footprint anywhere -outside the packs, and a read of its plausible consumer confirming no i18n -wiring reaches it. Five namespaces held nothing but retired leaves and went with -them — `map`, `cellRender`, `rowAction`, `recordDetail`, and `home.stats`. - -The retirements fall into three shapes: - -- **Superseded twin vocabularies.** `cellRender.*` and `rowAction.*` duplicated - a `grid.*` vocabulary that won. `RowActionMenu.tsx` is fully i18n-wired and - reads `grid.openMenu` / `grid.edit` / `grid.delete`; `ObjectGrid.tsx` reads - `grid.empty` / `grid.yes` / `grid.no` / `grid.systemFields`. The twins had no - reader on either side. -- **Labels that outlived their control.** `calendar.agenda` labelled a view mode - objectui#5740 retired from `CalendarViewMode` (now `'month' | 'week' | 'day'`). - `home.quickActions.createApp*`, `layout.systemNav.createApp`, - `actionDialog.defaultActionTitle` / `.ok` and `grid.bulk.selectPlaceholder` - sit in namespaces whose consumers are live and wired but demonstrably read - other siblings. -- **Surfaces that left the product.** `map.*` is the strongest form: - `@object-ui/plugin-map` declares no `@object-ui/i18n` dependency and contains - no `t()` call at all, so it cannot consume a locale string. `home.stats.*` and - `recordDetail.viewersTooltip` name surfaces nothing renders. - -`packages/i18n/src/__tests__/dead-key-batch-retired-4730.test.ts` pins the -retirement, following the convention of the five retirement pins already in that -directory. It is load-bearing rather than decorative: every i18n gate in this -repo runs call site to key, so a dead key coming **back** into the packs is -invisible to all of them, and this pin is the only thing watching that direction. - -**Deliberately NOT deleted, and pinned as live.** Seven `console.*` bootstrap -strings that this same sweep reported CONFIRMED-dead are in fact **live**, and -were pulled back out of the batch. `LoadingScreen.tsx` is bootstrap-critical UI -that must render before i18n loads — precisely when the server is unreachable — -so it deliberately does not call `t()`. It imports the packs directly and reads -them as plain object properties (`strings.loadingSteps.connecting`). That -consumer is invisible to both legs of the sweep: there is no call for the AST -pass to classify, and the full dotted key is never spelled in source because the -namespace segment is bound to a local variable. The new pin asserts those keys -stay, so the next sweep round cannot repeat the mistake. diff --git a/.changeset/4783-drawerform-shared-container-grid-cols.md b/.changeset/4783-drawerform-shared-container-grid-cols.md deleted file mode 100644 index 2dfc9e442d..0000000000 --- a/.changeset/4783-drawerform-shared-container-grid-cols.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Internal only, no release: `DrawerForm.tsx` now imports `CONTAINER_GRID_COLS` from -`./autoLayout` instead of keeping its own private copy of the table -(objectui#4783 observation 二). `ModalForm.tsx` already consumed the shared -export from the same import block, so this makes the two siblings agree on where -the container-query grid classes come from. - -The copy was verified byte-identical before deleting it, not assumed to be — -the finding was filed on 2026-08-16 and a drifted copy deleted as "cleanup" -would ship a silent behaviour change. Both declarations were extracted -mechanically and compared: with the leading `export ` keyword stripped, the two -six-line blocks hash to the same SHA-256 -(`e41f57cf2cd6ce0966a1fcbf27c78c56ca1abc4236015bc12b3189146837ecbf`), so all four -column entries — including the `1: undefined` row that makes callers fall back to -the single-column stack — are unchanged for every reader. Both drawer call sites -index the table exactly as before; only the binding's origin moved. - -No user-visible change, hence empty frontmatter: same class strings, same -lookups, same rendered markup. diff --git a/.changeset/4795-bindable-text-keys.md b/.changeset/4795-bindable-text-keys.md deleted file mode 100644 index c650b7b171..0000000000 --- a/.changeset/4795-bindable-text-keys.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/react': minor ---- - -Expression-bindable text keys: `statistic.value`, `card.title`, `button.label` -and their siblings now evaluate `${...}` on the node (objectui#4795 Direction 1, -maintainer ruling 2026-08-25). - -**What changes for you.** Four text keys — `title`, `label`, `value`, -`description` — can now carry an expression written directly on the component -node, on the component types that declare them: - -| Component | Bindable node keys | -|---|---| -| `statistic` | `label`, `value`, `description` | -| `card` | `title`, `description` | -| `button` | `label` | - -```json -{ "type": "statistic", "label": "Active users", "value": "${data.metrics.active}" } -``` - -That node used to render the literal text `${data.metrics.active}`. A dashboard -`statistic` previously had no way at all to bind a dynamic number — the -documented workaround (moving the key under `props`) evaluated the value and -then discarded it, painting a blank card instead. Both shapes are fixed by the -same change: the value is evaluated once, at the single place that produces -evaluated schema, and lands where the renderers already read. - -**No component behaviour changed.** `statistic.tsx`, `card.tsx` and `button.tsx` -are untouched — they always read these keys off the node; nothing was writing an -evaluated value there. - -**Scope, and how it grows.** The list is closed and lives in -`@objectstack/spec` (`EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT`); the renderer -reads that declaration rather than keeping a copy. On any other component type -these four keys are still read raw, so an expression reaches the screen as -literal text — notably `text`, whose `value` is read but has no declaration. -Adding a type or a key is a change to the spec, never something the renderer -infers. - -**Nothing is newly rejected.** This release only widens what evaluates; no -metadata that used to render now fails to. The build-time rejection of `${...}` -in undeclared keys — the second half of the same ruling — is not in this release -and is still open. - -Published authoring guidance updated to match: `skills/objectui/rules/protocol.md` -(new "Bindable Text Keys" rule), plus the `page-builder`, `schema-expressions` -and `data-integration` guides, which taught the now-retired "never evaluated" -statement and its host-pre-resolution workaround. diff --git a/.changeset/4830-app-shell-branding-rightrail-prose.md b/.changeset/4830-app-shell-branding-rightrail-prose.md deleted file mode 100644 index 6f87a23a0e..0000000000 --- a/.changeset/4830-app-shell-branding-rightrail-prose.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Documentation-only: `content/docs/layout/app-shell.mdx` now describes `branding` and -`rightRail` in prose, not just in the `AppShellProps` key table. The page's Features and -Styling sections had never mentioned either prop — the table half landed with objectui#4808 -while the prose was deliberately held back until objectui#4818 ruled on -`AppShellBranding.logo`. That ruling was REMOVE, so the new copy describes the -post-removal surface: the four fields `useAppShellBranding` actually reads -(`primaryColor`, `accentColor`, `favicon`, `title`), which Shadcn theme tokens each colour -writes (`--primary` / `--primary-foreground` / `--ring` / `--sidebar-primary` / -`--sidebar-ring` and `--accent` / `--accent-foreground`), and that `rightRail` renders as -a flex sibling that reflows the content rather than overlaying it (ADR-0057 P3a). Every -sentence was checked against `packages/layout/src/AppShell.tsx` and -`packages/components/src/index.css` rather than against the key table — that is the -failure this page has already been corrected for four times. No published behaviour -changes and no package source was touched. diff --git a/.changeset/4850-misplaced-eslint-disable.md b/.changeset/4850-misplaced-eslint-disable.md deleted file mode 100644 index 06a3767dab..0000000000 --- a/.changeset/4850-misplaced-eslint-disable.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Comment-only fix: two `eslint-disable-next-line` directives were placed on the -wrong line and had never suppressed anything since they landed. - -- `apps/console/src/pages/developer/PublicFormsPage.tsx` — the - `react-hooks/exhaustive-deps` directive was an inline block comment inside the - `useEffect(() => { load(); }, [])` statement, so its "next line" was a blank - line, not the statement itself. Moved above the statement with the reason - written out (mount-once by design; refresh is explicit via the Refresh - button and post-mutation `await load()` calls). -- `packages/react/src/SchemaRenderer.tsx` — the - `@typescript-eslint/no-explicit-any` directive's `--` reason wrapped onto a - second comment line, so its "next line" was that continuation comment, not - `type ForwardedProps = Record;`. Collapsed to one line so the - directive is immediately above its target. - -No logic changes. Verified with `eslint --report-unused-disable-directives`: -both directives are now effective (the two warnings they were meant to -suppress are gone) and neither directive is reported unused. diff --git a/.changeset/4894-shared-registration-key-reader.md b/.changeset/4894-shared-registration-key-reader.md deleted file mode 100644 index 1f8cca1445..0000000000 --- a/.changeset/4894-shared-registration-key-reader.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-and-tooling change only; no published behaviour changes. The four pins that read -"which component keys does `packages/layout/src/index.ts` register?" now share one reader -(`scripts/component-registrations.mjs`) instead of four copies of a regex that accepted a -single-quoted key and nothing else — a double-quoted registration was legal, lint-clean -and invisible to all four at once (objectui#4894). diff --git a/.changeset/4895-retire-block-schema-family.md b/.changeset/4895-retire-block-schema-family.md deleted file mode 100644 index 67af25b994..0000000000 --- a/.changeset/4895-retire-block-schema-family.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': patch ---- - -Removed the block schema family (objectui#4895, ADR-0049 enforce-or-remove, maintainer -ruling 2026-09-02 — option C1, retire the family in one change, no transition window). - -**Breaking on a published surface, deliberately.** These names are gone from -`@object-ui/types`, from both subpaths that carried them: - -- `.` (types): `BlockSchema`, `BlockSlot`, `BlockLibrarySchema`, `BlockEditorSchema`, - `BlockInstanceSchema`, plus the support types with no other reader — `BlockVariable`, - `BlockMetadata`, `BlockLibraryItem` — and `ComponentSchema`. -- `./zod` (runtime validators): `BlockVariableSchema`, `BlockSlotSchema`, - `BlockMetadataSchema`, `BlockSchema`, `BlockLibraryItemSchema`, `BlockLibrarySchema`, - `BlockEditorSchema`, `BlockInstanceSchema`, `ComponentSchema`, and the - `BlockComponentSchema` union over them — which was also `AnyComponentSchema`'s block arm. - -The zod half is the one that mattered. `AnyComponentSchema.safeParse({ type: -'block-library' })` returned **success** on 17.6.0 for a node no page can render, so an -author who copied the documented shape was told green by the shipped validator and then got -the registry's `OBJUI-001` "Unknown component type" panel. Validated-then-broken is worse -than never-validated, because the green light is what the author trusted. All five -discriminants — `block`, `block-library`, `block-editor`, `block-instance`, `component` — -are now **refused**, pinned in `phase2-schemas.test.ts` alongside the theme refusals -retired the same way. - -Evidence the family was declared-but-unenforced: zero `ComponentRegistry.register(...)` -sites claimed any of the five keys (positive control `'table'` resolves to two), zero -renderers, and zero readers outside `packages/types/src`. The liveness pass this card's -earlier deferral was keyed to (objectui#6935) established that external consumption of this -package is structurally unmeasurable — the certainly-live control `TableSchema` returns the -same zero external consumers — so the ruling was taken on the evidence in hand rather than -on a deferral whose exit cannot fire. - -⚠️ **Not this family, and not touched.** The live slotted record-page vocabulary — -`PageNodeSchema.kind === 'slotted'` with `slots?: PageSlotMap` (`packages/types/src/layout.ts`), -rendered by `usePageAssignment` / `PageBlockCanvas` / `PageBlockInspector` in -`@object-ui/app-shell` — shares the words "block" and "slot" with the retired family and -shares no declaration, type or file with it. Neither is the `type: 'component'` NAVIGATION -item kind (`{ type: 'component', componentRef }`, `NavigationItemSchema` in -`zod/app.zod.ts`), a different declaration in a different module. - -`@object-ui/components` carries one forced consequence: `renderers/feedback/empty.tsx` -annotated its `action` child as the retired `ComponentSchema` and now says `SchemaNode`, -the node type `SchemaRenderer` actually takes. - -`packages/types/src/blocks.ts` and `packages/types/src/zod/blocks.zod.ts` are kept as -ADR-0049 tombstones exporting nothing, and `block-family-retired-4895.test.ts` pins every -retired name out of them. `content/docs/blocks/block-schema.mdx` is deleted with the family, -and objectui#7023 — the narrower validator-only fix — dissolves into this retirement. diff --git a/.changeset/4905-inline-default-value-pin.md b/.changeset/4905-inline-default-value-pin.md deleted file mode 100644 index 266fe1250d..0000000000 --- a/.changeset/4905-inline-default-value-pin.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch ---- - -Inline `t(key, { defaultValue })` strings are now held to the one placeholder spelling a -provider-less host can resolve, and five of them are pinned to the pack value for the first -time (objectui#4905). - -`check:i18n-keys`' `default-value-drift` class pins an inline default byte-identical to its -`en` row, and objectui#3512 holds `en` to the one spelling `createSafeTranslation`'s -`fallbackT` interpolates — so most inline defaults were covered transitively. Re-measured -on this tree, 66 of 1003 were not: three literal defaults on dynamic keys, and 63 written -as a computed expression. A new `unresolvable-default-spelling` class in -`scripts/check-i18n-call-site-keys.mjs` now judges the text every inline default carries — -the folded sentence, or a template literal's static segments — so `{{ name }}`, -`{{count, number}}`, `{{- name}}` and `$t(key)` are refused wherever they are written, -rather than only inside a copy table. - -Four call sites gain a real pin because their default carries no placeholder at all: the -record-form submit button now falls back to `Update`/`Create` (the pack's wording) instead -of `Save`/`Create` via a nested `t('common.save')`, the context selector's package label -and the approvals separator now state their literal. One more (`detail.showEmptyFields`) -is behind a `createSafeTranslation` hook, whose fallback does interpolate, so it can safely -say what the pack says. - -The other 61 are deliberately left computed, and the measurement behind that is the useful -part: react-i18next's not-ready `t` returns `options.defaultValue` **verbatim, without -interpolating it**. At a call site bound to a bare `useObjectTranslation()`, a default -written as `` `Signed in as ${user.email}` `` is therefore the only form that renders -correctly with no provider — rewriting it to `'Signed in as {{email}}'` would put literal -braces in front of the user, which is the exact defect this family of cards exists to -prevent. Those sites keep their template literals and are covered by the spelling class -instead. diff --git a/.changeset/4918-stale-strip-mode-prose.md b/.changeset/4918-stale-strip-mode-prose.md deleted file mode 100644 index 5f16c6e8bb..0000000000 --- a/.changeset/4918-stale-strip-mode-prose.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in `@object-ui/plugin-detail`'s spec-parity tests. The -`relationshipValueField` block in `recordRelatedListInputs.spec-parity.test.ts` and the -`hideFields` block in `recordDetailsInputs.spec-parity.test.ts` each narrated strip mode -as a present-tense fact about the installed `@objectstack/spec` — that an undeclared -top-level key parses green and is silently dropped from `data`. The props schemas were -closed upstream, so an undeclared key now draws a named `unrecognized_keys` refusal, and -both comments read as current while being false against the installed pin. Both now state -the pin-independent verdict (an undeclared top-level key is never authoring surface) and -note that the contract expresses it two ways depending on the installed pin, pointing at -`specRefusesUnknownTopLevelKeys` in the sibling `recordHighlightsInputs.spec-parity.test.ts` -as the behavioural probe. This also settles `recordDetailsInputs` disagreeing with itself, -its section-key block having already taken the pin-aware disposition. No assertion changed -and no published behaviour changes. diff --git a/.changeset/4919-retire-mobile-overrides.md b/.changeset/4919-retire-mobile-overrides.md deleted file mode 100644 index 2e0d9a4457..0000000000 --- a/.changeset/4919-retire-mobile-overrides.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/mobile': minor ---- - -Retire the `MobileOverrides` type and its `mobileOverrides` mount point (objectui#4919, -maintainer ruling 2026-08-19, ADR-0049 enforce-or-remove). - -`MobileOverrides` published a six-key mobile override surface — `layout`, `columns`, -`useBottomSheet`, `fullScreen`, `touchTarget` and a three-value `navigation` vocabulary -(`'bottom-tabs' | 'hamburger' | 'drawer'`) — from `@object-ui/types` and, re-exported, -from `@object-ui/mobile`. Nothing read any of it. Measured on current `main`: the type had -exactly four mentions repo-wide — its own declaration, the single -`MobileComponentConfig.mobileOverrides` mount point, and the two barrel re-exports — and -the lower-case property name (the spelling a renderer would actually read) appeared only -in that declaration. No renderer, hook or adapter resolved it, and a sweep of the example -apps and the `objectstack` sibling checkout found zero authors. The three `navigation` -values were three spellings of the same no-op. - -The declared surface is removed rather than narrowed. The #3985 lineage's rule is "narrow -to the implemented values"; here the implemented set is empty, so that rule terminates in -deletion — a config that type-checks, builds and silently does nothing is the -declare-without-enforce shape the platform doctrine forbids. - -Removal rather than a `?: never` tombstone follows this package's own discriminator. A -tombstone exists to steer authors to a named live replacement — `crud.ts` `confirm` → -`confirmText` (objectui#4314), `data-display.ts` `hoverable` / `striped` → `data-table` -(objectui#5474) — or to keep a key loud that the docs had actively taught as working. -Neither applies: there is no replacement key to steer to, no documentation ever described -the surface, and there is no successor spelling. That is the same zero-pull, no-successor -shape as the retired `AccordionItem.icon` (objectui#4652) and `ToggleGroupItem.icon` -(objectui#4632), both of which were removed outright rather than tombstoned. - -**Breaking for TypeScript authors of `MobileOverrides` / `mobileOverrides` only** (marked -`minor` per this repo's version-alignment rule, which reserves `major` for following -`@objectstack` across a major — see AGENTS.md's 版本号策略, and the identical -classification used for `AccordionItem.icon`). Runtime behaviour is unchanged: an authored -`mobileOverrides` did nothing before and does nothing now. What changes is that the -contract no longer claims otherwise, so the mistake surfaces at authoring time — importing -the type is now a "has no exported member" error, and authoring the key on a -`MobileComponentConfig` object literal is an excess-property error, instead of a silent -no-op that type-checks and builds. - -If real mobile-override renderer work is ever wanted it re-enters deliberately, as designed -product surface on its own card, with the renderer landing in the same change as the -declaration — not by resurrecting this declaration. diff --git a/.changeset/4929-plugin-published-stylesheets.md b/.changeset/4929-plugin-published-stylesheets.md deleted file mode 100644 index b2cb6e0b03..0000000000 --- a/.changeset/4929-plugin-published-stylesheets.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch ---- - -`@object-ui/plugin-grid` and `@object-ui/plugin-kanban` now publish a stylesheet — -`"./style.css"`, mapped to `dist/index.css` and compiled at build time from the package's -own sources (objectui#4929, maintainer ruling 2026-08-17, Direction 1). - -**What was broken.** Only `@object-ui/components` and `@object-ui/fields` shipped CSS, and -each scans its own `src` only, so a class used exclusively by a plugin could not appear in -either sheet BY CONSTRUCTION. A published-state Vite app that installed one of these two -plugins and followed the quick-start rendered the grid or the board with **25 themed -utilities that had no source anywhere in the world** — `bg-muted/10`, `bg-card/60`, -`text-muted-foreground/60`, `ring-primary/40` and friends, ordinary appearance classes — -plus ~103 plain ones. Re-measured on the merged tree: the 21 the card listed all still hold, -and four more (`[&>h3]:text-foreground/80`, `border-l-primary/40`, `border-primary/30`, -`hover:text-primary`) that its literal-grep method could not see. - -The plain utilities a consumer could in principle regenerate by pointing `@source` at the -package's `dist`. The themed ones they cannot, at all: they resolve `@theme` tokens declared -in `packages/components/src/index.css`, which that package does not publish. A build inside -this monorepo is their only possible producer — which is why the fix is a stylesheet we -ship, not documentation teaching consumers to hand-declare the theme and scan -`node_modules` (the advice objectui#4858 had just retired from the guides). - -**The shape**, inherited from `@object-ui/fields` (objectui#4059): each package gains -`src/index.css` that `@reference`s the components entry — theme tokens, the class-based -`dark` variant and the animate plugin become available for resolution while emitting -nothing — plus `scripts/build-css.mjs`, which subtracts every rule components' published -sheet already ships. So these are **supplements, imported after** the components sheet, and -they are 16.30 kB and 11.41 kB rather than another ~170 kB each: - -```css -@import 'tailwindcss'; -@import '@object-ui/components/style.css'; -@import '@object-ui/fields/style.css'; -@import '@object-ui/plugin-grid/style.css'; -@import '@object-ui/plugin-kanban/style.css'; -``` - -Add a line only for the plugins you install; no other `@object-ui/plugin-*` package -publishes a stylesheet yet. The build step is shared -(`scripts/build-plugin-stylesheet.mjs`) so it is the pattern the next one inherits rather -than a file to copy, and it refuses to write a sheet that fails any of four assertions — no -rule may vanish, the subtraction must have removed something, the class count may not pass -a leak ceiling, and named themed utilities only this build can produce must still be -present. - -Nothing is removed and no existing import changes: a consumer who does not import the new -sheets is exactly where they were, and the guides' "do not scan `node_modules`" advice -stays correct — it is now correct for plugins too. diff --git a/.changeset/4934-dropped-fields-reason-boundary.md b/.changeset/4934-dropped-fields-reason-boundary.md deleted file mode 100644 index 3cbc10d5ce..0000000000 --- a/.changeset/4934-dropped-fields-reason-boundary.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -Parse a write-strip's `reason` against the spec enum at the boundary -(objectui#4934). - -`notifyDroppedFields` filtered a create/update response's `droppedFields` on -SHAPE alone — a hand-written `e is DroppedFieldsEvent` guard that checked -`Array.isArray(fields)` and nothing else — so a `reason` outside -`'readonly' | 'readonly_when' | 'primary_key'` reached every subscriber typed as -though it were inside the union. A deployed client normally runs BEHIND the -server it talks to, so a reason from the future is the expected skew direction, -not a corrupt payload; the interior was typed to trust a union no one had -checked, and nothing in the repo could say so. `notifyBatchDroppedFields` did -the same through its `entry as DroppedFieldsEvent & { index?: number }` cast. - -Both paths now read `reason` against `DroppedFieldsEventSchema.shape.reason` — -the enum the installed pin declares, derived rather than restated, so a pin bump -that adds an arm widens the accept set on its own: - -- **Every entry is kept.** Dropping the unparsable ones would tell the user - nothing about fields the server really did strip, which is exactly the silence - objectui#3484 removed. -- An unrecognized `reason` arrives on a named skew arm, - `UnrecognizedDropReasonEvent`, carrying `UNRECOGNIZED_DROP_REASON` plus the - wire value **verbatim** in `unrecognizedReason` — never coerced onto a known - arm, because claiming `readonly` for a reason we cannot name is a false - statement about the user's data. -- `WriteWarningEvent['droppedFields']` is therefore the two-arm - `DroppedFieldsNotice`. The spec type stays the canonical arm and is not - widened to `string` (objectui#3160): the skew arm is not assignable to - `DroppedFieldsEvent`, so a consumer branching on `reason` now hears about - server skew from `tsc` instead of from a per-consumer discipline. - -Runtime wording is unchanged: the one reader, the app shell's write-warning -toast, already answered an unrecognized reason with its cause-free line. - -**Blast radius — the compile error IS the intended signal, not a regression.** A -consumer that branches exhaustively on `reason` — a parameter, a `Map` key or a -`Record` annotated `DroppedFieldsEvent['reason']` — stops compiling against this -release, with a `TS2345` at each such site. That error is the notification, and -the only one: the skew arm is deliberately NOT assignable to the spec union, so -`tsc` reports server skew at the one place the wire is read rather than leaving -it to a per-consumer discipline. Do not cast it away. Widen the annotation to -`DroppedFieldsNotice['reason']`, and where the two arms have to be told apart, -narrow with `entry.reason === UNRECOGNIZED_DROP_REASON` and read the wire value -verbatim from `unrecognizedReason`. - -Widen the LOOKUPS, not the table. A `Record` that must stay exhaustive over the -SPEC arms keeps its `DroppedFieldsEvent['reason']` key: widening that one would -trade away the guarantee that a newly pinned spec reason fails `type-check` -unworded (objectui#3935). - -In this repo the entire blast radius is the app shell's write-warning toast — -two type annotations, no runtime change. Its executable JavaScript is byte-identical -and its wording tests pass unchanged, because the file was already written for -this value: its own docstring says the runtime `reason` may sit outside the spec -union and that the cause-free fallback is reachable, not dead. Only the parameter -and the `Map` key had been left narrower than that documented contract. diff --git a/.changeset/4935-element-filter-comment-residue.md b/.changeset/4935-element-filter-comment-residue.md deleted file mode 100644 index caebf8b3b5..0000000000 --- a/.changeset/4935-element-filter-comment-residue.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Comment-only cleanup in `@object-ui/components`: the `elements.tsx` header note no -longer lists `element:filter` among the heavier interactive elements said to live in -their owning plugins. No such file ever existed for it, and the element was retired at -element grain upstream (ADR-0049), so the type is gone from `PageComponentType` -entirely. No published behaviour changes. diff --git a/.changeset/4938-timeline-dead-example-link.md b/.changeset/4938-timeline-dead-example-link.md deleted file mode 100644 index f000f50e64..0000000000 --- a/.changeset/4938-timeline-dead-example-link.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Doc-only fix in `@object-ui/components`: `packages/components/src/renderers/complex/TIMELINE.md` -linked `../../examples/prototype/src/App.tsx` for "comprehensive examples of all -three timeline variants in action" — that example app was deleted whole in -`3aa84cee0` with no successor at the same path, and no gate had ever opened this -file, so the link sat dead with nothing to notice (objectui#4938). Repointed at -the interactive demo the docs site now serves for the same three variants -(`https://www.objectui.org/docs/plugins/plugin-timeline`). - -No source or behaviour change; text only. diff --git a/.changeset/4947-tombstone-judge-convergence.md b/.changeset/4947-tombstone-judge-convergence.md deleted file mode 100644 index 7f6937b240..0000000000 --- a/.changeset/4947-tombstone-judge-convergence.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-infrastructure convergence, no shipped code touched: the last two local copies of -the ADR-0087 D2 tombstone criterion — in `packages/plugin-detail`'s -`recordDetailsInputs` spec-parity test and `packages/app-shell`'s `block-config` -preview test — now import `@object-ui/test-support`'s shared judge instead of -spelling out `unwrap()._def.type === 'never'` by hand. Both copies carried the -structural channel alone; the shared judge OR-s it with the `[REMOVED]` description -channel, so neither channel can go quietly permissive on its own, and every -tombstone-aware gate in the repo now moves together when the criterion changes. diff --git a/.changeset/4971-declared-arm-subset-gate.md b/.changeset/4971-declared-arm-subset-gate.md deleted file mode 100644 index 1d3b39bc39..0000000000 --- a/.changeset/4971-declared-arm-subset-gate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Test-only: `registry-inputs-spec-parity.test.ts` gains a THIRD direction — every coarse -arm a block declares on a key must be one `@objectstack/spec` accepts there (objectui#4971). - -objectui#3832 gave `ComponentInput.type` the array form so a union key can declare its real -arms, and in doing so created a second way to disagree with the contract. The two are not -symmetric: declaring FEWER arms than the spec accepts produces NOISE (the manifest gate warns -on a legal write — audible), while declaring an arm the spec REJECTS is SILENT — `checkType` -clears a value the contract refuses, the manifest and the generated `.d.ts` publish it as -legal, and `declared = enforced` inverts with nothing to announce it. Measured on #3832's own -branch: a fake `'object'` arm on `element:text_input.defaultValue` reddened that block's -per-block test, while a fake `'number'` arm on `page:card.title` left all 856 tests green — -the property held by per-block discipline, not by a gate. It is one-directional for that -reason. - -An arm names a value's KIND, never its domain (`ComponentInput.type`, maintainer ruling -2026-08-17), so the gate refutes an arm only when the contract refuses the KIND — read off the -parse ISSUES rather than a boolean, scoped to the key so a required sibling cannot speak for -it, and recursing through a union's branches. A refusal of the VALUE leaves the arm standing, -which is what keeps a correct `'string'` arm on a spec-enum key from reading as invented; the -same rule still refutes a `'number'` arm there, and both halves are pinned by name. An `enum` -arm is judged exactly instead — every declared member must be a value the spec accepts. - -Two red-on-arrival findings, reported rather than declared away: `element:number.filter` -(objectui#6206) and `object-grid.data` (objectui#6207), both carried as reasoned, issue-backed -exemptions that a stale-exemption test deletes the moment either side moves. diff --git a/.changeset/4991-lucide-pin-range-consistency.md b/.changeset/4991-lucide-pin-range-consistency.md deleted file mode 100644 index 9982eb31c2..0000000000 --- a/.changeset/4991-lucide-pin-range-consistency.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change to `packages/cli/src/__tests__/app-generator.test.ts`: the -`lucide-react` manifest pin, and the pre-fix reverse-verification's drift -check, no longer read an arbitrary member off `inRepoRangesOf(...)` without -first asserting the repo's in-repo declarations for that dependency actually -agree. No published behaviour changes. diff --git a/.changeset/5034-navigateonsuccess-url-contract.md b/.changeset/5034-navigateonsuccess-url-contract.md deleted file mode 100644 index 567b1f9cf8..0000000000 --- a/.changeset/5034-navigateonsuccess-url-contract.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@object-ui/plugin-form": minor -"@object-ui/types": minor ---- - -`navigateOnSuccess` is relative-only, escapes the interpolated id, and is deprecated in favour of `submitBehavior` - -The url contract for this key was undeclared: it was same-origin-guarded (so a same-origin -ABSOLUTE value was accepted), it interpolated `{id}` / `{recordId}` without escaping the -substituted value, and nothing said which of those was intended. The maintainer ruled it on -2026-08-17: `navigateOnSuccess` is the pre-ruling ancestor of the `submitBehavior` family -rather than a second dialect, so as a compat alias it runs under the semantics -objectstack#7496 ruled for that family. - -**Relative paths only.** A same-origin absolute such as `https://own-host/record/{id}` is -now refused like any other out-of-contract value, rather than accepted and navigated at -browser level. The destination is authored metadata, which is exactly where an address -somebody else chose gets copied in. Cross-origin and protocol-relative values were already -refused and still are; every relative shape that worked before still works. - -**The interpolated id is URL-escaped.** `/r/{id}` with an id of `a/b c` resolved to -`/r/a/b c`, silently growing a path segment, and a template of `{id}` let the id become the -whole destination. The substituted value now goes through `encodeURIComponent`, so a token -is a value in the path and never a way to add path structure. The template is the author's -and is untouched — only the id, which is data read off the written record, is escaped. - -Both halves are needed and neither implies the other: relative-only is a rule about where a -destination starts, so it cannot see structure injected further along; escaping runs only on -the substituted value, so it cannot see an absolute the author wrote out. - -This can only narrow what is reachable. Every destination the key now accepts is a relative -reference, and a relative reference cannot carry an authority, so it was already accepted by -the same-origin guard this replaces — no value that was refused is now followed. With every -accepted destination relative, the browser-level `window.location.assign` fallback at both -call sites became unreachable and was removed; an accepted destination goes to the injected -navigation seam, and the absent-seam fallback inside the shared hook is unchanged. - -**Deprecation.** `navigateOnSuccess` is marked `@deprecated` in favour of `submitBehavior`, -which already takes precedence over it and carries the richer `{{record.field_name}}` -interpolation. The `{id}` / `{recordId}` dialect keeps working for forms that already -declare it — the ruling converges the documentation and the semantics, not the spelling. diff --git a/.changeset/5106-doc-key-table-scan-surface.md b/.changeset/5106-doc-key-table-scan-surface.md deleted file mode 100644 index 6dc86a21d0..0000000000 --- a/.changeset/5106-doc-key-table-scan-surface.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Tooling only, no published surface. `scripts/check-doc-component-types.mjs` now -reads the plugin KEY TABLES in `content/docs/**` — the canonical -`| Namespaced key | Bare-name fallback | Renderer behind it |` form the -objectui#5002 family standardised on — and judges both halves of every row, -including the namespaced one the gate never judged at all. Also fixes a floor -that named a counter which never existed (`docFiles`), so the check that catches -the docs walk finding nothing had been inert, and adds a guard that makes the -same mis-key fail loudly instead of silently passing. diff --git a/.changeset/5120-retire-data-table-name-alias.md b/.changeset/5120-retire-data-table-name-alias.md deleted file mode 100644 index e9397beb38..0000000000 --- a/.changeset/5120-retire-data-table-name-alias.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -'@object-ui/components': minor ---- - -**Breaking for authored metadata:** a `data-table` column spelled `name` no -longer resolves its cells. Use the declared `accessorKey`. - -`data-table`'s column normalization used to read -`accessorKey: col.accessorKey || col.name` — but `TableColumn` -(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared -surface admitted one spelling while the runtime admitted two, which is the -second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of -2026-08-20 settled the direction for the whole family: retire the consumer-side -alias, translate at the producers. `label` → `header` retired first -(objectui#5351); this retires `name` → `accessorKey` and closes the family. - -**Who is affected — a column authored DIRECTLY onto a `data-table` node:** - -```json -{ "type": "data-table", - "columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated -``` - -becomes - -```json -{ "type": "data-table", - "columns": [{ "header": "Email", "accessorKey": "email" }] } -``` - -**Who is NOT affected.** Columns reaching the table through `object-data-table`, -a detail view's `related[]` list, or `object-grid` are unchanged — the adapter -never sees a legacy spelling from any of them. The reason differs by producer, -and the difference matters if you are debugging one: - -- `object-data-table` and a detail view's `related[]` list **resolve** the - legacy spelling before delivery, stamping `accessorKey` from `name` (via - `columnIdentity`). A `name`-spelled column keeps working there. -- `object-grid` **refuses** it instead: since objectui#5068 an authored column - must spell the declared `field`, and one that does not is dropped at intake - and never reaches the table. Its delivered columns carry `accessorKey` - stamped from `field`. So a `name`-spelled `object-grid` column does not - render today either — that is objectui#5352's open question, unchanged by - this release. - -Only the directly-authored `data-table` node narrows here. - -**How the break presents, so you can recognise it:** the column is not dropped -and nothing is thrown — its header still renders over blank cells, and -neighbouring columns are unaffected. If a table's header row looks right but one -column's cells are empty, check that column's key spelling first. - -The two published skill guides that taught the `name` spelling -(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate -in this same release, so the platform never refuses a spelling it still ships. - -Graded `minor`, not `patch`: this narrows the accepted input set, which is a -breaking change for any author who used the tolerated spelling. It is not -`major` per this repo's fixed-group convention (objectui's own breaking changes -ship as `minor`; the group's major tracks `@objectstack`). diff --git a/.changeset/5213-widget-ts-retired-spec-anchor-history.md b/.changeset/5213-widget-ts-retired-spec-anchor-history.md deleted file mode 100644 index 3571ee7fdb..0000000000 --- a/.changeset/5213-widget-ts-retired-spec-anchor-history.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`RuntimeWidgetManifest` / `RuntimeWidgetSource` document the spec's retired -`WidgetManifest` / `WidgetSource` as HISTORY, instead of describing them as a -live schema (objectui#5213). - -Both JSDoc blocks were written while `@objectstack/spec/ui` still exported a -field-widget-plugin `WidgetManifest` and a `WidgetSource` union — they said the -local types were "renamed off the spec's name", in the present tense, and the -manifest block enumerated the spec shape key by key (`fieldTypes`, `category`, -`lifecycle`, `events`, `properties`, `implementation`, `screenshots`, `license`, -`aria`, `performance`). Protocol 17 retired that entire widget-registration -vocabulary under ADR-0049 enforce-or-remove (objectstack#5055): the installed -`@objectstack/spec` 17.2.0 exports none of those names, and its own tombstone -records why there is nothing to migrate — no schema ever declared a carrier key -of a widget shape, so the record is the D3 `SemanticMigration` -`ui-widget-i18n-family-retired` plus `ui/WidgetManifest` in -`RETIRED_DEFS_BY_MAJOR` for major 17. - -A per-key description of a schema that no longer exists is the ADR-0033 failure: -an AI author reads a published docblock as present-tense fact and builds on it. -The enumeration is dropped rather than re-dated, and the blocks now say what is -true today — the bare names are owned by NOBODY, `RuntimeWidgetManifest` is -objectui's only widget-registration contract, and the `Runtime` prefix is kept -BY CHOICE (objectstack#4988's precedent: a freed word is not a reason to spend a -second breaking rename taking it back; the unlock is recorded, not taken, in -objectui#4164). The `inline` collision that made the `WidgetSource` rename -urgent is kept, in the past tense, because it is the reason the prefix exists. -Both blocks now point at the live assertion instead of restating it: the "the -spec no longer owns" rows in -`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`, which are what -goes red if the spec ever re-publishes either name. - -Comments only. No type, signature, member or test changed, and the parity test -that owns this fact was already correct — it moved both rows to its "spec no -longer owns" table on the 17.0.0-rc.6 bump. - -Declared a `patch` for `@object-ui/types` alone because the emit was measured, -not assumed: both blocks sit on EXPORTED declarations, so they publish. Rebuilt -from a cleared `dist` and `tsconfig.tsbuildinfo` on both sides and compared by -SHA-256 — `dist/widget.d.ts` `bb4f2fd702cac02a…` -> `db1d5fbd53d305f5…` -(a consumer reads this text on hover and in the API docs), while -`dist/widget.js` is byte-identical across the rebuild -(`a3de34c54213a269…` both sides), so nothing runtime moved. diff --git a/.changeset/5221-onsuccess-navigation.md b/.changeset/5221-onsuccess-navigation.md deleted file mode 100644 index 709dc4aa88..0000000000 --- a/.changeset/5221-onsuccess-navigation.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/app-shell': minor ---- - -Console half of `ActionSchema.onSuccess` post-success navigation. - -`@objectstack/spec` declares `onSuccess` as a closed strict object -`{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to `type: 'api'` and -`type: 'script'` — the two action types whose success event carries a server response. -Nothing in this renderer read it, so an action declaring the hop navigated nowhere: the -block fell into `ActionRunner`'s older `ActionDef.onSuccess` chained-callback channel, -was dispatched as an action, and failed inside `executeNavigation` with "No URL provided -for navigation action" — a red toast and no jump. The motivating report is a clone action -that leaves the user sitting on the record they cloned from. - -`ActionRunner.handlePostExecution` now performs the declared hop through -`navigationHandler` — the same SPA seam every other navigator in that file uses, which -the console wires to react-router's `navigate`, so `openIn: 'self'` is a real in-place -route hop rather than a full-page load. `interpolateTarget` gains a `${result.*}` scope -alongside `${param.*}` and `${ctx.*}`, resolved against the handler's own return value -(the level `readActionPayload` reads, one below the action envelope) and supplied only by -this call site, so a target interpolated before its request still has no `result` to -name. `openIn` is read as the one member that changes the branch and no default is -written here — the spec materialises `.default('self')`, so parse output always carries a -resolved member — and the two `openIn` spellings stay apart: this reads -`onSuccess.openIn` (`'self' | 'newTab'`), never the top-level `type: 'url'` switch -(`'self' | 'new-tab'`), each of which spec refuses in the other's position. - -The console's server-action wrapper gains the matching handler-return half: a handler may -now return `openIn: 'self'` next to its `redirectUrl` to ask for the same-tab jump, while -a `redirectUrl` **without** `openIn` keeps its shipped new-tab behaviour unchanged. When -an action declares an `onSuccess` block, the wrapper defers to the runner and only tidies -its pre-opened tab, so one navigation happens rather than two. - -The pre-existing `ActionDef.onSuccess` chained-callback channel is unchanged. It is told -apart by the spec's own declaration — a non-array object whose `navigate` is a string — -and keeps running for every other shape. diff --git a/.changeset/5232-view-config-org-wide.md b/.changeset/5232-view-config-org-wide.md deleted file mode 100644 index fe7e5b2452..0000000000 --- a/.changeset/5232-view-config-org-wide.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/app-shell': minor -'@object-ui/plugin-list': patch -'@object-ui/i18n': patch ---- - -**View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's -ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a -list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user -dragging a column or cycling density was re-styling that view for the entire organization. Nothing -in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, -v18) and is deliberately not built here — which is precisely why the write has to be gated rather -than narrowed: there is no second, private store for it to fall back to. - -`ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability -set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` -(`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the -`VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — -before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. -It is on the write rather than on the toolbar button on purpose: withholding the affordance would -leave the method still accepting the call from anything else holding the adapter, whereas a gate on -the write is inherited by every caller, present and future. - -`manage_metadata` is not a newly minted name. It is the capability this repo already treats as -metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself -refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very -same ADR-0005 metadata door, so this applies the authority the server is already applying instead of -inventing a parallel one. - -**Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating -ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a -client-side refusal on missing data cannot protect anything and can only break a permitted user. A -*reported* empty grant gates strictly. Hosts push the session's capabilities in with the new -`setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. - -The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure -into `console.error`, which for a debounced toggle whose UI has already moved would have left the -operator looking at a density they did not get; a denied write now raises a toast. And the "View -settings" popover — where density and field visibility are actually changed — now states the scope -before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who -uses this view."*, translated in all ten packs. diff --git a/.changeset/5234-onnavigate-documented-exception.md b/.changeset/5234-onnavigate-documented-exception.md deleted file mode 100644 index a0aabb6e02..0000000000 --- a/.changeset/5234-onnavigate-documented-exception.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Comment- and docs-only: `ObjectGridSchema.onNavigate` is now stated as the explicit, -documented exception the maintainer ruled it on 2026-08-19 (objectui#5234, option C). -Nothing is added to or removed from any accept set, and no published behaviour changes. - -`@object-ui/plugin-grid`'s `ObjectGrid` reads `schema.onNavigate` at the -`useNavigationOverlay` call, while `GRID_QUERY_INPUTS` does not publish the key — so the -manifest, the designer panel and the generated `sdui-intrinsics.d.ts` deny a key the -renderer honours. That gap is now deliberate and said out loud rather than left to be -re-discovered by the next census: - -- An exemption comment at the read site, in the shape objectui#5091 / PR #5241 - established for `columnState`, `hideRowHeightToggle`, `maxInlineRowActions` and - `rowActionDefs` — with the one difference those four do not share: this key IS - declared in `@object-ui/types`, so the read is plain rather than a cast. -- A programmatic-only note on the declaration in `@object-ui/types` - (`ObjectGridSchema.onNavigate`): it is a function value and a schema is a serialisable - document, so `(recordId, action) => void` cannot survive a metadata round-trip whatever - declares it, and programmatic callers should prefer `ObjectGridComponentProps` where its - nine sibling callbacks live. -- `packages/plugin-grid/README.md` carried the un-narrowed universal claim that the grid - "never reads a callback off the schema", which `onNavigate` falsifies. Narrowed to match - `content/docs/plugins/plugin-grid.mdx`, which had already been narrowed to "any of these - nine"; both pages now also name the one callback that is read. -- `gridNonAuthorKeys.test.tsx` extended, not rewritten: the four objectui#5091 keys keep - every assertion they had, and ten cases are added for this key — the ledger premises - plus two source-reading cases that pin the exemption prose itself, which is the only - part of a zero-behaviour-change ruling an ordinary assertion cannot see. - -The key is deliberately NOT removed (option A: a breaking public type change plus a -deprecation cycle for zero measured harm) and deliberately NOT added to -`GRID_QUERY_INPUTS` (option B: publishing to the designer a key no author can express). diff --git a/.changeset/5293-view-sort-order-spelling.md b/.changeset/5293-view-sort-order-spelling.md deleted file mode 100644 index 47728f24b6..0000000000 --- a/.changeset/5293-view-sort-order-spelling.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -'@object-ui/plugin-view': minor ---- - -**Breaking (shipped as `minor` per AGENTS.md §版本号策略).** `ObjectViewProps.views[].sort` -now spells its direction key **`order`**. The retired spelling is **`direction`** — named -here so that a host still writing it can find this entry by searching the old key -(objectui#5293). - -```diff - -``` - -**Nothing that worked stops working on this surface, because on the `views` prop -`direction` never worked.** All three consumers of the resolved `activeView.sort` read -`order`: the non-grid fetch lowers it through the shared sink `convertSortToQueryParams`, -whose `entry.order === 'desc'` is false for a missing key; the grid path forwards it to -`ObjectGridSchema.sort`, where `ObjectGrid` builds the wire string `` `${s.field} ${s.order}` `` -— literally `"created_at undefined"` — and `parseSchemaSort` reads a missing `order` as -ascending, so the column header even drew an ascending arrow; `mergedSort` hands the same -value to the delegated list view. - -So a host writing the exact shape the prop declared got an **ascending** list with no -failure signal anywhere: the declaration said the value was well-formed, and the direction -was dropped at three independent readers rather than rejected at one. This rename does not -take away a feature — it converts a silent wrong answer into a loud type error at the one -place that can still be fixed cheaply. - -**Scope — one published export still accepts `direction`, and this release does not retire -it.** `toSortItems` (`packages/plugin-view/src/config/view-config-utils.ts`, re-exported -from the package root and listed in the README) folds `s.order || s.direction || 'asc'`. -It serves a different surface — the studio inspector-draft that feeds `SortBuilder` — and -it is not reachable from the `views` prop, so it neither affects nor is affected by this -rename. If you migrate by searching for the old key, that is the other hit you will find: -it is dormant (nothing in this repo calls it outside a test), and removing it would be a -separate break on a separate public export, tracked as objectui#6011. It is not a partial -retirement of this one. - -`order` is the spelling every other sort surface already uses (`SortConfig`, -`NamedListView.sort`, `ObjectGridSchema.sort` / `.defaultSort`, and the shared -`QuerySortEntry` sink), so the prop now has one spelling repo-wide and declared equals -enforced. - -⛔ Deliberately **not** a tolerant dual-read (`direction ?? order`): that is the tolerance -layer objectui#4869 ruled against, and admitting the old key as an alias would rebuild the -drift this change removes. `SortUI` is untouched — it legitimately owns `direction` on its -own `SortUISchema` and converts at its boundaries. diff --git a/.changeset/5325-ineffective-dynamic-import-ledger.md b/.changeset/5325-ineffective-dynamic-import-ledger.md deleted file mode 100644 index c1652b19e7..0000000000 --- a/.changeset/5325-ineffective-dynamic-import-ledger.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Build tooling only, no published package source changed. - -The console build emitted 43 `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every run -— every `import()` in `@object-ui/fields`' widget-loader map is defeated by a -static edge to the same module. They are true, and they scroll past every build -until nobody reads the log at all. - -They are now pinned to a ledger (`scripts/vite-ineffective-dynamic-imports.ts`) -rather than filtered away: the pinned 43 are replaced by one summary line, an -ineffective dynamic import the ledger does NOT know about keeps rolldown's -original warning and fails the build, and a pinned entry that stops firing fails -it too — so a build that dies before chunk assignment reports 43 named absences -instead of a clean-looking zero. - -The module graph is deliberately unchanged; the ledger's header carries the -measurement of why, and the eager-closure budget already governs the bytes. diff --git a/.changeset/5397-release-pr-post-version-validation.md b/.changeset/5397-release-pr-post-version-validation.md deleted file mode 100644 index 06dfedf041..0000000000 --- a/.changeset/5397-release-pr-post-version-validation.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -CI-only: the changeset release workflow's refresh lane now renders the -post-version tree, validates the surfaces `changeset version` can move -(`QUICK_REFERENCE.md`, the generated `CHANGELOG.md` files, the manifest -versions as the tooling suite reads them) and restores the tree before -`changesets/action` opens or updates the version PR. No package changes. diff --git a/.changeset/5422-coverage-lane-concurrency.md b/.changeset/5422-coverage-lane-concurrency.md deleted file mode 100644 index 9a77ba1754..0000000000 --- a/.changeset/5422-coverage-lane-concurrency.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -CI-only change: `ci.yml`'s workflow `concurrency` group now carries `github.sha` on -the `push` trigger, so a merge to `main` no longer cancels the previous merge's -still-running CI. The `pull_request` group (the PR number, `cancel-in-progress: true`) -and the `merge_group` fallback to `github.ref` are both unchanged. - -The push lane is the only lane that runs the coverage gate, and the merged 4-shard -report is what enforces `coverage.thresholds` for a commit. Measured over the 64 -completed push-lane runs on `main` between 2026-08-23T06:34Z and 2026-08-24T13:56Z, -39 of them — 61% — lost that gate to cancellation rather than to a red suite. - -No published package changes. diff --git a/.changeset/5453-retire-grid-column-wrap-forward.md b/.changeset/5453-retire-grid-column-wrap-forward.md deleted file mode 100644 index c3c6010431..0000000000 --- a/.changeset/5453-retire-grid-column-wrap-forward.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -Retire the dead per-column `wrap` forward from `ObjectGrid`'s data-table emit (objectui#5453, -ADR-0049 enforce-or-remove). - -`generateColumns()` copied a per-column `wrap` onto every column object handed to -`data-table`, and `packages/components`' `data-table.tsx` never read it. Measured on the -current ref rather than inherited from the card, with comments stripped so prose mentions -cannot be counted as reads: a column-level `wrap` scores **0**, against `accessorKey` 34, -`align` 5, `header` 4, `className` 4, `width` 8 and `fitContent` 2 in the same query shape. -Those sibling counts are the positive control — the search style does find the keys that are -genuinely consumed, so the zero is a measurement and not a mis-aimed grep. The raw string -`wrap` does occur in that file; every occurrence is `flex-wrap`, `whitespace-nowrap`, or a -variable named `wrapper`. - -The implement leg was ruled out by the same measurement rather than by preference. `wrap: true` -would have to drop a `truncate`, which presupposes the renderer has somewhere to put a second -line — and it does not. `data-table`'s cell wrapper is a two-way switch, -`isFit ? 'w-full whitespace-nowrap' : 'truncate w-full'`, with a native `title` tooltip as the -only concession to overflow; the file does not read `density` or `rowHeight` at all. There is -no clamp, no expand, no line-clamp and no multi-line affordance for a per-column `wrap` to turn -on, so the enforce-or-remove default applies. - -Removing the forward rather than declaring the key follows from `wrap` having **no second road -to a consumer**. That check is what separates this verdict from `pinned`, which `data-table` -also never reads and which is nonetheless kept: `ObjectGrid`'s own reorder pass consumes -`pinned` before the array reaches the slot and re-expresses it as the sticky `className` the -renderer does read. `wrap` had no such pass anywhere. - -**Retired, not merely deleted.** `wrap` is no longer carved out of `RetiredListColumnKey`'s -`Exclude`, so the derived tombstone band that objectui#6461 installed on this producer now types -it `never`. Re-adding `...(col.wrap !== undefined && { wrap: col.wrap })` is a compile error -naming `wrap` — otherwise "retired" is just a deleted line the next edit can put back for free. -Deriving the band from the authored `ListColumn` is also why this needed no hand-maintained -list: taking the carve-out out was the whole edit. - -**Breaking for TypeScript consumers of `@object-ui/plugin-grid`'s exported emit types only** -(`ObjectGridColumnHolds` loses its `wrap` member; `ObjectGridColumnDraft['wrap']` becomes -`never`). Marked `minor` per this repo's version-alignment rule, which reserves `major` for -following `@objectstack` across a major — the same classification the `MobileOverrides` -retirement used. **Runtime behaviour is unchanged**: an authored `wrap` did nothing before and -does nothing now. What changed is that the code no longer implies `data-table` might consume it. - -⚠️ The **authorable** spelling is untouched and still declared: `@objectstack/spec`'s -`ListColumn.wrap`, which `packages/react`'s spec-bridge still forwards into the grid schema. -Whether that spec property should keep being declared with no renderer anywhere is a spec-side -enforce-or-remove question, filed separately rather than settled from inside this renderer. diff --git a/.changeset/5458-unprefixed-query-params.md b/.changeset/5458-unprefixed-query-params.md deleted file mode 100644 index 040a74bb72..0000000000 --- a/.changeset/5458-unprefixed-query-params.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/console': patch ---- - -Fix four `find()` calls that passed a query option without its `$`, and gate the shape. - -`QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies -exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw, -no warning, and it type-checks because the type carries `[key: string]: any` for -adapter-specific params. For a dropped cap the result is an **unbounded** read rather than -a truncated one: the platform's GET list route has no default page size, so the query -returns the whole match set and stays invisible until the object is large. - -- `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one - **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so - the dropped key turned "count only, fetch nothing" into "fetch every row in the object", - on every mount and every refresh of every list view. It now sends `$top: 0` and reads - the count off `total` only; the row-counting fallbacks are gone rather than repointed, - because once zero rows are requested an empty `data` means "you asked for none", not - "the object is empty", and counting it would assert a confident `0`. With no total the - footer line is omitted instead. -- `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`, - one line from three correct `$top` calls. -- `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so - a filter's option list read every row and every column of its source object while its - own comment described it as capped at 200. The same call read `records.items`, which is - not a `QueryResult` member, so against a real adapter the fallback produced no options - at all. -- `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the - result in its page-source metadata. - -A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time: -a known query-option name missing its `$` in the second argument of a `find`/`findOne` -call. It is narrow on purpose — a closed list of spellings, anchored to the call — because -the index signature exists so adapters can take adapter-specific params, and a rule that -flagged any unprefixed key would report the shape the type was written to allow. Its -sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged. diff --git a/.changeset/5483-5439-dts-config-wiring.md b/.changeset/5483-5439-dts-config-wiring.md deleted file mode 100644 index 02dff4f5b5..0000000000 --- a/.changeset/5483-5439-dts-config-wiring.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/fields': patch -'@object-ui/plugin-ai': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-charts': patch -'@object-ui/plugin-chatbot': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-designer': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-editor': patch -'@object-ui/plugin-form': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-markdown': patch -'@object-ui/plugin-report': patch -'@object-ui/plugin-timeline': patch -'@object-ui/plugin-tree': patch -'@object-ui/plugin-view': patch ---- - -Published typings from every `vite-plugin-dts` package now carry an explicit extension on -every relative specifier, and a type error in the declaration build now fails the build -instead of being printed and ignored (objectui#5439, objectui#5483). - -**Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that -is the fix working.** These packages re-export mostly through NAMED re-exports — -`export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the -extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent -`any`. Nothing errored; consumers simply got no types. With the extension emitted, the -symbol carries its real type, and any call site that was relying on the `any` now type -checks for the first time. This is the mode that produced the 21 residual `TS7006` on -`@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, -unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced -immediately as `TS2305: has no exported member`. - -410 extensionless relative specifiers across 19 packages were emitted before this change; -the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. -`@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and -is wired so it stays that way. - -The second half changes no emitted output today: 22/22 packages built green unmodified, so -making the declaration step's exit code honest turns nothing red. It changes what a FUTURE -regression does — print and exit 0, versus fail the build. diff --git a/.changeset/5490-per-chunk-eager-budgets.md b/.changeset/5490-per-chunk-eager-budgets.md deleted file mode 100644 index f058a3687b..0000000000 --- a/.changeset/5490-per-chunk-eager-budgets.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Build tooling and CI only — `apps/console/vite.config.ts` (not published source: -`@object-ui/console`'s `files` list carries `dist`, `plugin.*` and `README.md`), -`scripts/check-eager-closure-budget.mjs` and its unit test. Nothing ships from -this change. - -The console eager-closure budget weighed one total across 52 chunks. Inside its -headroom a single chunk can absorb the whole allowance while the others shrink, -and the total never moves — the shape of objectui#5266, whose 89 KiB landed -entirely in `vendor-objectstack`. Per-chunk gzipped ceilings now sit on top of -the aggregate for the three largest eager chunks, set at the measured current -state plus ~2%, with each headroom narrower than the regression the gate exists -to catch. - -The ceilings key on the chunk names the report itself carries (`files[].name`, -new in report v2, taken from rolldown's own `chunk.name`) rather than on names -this checker expects to exist. A budgeted chunk that is absent — renamed group, -chunk gone — is therefore an error, not a skip: a ceiling with no subject weighs -nothing and would be green forever. diff --git a/.changeset/5522-runtime-telemetry-consumer.md b/.changeset/5522-runtime-telemetry-consumer.md deleted file mode 100644 index 7e626d0bab..0000000000 --- a/.changeset/5522-runtime-telemetry-consumer.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': minor ---- - -Console telemetry can now be hard-disabled on an already-built artifact - -`/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting` -(objectstack#11382), and the Console now reads it. The Sentry decision becomes a -conjunction of two independent grants — a DSN injected at **build** time AND a -positive permission from the **runtime** — so the single pre-built SPA that both -the hosted SaaS console and the on-premises / air-gapped EE images embed can be -silenced by the deployment it lands in, with no rebuild and without editing files -inside a published bundle. That was the half objectui#5522 could not close before: -every other input to the gate is a Vite build-time variable frozen into the bundle -as a literal, which is how an air-gapped EE Console came to send 14 Sentry -envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for -the customer to turn it off (objectstack-ai/cloud#1508). - -The permission fails **closed** in every direction: absent key, `telemetry` block -absent, malformed payload, failed fetch, or a runtime predating the key all read as -*do not send* — which is precisely the set of runtimes leaking today. It is a -permission and never a source: the server supplies no DSN and cannot turn telemetry -on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1` -and other truthy lookalikes do not. - -Behaviour change for deployments that already inject a DSN: reporting now also -requires the runtime to grant permission, via -`OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s -`allowClientErrorReporting`). A build that opted in but whose runtime says nothing -will go quiet — deliberately, since that is the same artifact an air-gapped -customer runs. - -`@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and -the `RuntimeTelemetry` type, so consumers read the permission through the one -fail-closed accessor instead of writing their own optional-chain against the -payload. diff --git a/.changeset/5631-ui-icon-icon-key-migration.md b/.changeset/5631-ui-icon-icon-key-migration.md deleted file mode 100644 index fa31693dee..0000000000 --- a/.changeset/5631-ui-icon-icon-key-migration.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -**BREAKING (authoring): `ui:icon` names its glyph with `icon`, not `name`** - -`{ "type": "icon", "name": "check" }` no longer renders an icon. Write -`{ "type": "icon", "icon": "check" }`. Stored metadata authored before this -release needs converting — see the migration below. - -Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` -outside an `@objectstack` major sync); the break is real and is stated here. - -**Why** - -`name` is the SDUI identity key every authored node carries, alongside `id` — -it is not `ui:icon`'s private prop. So an ordinary node like -`{ type: 'icon', id: 'save_icon', name: 'save_icon' }` asked lucide for a glyph -called `SaveIcon`, missed, and rendered **nothing at all**: silent to a human, -and clean to a DOM gate, because a renderer that renders nothing spreads no -attributes to find. `action:*` already reads `icon`, so this is the vocabulary's -existing answer, and it leaves no node type on which the identity key is -unusable. - -**What changed** - -- `IconSchema` (types + its zod mirror) declares `icon: string` **required**, - exactly as `name` was required before it — a key rename at constant - strictness. `name` reverts to the optional identity inherited from - `BaseSchema`. The mirror previously *required* `name`, which is why the - renderer could not be migrated on its own: the published contract refused the - correct shape. -- `ui:icon` resolves its glyph from `schema.icon`. There is deliberately **no** - `icon ?? name` fallback: a key meaning "identity" or "glyph" depending on - whether a lucide lookup happened to hit is the ambiguity being removed. -- The registry's `inputs` entry and `content/docs/components/basic/icon.mdx` - moved in the same change as the resolver. -- All 98 authored icon nodes in this repo are converted. - -**The break is loud in three places, never silent** - -1. `IconSchema` **refuses** a legacy node, with a message that names the rename - and points at the converter — not zod's default `expected string, received - undefined`. -2. A legacy node that reaches the renderer unvalidated draws the visible - placeholder shipped in the previous release, and its `console.warn` now - carries the exact rename (`icon: "save_icon"`) plus the converter's name. - Its accessible name says so too, and it gains a - `data-objectui-icon-legacy-name-key` marker so a gate can tell - "unmigrated node" from "glyph that does not resolve". -3. **Migration for stored metadata** — `migrateIconNodeKeys` from - `@object-ui/types`: - - ```ts - import { migrateIconNodeKeys } from '@object-ui/types'; - - const { document, converted, warnings } = migrateIconNodeKeys(storedPage); - if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n')); - if (document !== storedPage) await save(document); - ``` - - It walks the whole document and lifts `name` to `icon` on every icon node. - It is a one-shot conversion a deployer runs over stored documents — **not** a - read-path fallback; nothing calls it during rendering or parsing. It - **reports rather than guesses** for the two cases it will not touch: a node - already declaring both keys (`icon` wins, `name` stays the identity it is), - and a node naming no glyph at all. diff --git a/.changeset/5631-ui-icon-visible-placeholder.md b/.changeset/5631-ui-icon-visible-placeholder.md deleted file mode 100644 index 0e64171ad8..0000000000 --- a/.changeset/5631-ui-icon-visible-placeholder.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`ui:icon`: an unresolvable glyph now renders a visible placeholder instead of nothing - -An icon whose name does not resolve to a lucide glyph used to `return null`. -That failed silently in two independent ways at once: invisible to a human (no -gap, no error boundary — just an absent glyph), and clean-looking to a gate (a -renderer that returns `null` spreads no attributes, so a DOM scan of it reports -no findings). - -It now renders a dashed-square placeholder on the same SVG host, keeping the -authored `className`, `size` and colour so the gap sits exactly where the icon -would have been, with `role="img"`, an accessible name identifying the icon -that failed, and a `data-objectui-icon-unresolved` marker. The `console.warn` -stays and now names the cause. - -Also fixed: a node with no `name` at all reached `toPascalCase(undefined)` and -threw, which the error boundary then swallowed — a third silent failure. It -renders the placeholder too. - -Not included: `ui:icon` still reads the SDUI identity key `name` as its glyph -name. Moving it to `schema.icon` is ruled but blocked on an authored-metadata -migration — see objectui#5631. diff --git a/.changeset/5632-svg-host-dom-passthrough.md b/.changeset/5632-svg-host-dom-passthrough.md deleted file mode 100644 index ec15b9639b..0000000000 --- a/.changeset/5632-svg-host-dom-passthrough.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`ui:icon` and `ui:spinner` route their host spread through `toDomProps` -(objectui#5632, the `BARE_SPREAD_ON_SVG` slice of objectui#5574). - -Both renderers forwarded their whole prop bag to the SVG they render, so every -authored SDUI key on the node became an attribute — 14 per target, and -`icon="check"` on all 71 icon nodes in the schema catalog. `IconSchema` and -`SpinnerSchema` declare only `icon` / `size` / `color`, and both renderers -already consume all three by name, so the SDUI pass-through list withholds -nothing they need. - -Two user-visible behaviours change, both of which were invisible to the DOM-leak -gate because the judge counts `stroke` / `width` / `height` as legitimate on an -SVG host: - -- **`ui:spinner` now spins.** Its computed `class` (`animate-spin` plus the size - class) was being overwritten by the `className` carried in the spread, so a - spinner rendered through `SchemaRenderer` had neither. It is merged now. -- **A sized `ui:spinner` no longer emits invalid dimensions.** `size` is an enum - (`sm`/`md`/`lg`/`xl`) and the spread handed the string to lucide's numeric - `size` prop, putting `width="lg" height="lg"` on the element. - -Also: an `icon` node's `color` is a Tailwind class (as `IconSchema.color` -declares, and as every authored value in the catalog uses). It reached lucide's -`color` prop through the spread as well, emitting an invalid -`stroke="text-red-500"` beside the class that does the real work; only the class -path remains. An authored raw CSS colour (e.g. `color: "red"`) no longer tints -the glyph through that accident — declare the colour as a class, which is the -declared contract. diff --git a/.changeset/5676-layered-envelope-boundary-parse.md b/.changeset/5676-layered-envelope-boundary-parse.md deleted file mode 100644 index 88c2a25333..0000000000 --- a/.changeset/5676-layered-envelope-boundary-parse.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -`MetadataClient.layered()` validates the ADR-0010 protection envelope against the -producer's own schema at the boundary, instead of casting ten wire fields through -unchecked (objectui#5676, triage adjudication 2026-08-22). - -The envelope arrived by ten `as` assertions over a raw `res.json()` body — no parse, no -allowlist, no default. The consumer that reads it opens the metadata lock banner on -`layered?.lock && layered.lock !== 'none'`, true for **any** non-`none` value, so a server -sending a lock state this console had never heard of opened the amber box, drew the padlock -and the border, and rendered an empty title. No fifth state ever had to be added to this -repo for that to happen: a union types what this repo writes and constrains nothing about -what a server sends. - -The boundary now runs `GetMetaItemLayeredResponseSchema.safeParse`. On the conforming path -every value is the producer's schema output and the ten assertions are gone. `safeParse` -and never `parse`: a metadata console that rejected every dialect it had not been compiled -against would answer a newer server with a blank page, which is strictly worse than the -wrong render being fixed. Values the schema rejects are still **forwarded** — dropping them -would be that same refused rejection wearing different clothes — and are named in a new -optional `MetadataLayered._unrecognized`, absent whenever everything parsed. This extends -to the whole envelope the "pass through and label" treatment objectui#5672 chose for `lock` -alone; the banner's existing unrecognised-token title is unchanged and needed no edit. - -The labelling is per field, which is the part that makes it a degrade rather than a subtler -version of the same bug. Measured on the installed spec (17.2.0): -`GetMetaItemLayeredResponseSchema.safeParse(body)` is all-or-nothing — one unknown `lock` -returns `success: false` with `data` undefined — so the failure branch re-checks each key -against that schema's own `shape[key]`, where only the offending field fails and the other -six still arrive typed. Absence is never "unrecognised": the four resolved verdicts are -required upstream on this path, so a pre-ADR-0010 backend takes the failure branch with -nothing flagged and behaves exactly as before. - -One consequence of the same ruling, fixed alongside because it defeats it: a 200 answer -whose body was a bare JSON string or number **rejected** the promise with a -`TypeError: Cannot use 'in' operator`, from the envelope-detection guard's bare truthiness -check. A malformed body must degrade, never throw. diff --git a/.changeset/5700-shared-ref-namespaces.md b/.changeset/5700-shared-ref-namespaces.md deleted file mode 100644 index ead20e9239..0000000000 --- a/.changeset/5700-shared-ref-namespaces.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Instruction-file only. `AGENTS.md` §9 多 agent 协作纪律 gains one bullet beside the -existing `git stash` bullet, generalising it: a worktree isolates your checkout and -exactly four ref namespaces (`HEAD`, `refs/bisect`, `refs/worktree`, `refs/rewritten`) -and nothing else — not the object store, not the repo config, not any other ref. The -existing stash rule is reframed as one case of that, because a reader who learns only -the stash rule draws the opposite general conclusion. - -Two further instances that have cost work are stated: `refs/remotes/*` (a sibling -agent's fetch advances *your* `origin/main`, so a path-scoped `git checkout origin/main` -restores whatever that ref points at now — possibly newer than your branch base — and -stages it), and `FETCH_HEAD` (last fetch in the checkout wins; the symptom is an empty -diff that exits 0, which reads as "the change is not there" rather than as wrong -content — a confidently wrong review conclusion about someone else's work). - -The `FETCH_HEAD` isolation boundary is stated as measured rather than extrapolated from -`refs/stash`: on git 2.43 `git rev-parse --git-path FETCH_HEAD` resolves per-worktree in -a linked worktree, so a sibling's fetch in *its* worktree does not move yours; the -hazard is the shared primary checkout, where the same command resolves to the common -`.git/FETCH_HEAD` and where every agent's first fetch lands before it creates a worktree. - -No hook is added — the safe forms are ordinary commands and the unsafe form is -legitimate elsewhere, so a mechanical block would fire on correct usage. No published -package changes. diff --git a/.changeset/5729-column-sortability-signal.md b/.changeset/5729-column-sortability-signal.md deleted file mode 100644 index d5f990d8e0..0000000000 --- a/.changeset/5729-column-sortability-signal.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/data-objectstack': patch -'@object-ui/plugin-grid': patch ---- - -Grid headers offer a sort click only on columns the PLATFORM says it will order by -(objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23: -the platform serves an explicit per-column sortability signal and the grid reads it, -rather than re-deriving "virtual ⇒ unsortable" from field type). - -`GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its -ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve -time from the platform's own storage predicates, deliberately beside `item` rather than -inside it so the key stays un-authorable. The signal was reaching the browser and being -discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps -the envelope to `item`, so every UI reader saw a document with no signal on it. It now -survives that unwrap, carried on the schema under a symbol key — invisible to -`JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata -write endpoint can never take it into a body the server parses strictly. - -`@object-ui/core` gains the one spelling of the consumer contract: -`isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and -says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown -name, a dotted path and an unprovisioned audit column, all three of which the runtime -doors reject — so the `!== false` spelling every other optional flag in this repo uses -would get exactly that family backwards. A projection that is absent ALTOGETHER is a -different question with a different answer (`undefined`: no signal was served) and is -typed apart from an empty one, so a deployment older than the upstream change keeps the -behaviour it had rather than being told, falsely, that nothing on the object is sortable. - -Three things follow in the grid. The header click on a refused column ceases to exist, so -neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is -reachable from it. A sort PERSISTED before the signal existed is filtered out of both what -the grid renders and what it emits, so a restored personalization cannot ride back into -the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the -PUT still fires. And the relational carve-out is untouched and deliberately not delegated -to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored -foreign key and both runtime doors accept ordering by it), while the grid withholds that -header for a different reason — a column of names ordered by an invisible id. - -Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts -those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced -drift in mirror image. diff --git a/.changeset/5733-changeset-presence-population.md b/.changeset/5733-changeset-presence-population.md deleted file mode 100644 index a50347b9c0..0000000000 --- a/.changeset/5733-changeset-presence-population.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Tooling only; no published package behaviour changes. - -`scripts/check-changeset-presence.mjs` counted a changed file as a package's source -only when it sat under `/src/`. `apps/console/index.html` does not — it is the -console's HTML entry, compiled into the published `dist/`, and it carries two inline -classic scripts that run in every user's browser during parse. A pull request editing -only that file changed shipped console behaviour while the gate reported -`No source of a released package changed in this range` (objectui#5733). - -The population is now the package's published, executable source: `/src/`, plus -the bundler's HTML entry, plus whatever the package's own `package.json` `files` list -publishes verbatim — minus documentation and licences, which ship but are not -behaviour. On this tree the widening adds exactly three files -(`apps/console/index.html`, `apps/console/plugin.ts`, `packages/runner/index.html`) -and nothing else in any package directory. diff --git a/.changeset/5753-providers-drop-spec-dep.md b/.changeset/5753-providers-drop-spec-dep.md deleted file mode 100644 index eb0756fe59..0000000000 --- a/.changeset/5753-providers-drop-spec-dep.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/providers': patch ---- - -`@object-ui/providers` no longer declares `@objectstack/spec` as a dependency. Nothing -in the package imports it, so consumers stop installing it on this package's account -(objectui#5753). - -The edge was live for exactly one release cycle. It was promoted from `devDependencies` -to `dependencies` when `ThemePreference` was derived from the spec's `ThemeMode` union, -because the package's public `.d.ts` then referenced the spec. objectui#5716 re-pointed -that derivation at `@object-ui/types` (`ThemeMode` / `THEME_MODES`), which removed the -last three import sites — `src/types.ts` and the two retirement-era test files — and -left the declaration behind with no reader. - -Re-measured on `origin/main` at `ad0f5f11f` before removal, with a positive control so -the empty result is a real absence rather than a broken command: the import-shaped grep -(`from` / `require(` / `import(` / `vi.mock(` against `@objectstack/spec`, bare name and -every subpath) returns **0** hits under `packages/providers/` and **434** across -`packages/` + `apps/` — same command, same invocation. The only surviving mentions in -the package are the declaration itself, immutable `CHANGELOG.md` history, and a prose -comment in `tsconfig.test.json` that this change corrects. - -No API change and no behaviour change: `dist/types.d.ts` imports only `react` and -`@object-ui/types`, and no emitted file references a spec symbol. Consumers on an -isolated `node_modules` (pnpm) never had supported access to the spec through this -package, so nothing they could legitimately import goes away — the change is to the -install graph only, which is why it is scored `patch` rather than `minor`. diff --git a/.changeset/5754-ci-runs-hook-selftests.md b/.changeset/5754-ci-runs-hook-selftests.md deleted file mode 100644 index ed261bbe01..0000000000 --- a/.changeset/5754-ci-runs-hook-selftests.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -CI-only change: no published package's `src/` changed. - -Adds `.github/workflows/hook-selftests.yml`, a new PR/push gate over -`.claude/hooks/*.selftest.sh` — the hermetic self-test matrices for the -PreToolUse guards behind the worktree-first and never-`git stash` rules. Runs -on any PR touching `.claude/hooks/**` or the workflow itself; fails the build -the moment either matrix goes red. Does not modify the hooks or their -self-tests. Paired writes: `content/docs/guide/ci-cd-pipeline.md` (workflow -inventory) and `scripts/dependabot-merge-gate.mjs` (classifies the new check -as `OPTIONAL_CONTEXTS`, following `Changeset Bump Policy`'s path-filtered -shape). diff --git a/.changeset/5775-lane-changeset-reader-mirror.md b/.changeset/5775-lane-changeset-reader-mirror.md deleted file mode 100644 index 0a8e5a5070..0000000000 --- a/.changeset/5775-lane-changeset-reader-mirror.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -CI-only change: the `lane` job in `.github/workflows/changeset-release.yml` now scans -`.changeset/pre/*.md` as well as `.changeset/*.md`, and matches the reader's README -exclusion case-insensitively (`/^README\.md$/i`) instead of comparing the one exact -spelling. `scripts/__tests__/changeset-release-lane-mirror.test.ts` pins the step's -script against the installed `@changesets/read`, executed rather than transcribed, so a -dependency bump that changes the ignore list fails there instead of drifting. - -No published package changes, and no release behaviour changes: `pending_changesets` -gates only the release job's clear step, and both edits make it count MORE, never less. diff --git a/.changeset/5793-spec-range-floors.md b/.changeset/5793-spec-range-floors.md deleted file mode 100644 index 74fd71c447..0000000000 --- a/.changeset/5793-spec-range-floors.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`@object-ui/plugin-detail` now declares `"@objectstack/spec": "^17.1.0"` rather than -`^17.0.0`, which is the lowest published spec that carries the symbol its own build -output re-exports (objectui#5793). - -`dist/renderers/record-reference-rail.d.ts` reads -`export type { ReferenceRailEntry } from '@objectstack/spec/ui'`, and -`ReferenceRailEntry` first appears in `@objectstack/spec@17.1.0` — measured against the -published tarballs, not the installed tree: the name is absent from 17.0.0's -`dist/ui/index.d.mts` (425 exported names) and present in 17.1.0's (442). So the old -range was a claim the package could not honour. Any consumer resolution that lands -17.0.0 — a sibling pinning it exactly, an `overrides` entry, a mirror a minor behind — -satisfied `^17.0.0` and got a dangling type re-export. - -Nothing a consumer installs today changes: normal resolution already picks the newest -17.x, and `pnpm-lock.yaml` still resolves 17.2.0 on this edge after the bump. The change -is to the declared floor only, which is why it is scored `patch` rather than `minor` — -the same reasoning objectui#5753 used for the other direction on this dependency. - -The bump is one instance; the durable half is `scripts/check-spec-range-floors.mjs`, a -gate that compares every published package's `dist` imports of `@objectstack/spec/*` -against the export set of that package's own declared minimum. It runs on the publish -path (`pnpm changeset:publish`) and nightly, and it names the symbol behind every range -it asks for. No other package's floor is touched: the gate finds nothing else to justify -one across the 19 packages that declare the spec in a consumer-facing field. diff --git a/.changeset/5795-related-list-inherit-list-view-sort.md b/.changeset/5795-related-list-inherit-list-view-sort.md deleted file mode 100644 index ba4b8789e3..0000000000 --- a/.changeset/5795-related-list-inherit-list-view-sort.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-detail': minor ---- - -An auto-derived related list now orders its rows by the CHILD object's default list view -`sort`, instead of falling to the server's primary-key order (objectui#5795). A task -version's "check items" tab whose child object declares `sort: [{ field: 'seq_no' }]` -renders 10/20/30/40; before this it rendered whatever order the ids happened to give — -20/30/10/40 in the reported case — while the child object's own list page obeyed the -declaration. - -**Declared as user-visible, deliberately, even though no key was added.** The contract -question ("where does a derived related list's sort declaration live?") was ruled on -objectstack#11345 (maintainer, 2026-08-23) as **direction 1**: inherit the child's list -view sort, and add **no** new spec key — the field-level `relatedListSort` the issue also -proposed was explicitly not approved. So there is nothing new to author, and -`record:related_list.sort` was already declared, parsed and consumed; this fills it. What a -host observes is nonetheless new: a derived related-list descriptor gains a populated -`sort` where it had none, and the query it issues gains an `$orderby`. An app whose child -objects declare a default list order will see those tabs re-order on upgrade — which is the -point of the change, and is why this is not a patch. - -Nothing is inherited where nothing was declared: a child object with no default list-view -sort produces the same descriptor, the same node and the same `$orderby`-free query as -before. - -The two `sort` surfaces declare the same union and mean different things by its string arm -— a `ListView` string is the legacy space-separated `'seq_no desc'`, while the related -list's own reader takes `'field'` / `'-field'` — so the inherited value is normalized to -the array arm once, at the derivation, through `@object-ui/core`'s -`convertSortToQueryParams` (the repo's single definition of both authored dialects). An -un-normalized inherit would have ordered by a field literally named `seq_no desc`. - -Known and unchanged: `$orderby` is only assembled while the related list is in windowed -(server-paged) mode, so a declared *or* inherited sort still disappears while the built-in -client text filter is active. That hole pre-dates this change and affects the authored prop -identically; it is now pinned as a recorded fact in -`plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx` rather than -fixed here. diff --git a/.changeset/5853-tablecolumn-type-canonical-union.md b/.changeset/5853-tablecolumn-type-canonical-union.md deleted file mode 100644 index 47431dd506..0000000000 --- a/.changeset/5853-tablecolumn-type-canonical-union.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-dashboard': patch ---- - -`TableColumn.type` now has ONE canonical value set across all three ends that disagreed -(objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is -canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | -'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted -anything; the renderer branched on a third set and could only read the key through an -`as any` cast. - -## ⚠️ Accept-set narrowing — these spellings stop validating - -`TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now -`z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse -time with `type` named in the error path. Spellings that validated before and are **refused -now**, grouped by why they were being written: - -- **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, - `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is - the card's headline case: it validated, matched no renderer branch, and the column fell - through to plain text rendering with nothing reported. That silent fall-through is the - lenient-validation face that lets AI-authored metadata errors through, and it is now a - loud parse failure. -- **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, - `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of - `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, - not on the column: a column gets its dedicated widget from the field definition behind its - `accessorKey`, never from `type`. - -**Authored metadata in this repo needs no migration.** Measured before tightening, across -`examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files -plus the docs and playground sources): **zero** authored `TableColumn.type` values outside -the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column -position anywhere in the repository. If you author `type` on a table column, check it -against the eight; if the value describes the FIELD rather than the column, remove it. - -## The renderer's undeclared vocabulary disappears instead of being declared - -`int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` -and `datetime-local` had its own editor branch, none of them declared. They arrived because -column-inference producers forwarded an object schema's field type **verbatim** into -`TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred -value onto the declared vocabulary at their emit seam via the new -`normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, -`datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the -column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) -and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote -the raw field type into the same slot. - -Dropping the annotation is behaviour-preserving at the only consumer that reads the key. -`data-table`'s inline editor branches on `date`, `datetime` and the numeric set and -otherwise falls through to a text input — which is exactly the `undefined` path. The -dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, -which resolves the field through `column.accessorKey` and never reads `type`. - -## New public API - -`@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single -declaration the zod mirror builds its enum from, so the two cannot drift), the -`TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast -in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared -spelling is a tsc error rather than a silent widening. - -A value-level parity pin covers all three ends -(`packages/types/src/__tests__/table-column-type-canonical.test.ts` and -`packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). -objectui#5684's guard is key-set only and cannot see value drift — `type` was present on -both sides the whole time — which is how this instance survived while its siblings were -caught. A future inference value turning that pin red is by design; the note at the pin says -so, and names the two correct repairs. diff --git a/.changeset/5856-plugin-grid-catalog-entries.md b/.changeset/5856-plugin-grid-catalog-entries.md deleted file mode 100644 index e44709e0e4..0000000000 --- a/.changeset/5856-plugin-grid-catalog-entries.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Docs and catalog fixtures only, no shipped code touched: the two `plugin-grid` catalog -entries are now real `object-grid` nodes instead of hand-built static card layouts. -`content/docs/plugins/plugin-grid.mdx` mounted `product-inventory-grid` and -`team-members-grid` under `PluginLoader plugins={['grid']}` while both authored only -`badge button card flex stack text` — pictures of a grid, not a grid. They are replaced -by `object-grid-columns` (a `ListColumn` set with `sort`, `searchableFields` and -`pagination`) and `object-grid-selection-summaries` (multi-row `selection`, named -`rowActions` / `bulkActions`, per-column footer `summary` roll-ups), both querying the -docs gallery's demo data source the same way the `plugin-view` entries have since -objectui#5113. - -The two mock-ups are legitimate static layouts filed under the wrong plugin, so they are -re-seated into `components-layout-card` as `inventory-table-card` and `team-roster-card` -rather than deleted — deleting a catalog entry moves three corpus-wide counters -(`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx`, and the `className`-carrying layout -node and `stack` node floors in `layout-props-conversion.test.tsx`), and a floor that -moves because a fixture was deleted is indistinguishable later from one that moved -because coverage regressed. All three are unchanged, with no floor edited. - -objectui#5113's pin in `catalog-gallery-render.test.tsx` is extended to cover -`plugin-grid` through an explicit two-entry category/type map, keeping both of its halves -— every entry authors the type its own package registers, and the rendered tile shows a -record that exists only in the gallery's data source. diff --git a/.changeset/5859-retire-orphaned-object-exports.md b/.changeset/5859-retire-orphaned-object-exports.md deleted file mode 100644 index 7e460424ea..0000000000 --- a/.changeset/5859-retire-orphaned-object-exports.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ObjectTrigger` and `ObjectRelationship` are removed from `@object-ui/types` — two -hand-written interfaces orphaned by the `ObjectSchemaMetadata` derivation -(objectui#5859, triage adjudication 2026-08-24; the derivation itself was -objectui#5362). - -**Breaking for anyone importing either name.** The two symbols are, verbatim: - -- `ObjectTrigger` — the `{ name, when, on, condition?, action, config? }` trigger - configuration -- `ObjectRelationship` — the `{ name, object, type, foreign_key?, cascade_delete? }` - relationship configuration - -Both existed solely to type members of the retired hand-written object-document mirror: -`triggers?: ObjectTrigger[]` and `relationships?: ObjectRelationship[]`. objectui#5362 -replaced that mirror by deriving `ObjectSchemaMetadata` from `@objectstack/spec/data`'s -`ServiceObject`, and the spec's object document declares neither member — so the two -interfaces have had nothing to type since. objectui#5362 deliberately left them standing -because cutting published exports is a separate decision from the ruled derivation; this -is that decision. - -Measured before removing, on `main`: zero references in this repo outside the declaration -and the `src/index.ts` re-export (`packages/`, `apps/`, `examples/`, `*.ts`/`*.tsx`, -`node_modules` and `dist` excluded), and zero in the sibling `objectstack` checkout, which -does import `@object-ui/types` in eleven files. Absence of a spec correspondence was -verified at type level against the installed `@objectstack/spec` 17.2.0 rather than -inherited from the card: neither `triggers` nor `relationships` is a key of -`ServiceObject`. `DataModelDesigner`'s `relationships` state is its own local model and -never referenced these types. - -**That measurement cannot see npm.** In-repo zero is not consumer zero — an external -application importing either name from `@object-ui/types` will fail to compile after this -release, and nothing in this repository can detect that. Both names are spelled out above -so a host can search its own sources for them. If you were importing either, the shapes -were client-side vocabulary with no runtime behaviour and no spec backing: copy the -interface into your own code, or model the concept against `@objectstack/spec`, which owns -the object document. - -Type-only change; nothing is emitted and no runtime behaviour moves. Ships `minor`, not -`major`, per the version-alignment convention in AGENTS.md — objectui's major tracks -`@objectstack`'s, and breaking changes of objectui's own carry `minor` with the semantics -stated in the body. diff --git a/.changeset/5867-layout-plaintext-fences-batch5.md b/.changeset/5867-layout-plaintext-fences-batch5.md deleted file mode 100644 index 089856b3da..0000000000 --- a/.changeset/5867-layout-plaintext-fences-batch5.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Documentation only: re-fenced the 12 TypeScript blocks in `content/docs/layout/**` that were -fenced `plaintext`, so `check-doc-snippet-types` compiles them, and fixed the four blocks it -then reddened (a missing `AppShellBranding` import, three examples using components and values -they never imported, and two `AppShell` examples that passed no children). Lowered -`KNOWN_UNHIGHLIGHTED_TS_FENCES` by the three layout files in the same change. No published -behaviour changes. diff --git a/.changeset/5867-plaintext-fenced-ts-batch1.md b/.changeset/5867-plaintext-fenced-ts-batch1.md deleted file mode 100644 index 80ee21ed4b..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch1.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Docs only, publishes nothing: twenty-four TypeScript snippets across thirteen -`content/docs/fields/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 1 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts`; genuinely -prose blocks on the same pages (a bare object literal, a comment-only -illustration) are left alone. The gate's blocks-to-compile count rises from 157 -to 181 — exactly the batch — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the covered/ungated sets unmoved. The blocks also stop -rendering as unstyled plaintext and pick up TypeScript highlighting, which is -the reader-visible half. diff --git a/.changeset/5867-plaintext-fenced-ts-batch2.md b/.changeset/5867-plaintext-fenced-ts-batch2.md deleted file mode 100644 index 6c65b12f7c..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch2.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -Docs only, publishes nothing: twenty-one TypeScript snippets across three -`content/docs/core/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 2 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts` / `tsx`; -genuinely prose blocks on the same pages are left alone. Compiling them found -three real documentation defects, now fixed: `app-schema` documented an -`AppSchema` type `@object-ui/types` has never exported (the shipped name is -`AppComponentSchema`), and two `enhanced-actions` callbacks authored an -`ActionCallback.dialog` object with no `type` key, which `SchemaNode` requires. -Blocks that referenced ambient names are made self-contained with the import a -reader copying them needs. The gate's blocks-to-compile count rises from 181 to -202 — exactly the batch — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the covered/ungated sets unmoved. The blocks also stop -rendering as unstyled plaintext and pick up TypeScript highlighting, which is -the reader-visible half. diff --git a/.changeset/5867-plaintext-fenced-ts-batch3.md b/.changeset/5867-plaintext-fenced-ts-batch3.md deleted file mode 100644 index 523ebb6a4d..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch3.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Docs only, publishes nothing: seventeen TypeScript snippets across ten -`content/docs/fields/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 3 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts`; genuinely -prose blocks on the same pages are left alone. Compiling them found two real -documentation defects, now fixed: `image` annotated `ImageFieldSchema.value` -with `FileMetadata`, a name `@object-ui/types` does not export — it was -deliberately renamed to `UploadedFileMetadata` (objectstack#4115) because the -spec's same-named type is the storage layer's file record, a different shape — -and `lookup` referenced `DataSource` without importing it. The gate's -blocks-to-compile count rises from 206 to 223 — exactly the batch — with -diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and the -covered/ungated sets unmoved. The blocks also stop rendering as unstyled -plaintext and pick up TypeScript highlighting, which is reader-visible. Three -further `fields` pages are excluded by measurement with blockers filed: -`auto-number` (an ambient backend `db` handle), `object` (imports `ajv`, not a -workspace dependency) and `location` (one fence welds a JSX element and a bare -metadata object literal, so it parses as neither `ts` nor `tsx`). diff --git a/.changeset/5867-plaintext-fenced-ts-batch4.md b/.changeset/5867-plaintext-fenced-ts-batch4.md deleted file mode 100644 index 88d2a22e54..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch4.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Docs only, publishes nothing: nineteen TypeScript snippets across -`content/docs/blocks/block-schema.mdx` and six `content/docs/plugins/*.mdx` -reference pages were fenced ```plaintext, so `check-doc-snippet-types` — which -reads only `ts` / `tsx` fences — never saw them (objectui#5867, batch 4 of N). -Triage's classifier is the one applied: a plaintext block whose first line -starts with `import` / `export` / `interface` / `type X =` / `const x: T` is -code, and those fences are now `ts`; genuinely prose blocks on the same pages -are left alone. Compiling them found real documentation defects on -`block-schema`, now fixed: the *Complete Example*, *Block Slots* and -*Marketplace Example* blocks annotated `BlockSchema` / `SchemaNode` / -`BlockLibrarySchema` without importing them, and three blocks referenced values -defined in other blocks on the page. Resolving `BlockLibrarySchema` then -un-masked a defect the unresolved name had been suppressing: both marketplace -listings carried `schema: { /* block schema */ }`, an empty object that -`BlockLibraryItem.schema` rejects because `BlockSchema` requires `type`. The -gate's blocks-to-compile count rises from 206 to 225 — exactly the batch — with -diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and the -covered/ungated sets unmoved. The blocks also stop rendering as unstyled -plaintext and pick up TypeScript highlighting, which is the reader-visible half. diff --git a/.changeset/5869-runtime-features-scim.md b/.changeset/5869-runtime-features-scim.md deleted file mode 100644 index 68d8e09c83..0000000000 --- a/.changeset/5869-runtime-features-scim.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Declare `scim?: boolean` on `RuntimeFeatures` and map it through -`initRuntimeConfig` (objectui#5869), mirroring its two commercial siblings -`customDomain?` / `sso?` end to end: same doc-comment style -(server-derived, absent-on-vanilla), same `false` default, same -`body.features.scim === true` derivation. - -This documents and now honestly carries the wire a shipped cloud producer -already emits in the same `resolveFeatures` object literal as -`customDomain` / `sso`; the key already arrives at the SPA today, untyped. -Declaration plus plumbing only — this patch adds no read point, no gate, -and no SCIM UI affordance. Any actual SCIM-gated UI is future work. diff --git a/.changeset/5874-expandable-field-family-convergence.md b/.changeset/5874-expandable-field-family-convergence.md deleted file mode 100644 index bcbee7fa1d..0000000000 --- a/.changeset/5874-expandable-field-family-convergence.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -Four more private copies of the reference-bearing field family converge onto -`@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`, and the "fourth and last private copy" claim -that `paramToField` still stated is corrected (objectui#5874, objectui#5875). - -Each copy diverged from the published family in BOTH directions, so this is a behaviour -change on every face and not a refactor: - -- **`user` and `tree` are now treated as relations.** Both carry the same foreign-key - storage as `lookup` and resolve through the same expand path (objectui#2032), and each - face's own stated reason for special-casing `lookup` applied to them verbatim — so - gaining them restores the rule each face already meant. A `user` / `tree` field is now - read-only in the quick-look drawer (`RecordDetailDrawer`), where the drawer wires no - relation picker and a plain text input let a user overwrite the relation with a - free-form string; it gets the wide layout basis in the record header's highlights strip - (`HeaderHighlight`), whose inline editor is a record picker; and a field-backed action - param over one now inherits the picker config it needs (`resolveActionParams`). -- **`master_detail` is now treated as a relation by `resolveActionParams` too** — it was - the only face missing that member as well, so a field-backed `master_detail` action param - inherited no `referenceTo` at all and degraded to the unexplained "paste a record id" - text input that objectui#3405 exists to prevent. -- **The undeclarable `reference` spelling is gone from the three field-type faces.** - Measured against `@objectstack/spec`'s closed `FieldType` vocabulary with live controls - (`lookup` / `master_detail` / `user` / `tree`) and dead ones (the retired `owner`, plus a - nonsense spelling): `reference` is absent, so no spec-compliant object schema could - declare a field that reached those branches. It sat exactly where `owner` sat before - objectui#4814 retired it — dead weight that read as live capability. - -`resolveActionParams` keeps answering for `reference`, deliberately and by a different -route: it is refused by the spec's `ActionParamSchema` too, but the dialog still accepts it -from params already authored with it, and that acceptance belongs to the one alias table in -`paramToField` rather than to a hand-copied membership test. This face now asks the shared -family over the widget key that table produces — the same expression `paramToField` -evaluates one step later, so the half that populates a param's picker config and the half -that forwards it can no longer disagree. - -No face copies the set: each calls `.has()` on the object `@object-ui/core` exports, and -each carries an identity pin (a spy on that `has`) so a member-identical private copy fails -instead of quietly re-forking the table. diff --git a/.changeset/5876-one-lookup-predicate.md b/.changeset/5876-one-lookup-predicate.md deleted file mode 100644 index a774fa8767..0000000000 --- a/.changeset/5876-one-lookup-predicate.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -The dashboard package now holds ONE relation predicate instead of two that -agreed only because a sweep had just aligned them (objectui#5876). - -`computeLookupExpand` in `ObjectDataTable.tsx` carried its own `isLookup`, -byte-identical to the exported `isLookupType` in `recordFields.tsx` after -objectui#5692 pointed both at `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`. -Nothing kept them aligned: a future edit to either — a member added, the -retirement gate moved — would have re-forked the `$expand` decision from the -predicate whose docblock claims to drive it. `computeLookupExpand` now calls -`isLookupType`, which gains its first production consumer, and the module no -longer imports the shared family or the retirement gate at all. - -**No behaviour changes**, and that is measured rather than assumed: - -- The two bodies were identical, so every boolean answer — `tree` is expanded, - `reference` is not, ordinary relations are — is the same before and after. -- The retired-spelling warning is not emitted a different number of times. - `reportRetiredFieldType` dedupes per SPELLING in one module-level set inside - `@object-ui/core`, which both bodies already shared, so routing two callers - through one function cannot change the count. - -Nothing published moves: `isLookupType` is not re-exported from -`@object-ui/plugin-dashboard`'s entry, so this is internal shape only. - -Because a refactor with no observable delta cannot be pinned by a behavioural -test — a byte-identical local copy satisfies every assertion you can write -about `$expand` — the pin is identity, in -`__tests__/expandableFamily.identity-5692.test.ts`: `computeLookupExpand` is -observed CALLING `isLookupType`, and `ObjectDataTable.tsx` is read at source -level to confirm no second body survives for it to call instead. diff --git a/.changeset/5896-feeditem-single-constructor.md b/.changeset/5896-feeditem-single-constructor.md deleted file mode 100644 index 15b65a81b0..0000000000 --- a/.changeset/5896-feeditem-single-constructor.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-detail': minor -'@object-ui/app-shell': minor ---- - -One `sys_activity` row → `FeedItem` constructor, and the console record page -stops dropping author-extended activity types in silence (objectui#5896). - -**The defect.** `RecordDetailView`'s `sys_activity` merge read the shared type -table (objectui#5878) and then built the `FeedItem` itself, ending in -`if (!feedType) continue;`. That one `continue` collapsed two different -situations: a type the table maps to `undefined` **on purpose** (`commented` / -`mentioned` / `login` / `logout`), and a type the table has never heard of. The -second is an **author-extended** value — `sys_activity.type` is -author-extensible (objectstack#11507 direction 4, ruled 2026-08-24), every -column on that table is `readonly` so objectql never validates a write, and -ADR-0052 §5b.2 forwards an author's `activityMilestones[].type` into it -verbatim. So an activity that happened, was written and is queryable had no row -on the console record page: no placeholder, no empty state, no console message. -Stored, queryable, invisible — objectui#5840's failure mode reached by another -route, and one objectui#5969 (PR #6112) had already removed from the block -side, leaving the two surfaces disagreeing about the same row of the same table. - -**The fix is convergence, not a second decision.** `@object-ui/plugin-detail` -now exports the whole reading — `activityRowToFeedItem`, -`UNMAPPED_ACTIVITY_FEED_TYPE` and the `resetUnknownActivityTypeWarnings` test -seam alongside `ACTIVITY_TYPE_TO_FEED_TYPE` — and `RecordDetailView` calls the -constructor instead of paraphrasing it. Publishing the table alone had left the -mirror one level up, and it had already drifted three ways: the silent drop, a -timestamp fallback that could leave `createdAt` `undefined` where the helper -yields `''`, and a second hand-written system-actor lookup. - -**Behaviour change on the console record page** (breaking in the objectui sense, -shipped `minor` — objectui's `major` tracks `@objectstack`): - -- an unmapped `sys_activity.type` now **renders** through the generic - `UNMAPPED_ACTIVITY_FEED_TYPE` (`'system'`) presentation instead of vanishing, - and is announced once per distinct value on `console.warn` — a **missing - decision**, not lost data: the row is visible, what it lacks is its own icon - and colour. `FeedItemType` is a closed spec enum, so minting a kind for "we - don't know" would be a platform change, not this surface's. -- `createdAt` is always a string for a row with neither a usable `timestamp` - nor a `created_at`. - -**Unchanged, deliberately:** the four exclusions still produce no row and no -warning. They are decisions — comment content lives in `sys_comment`, and -login/logout are account events rather than record activity — and a warning -about a decision teaches authors to ignore the channel. diff --git a/.changeset/5902-marked-refusal-drag-write-surfaces.md b/.changeset/5902-marked-refusal-drag-write-surfaces.md deleted file mode 100644 index 831e3de14c..0000000000 --- a/.changeset/5902-marked-refusal-drag-write-surfaces.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-form': patch ---- - -Let a producer-marked refusal reach the drag-write surfaces (objectui#5902). - -The kanban card-move toast, the calendar drag-to-reschedule toast and the OCC -conflict dialog each substituted a generic string for a refusal the producer had -marked as user-facing (`userMessage`), so a user was told "Save failed" where the -application author had written a sentence addressed to them. All three now read -the marking through the shared `declaredUserMessage` reader, which covers both -places the adapter boundary parks it — the typed member on -`ConcurrentUpdateError` and the details bag on `DataApiValidationError`. - -Nothing unmarked changes: the reader answers `null` for it, so every existing -generic substitution — including the localized "not authorized" message that -keeps raw server diagnostics away from end users — still governs unmarked -refusals exactly as before. - -The two toasts substitute; the conflict dialog augments. Its description also -explains what the destructive "Overwrite" button does, which is affordance copy -that surface owns rather than a refusal message, so the marking leads and that -paragraph stays. diff --git a/.changeset/5903-objectgantt-declared-keys.md b/.changeset/5903-objectgantt-declared-keys.md deleted file mode 100644 index de0c6da6f5..0000000000 --- a/.changeset/5903-objectgantt-declared-keys.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -`ObjectGanttSchema` declares the ten gantt keys `ObjectGantt` actually reads -(objectui#5903, triage 2026-08-24). Every one is a real, working, documented -feature — `readOnly`, `mobileReadOnly`, `markers`, `navigation`, `skipWeekends`, -`holidays`, `criticalPath`, `showBaselines`, `persistLayout`, `viewName` — and -none of them was discoverable from the published type, because all ten were read -as `(schema as any).K`. The cast was the load-bearing part: it kept the read -invisible to `tsc`, to the zod mirror and to the designer's registry `inputs`. - -Both halves move together. The TS declaration (`packages/types/src/objectql.ts`) -and its zod mirror (`src/zod/objectql.zod.ts`) gain the same ten keys at the same -requiredness — all optional — so the `zod-mirror-parity` ratchet stays at zero -drift for this pair and no `KnownDrift` entry is added. `navigation` is taken -from `@objectstack/spec`'s `NavigationConfigSchema` by reference rather than -restated, matching `ObjectGridSchema.navigation`. - -`ObjectGanttProps.schema` is retyped from `ObjectGridSchema` to -`ObjectGanttSchema`. That is what makes the declaration load-bearing: the ten -keys are not grid keys, so with the old prop type, dropping the casts would have -left the reads landing on `BaseSchema`'s index signature — the same invisibility -in different syntax. The grid-style `{ gantt: { … } }` block is unaffected; -`getGanttConfig` reads it through that index signature exactly as before, and the -registered renderer passes `schema: any`, so no runtime shape is turned away. - -Accept-set change, stated plainly: a **declared** key is now type-validated, so -`readOnly: 'yes'` is refused where it used to parse green — the same narrowing -objectui#5074 landed for `viewMode`. An **undeclared** key is still accepted: -`BaseSchema` is `.passthrough()` and carries an index signature (objectui#5155's -structural ceiling), so declaring these ten did not buy rejection of a -misspelling. `packages/types/src/__tests__/gantt-declared-keys.test.ts` pins both -halves so neither can be misread. - -The eleventh reported key, `label`, needed no declaration — `BaseSchema` already -carries it — so only its cast was dropped. diff --git a/.changeset/5905-componentinput-inputtype-tombstone.md b/.changeset/5905-componentinput-inputtype-tombstone.md deleted file mode 100644 index f2ef359a70..0000000000 --- a/.changeset/5905-componentinput-inputtype-tombstone.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-markdown': minor ---- - -Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 -enforce-or-remove, maintainer ruling 2026-08-31, option B). - -`inputType` was held back when `min` / `max` / `step` / `placeholder` were retired, because -its defect was a different one. Those four were declared-and-UNREAD. `inputType` was -declared-and-DROPPED: the repository really did author it — `packages/plugin-markdown` -wrote `inputType: 'textarea'` on its `content` input, pinned by that package's own test — -while the manifest serializer dropped it. Retiring it therefore had to decide what that -registration should say instead, which is the fork the card reported and the ruling closed. - -FROM → TO: - -- `inputType?: string` → **tombstoned** (`?: never` on the interface, `retirementTombstone()` - named refusal on the Zod mirror). Put the control hint in `description`, which IS - published. -- `plugin-markdown`'s `inputType: 'textarea'` write → **deleted**, at zero capability cost. - -The write was measured as a no-op before it was deleted, and re-measured on this branch's -base rather than inherited from the card. A structural census over every `inputs:` array in -the repository (211 regions, all tracked TS/TSX/JS sources) scores `inputType` at exactly -ONE authoring site — the `plugin-markdown` registration — against `name` 953, `type` 969, -`label` 966, `description` 194, `enum` 119, `required` 86 and `binding` 4 in the same pass -over the same regions, so the instrument was not blind. The other 192 in-repo `inputType` -hits are a DIFFERENT face: `FormField.inputType` (`zod/form.zod.ts`), the text-input -renderer's prop, and `SchemaBuilder.inputType`, none of which sit on a `ComponentInput`. -The publication path is unchanged and was re-confirmed: `packages/sdui-parser/src/index.ts` -forwards exactly six keys per input — `name`, `type`, `required`, `enum`, `binding`, -`description` — so an authored `inputType` could not reach the published -`sdui.manifest.json` even in principle. - -Option A — teach `sdui-parser` to forward the key — is REFUSED on record. The only thing -that looked like demand for it was a write that had never taken effect, and a write nothing -reads is not demand for a feature. The neighbouring 2026-08-17 expression-ceiling ruling -(quoted on `ComponentInput.type`) is untouched and stays deferred, with its reopen -condition — a measured case of an author shipping a spec-rejected value objectui's silence -let through — unchanged. - -Deleting the member outright was again the option NOT taken, for the reason the four -siblings established: `ComponentInputSchema` is a non-strict `z.object`, so an undeclared -key is silently STRIPPED. The tombstone is what converts a write from OUTSIDE this -repository — the half objectui#5905 could not measure — into a named refusal carrying its -own remedy, with `code: 'invalid_type'` and the key named in the issue `path`. - -Accept-set change, stated plainly for reviewers: a document that sets `ComponentInput.inputType` -used to parse GREEN (the value was then dropped by the serializer) and now parses RED. That -is the intended effect and the reason this carries a contract-review label. - -Three pins were FLIPPED rather than deleted, so the closure stays asserted instead of -becoming a silent absence: `plugin-markdown`'s `index.test.ts` (which asserted the write) -now asserts the key's absence plus a `tsc` refusal at that package's own authoring site, -and the two fork-half controls in -`packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts` — one -type-level, one parse-level — now assert refusal where they asserted liveness. - -Stale wording corrected in the same pass, because this change falsifies it: `base.ts` and -`zod/base.zod.ts` both said the fork was "recorded for a ruling; until then this stays a -live, writable key", and `widget.ts` called it "the open fork". All three now record the -ruling. A reader who greps the source instead of the card thread was meeting an open fork -that no longer existed. diff --git a/.changeset/5905-componentinput-retire-constraint-keys.md b/.changeset/5905-componentinput-retire-constraint-keys.md deleted file mode 100644 index cca4c4d014..0000000000 --- a/.changeset/5905-componentinput-retire-constraint-keys.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `ComponentInput`'s four inert constraint keys — `min`, `max`, `step` and -`placeholder` (objectui#5905, ADR-0049 enforce-or-remove). - -All four were declared on `ComponentInput` and read by nothing, on either path. No consumer -reads them off a `ComponentInput` value, and the manifest serializer -(`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, `type`, -`required`, `enum`, `binding`, `description` — so a value authored here could not reach the -published `sdui.manifest.json` even in principle. Re-measured on this branch's merge-base -rather than inherited from the card: a structural census over every `inputs:` array in the -repository (219 regions, all tracked files) scores `min` **0**, `max` **0**, `step` **0** -and `placeholder` **0**, against `name` 926, `type` 926, `description` 161, `enum` 114 and -`required` 87 in the same pass over the same regions — the instrument was not blind. - -FROM → TO, per key: - -⚠️ The four are **TOMBSTONED, not removed** — the declaration stays, the key becomes -unwritable. An earlier draft of this list said "removed", which contradicted the paragraph -below it and described the option this change deliberately did NOT take. - -- `min: number` → **tombstoned** (`?: never`, named Zod refusal). Spell the numeric domain - out in `description`, which IS published (`'A positive integer — the contract rejects 0 - and fractional values'`). -- `max: number` → **tombstoned**. Same remedy. -- `step: number` → **tombstoned**. Same remedy. -- `placeholder: string` → **tombstoned**. Put the hint in `description`. ⚠️ - `BaseSchema.placeholder` — the node-level prop a renderer does read — is a DIFFERENT key - and is unaffected. - -The retirement kit: `?: never` on the interface (`packages/types/src/base.ts`), so authoring -one is a `tsc` error at the registration site; `retirementTombstone()` on the Zod mirror -(`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with -`code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the -message. Deleting the members outright was the option NOT taken: `ComponentInputSchema` is -a non-strict `z.object`, which strips an undeclared key silently — one silent no-op traded -for another. Pinned in -`packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts`. - -Two limits worth stating rather than papering over: - -- The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes - these keys is **not measurable from here** (the same limit objectui#5674 recorded for - `PluginComponentInput`). Converting such a write from a silent drop into a named refusal - is exactly what the tombstone buys. -- The fifth key objectui#5905 named, `inputType`, is **NOT retired here**. - `packages/plugin-markdown` authors it (`inputType: 'textarea'`), so it is - declared-and-DROPPED — a different defect that needs a ruling, not a removal. That - ruling landed on 2026-08-31 and `inputType` is tombstoned in the follow-up change; this - note records the state as of THIS change, which is what a changeset is for. - -This is not a verdict that constraint slots on `ComponentInput` were a mistake. The -neighbouring `type` field carries a maintainer ruling of 2026-08-17 recording that giving -`ComponentInput` real constraint slots was **deferred, not rejected** — `min`/`max`/`step` -read exactly like the slots that ruling declined to add. What is retired is this inert -spelling; the ruling's own reopen condition still stands. diff --git a/.changeset/5916-record-path-stage-state-accessible-name.md b/.changeset/5916-record-path-stage-state-accessible-name.md deleted file mode 100644 index 7b6bf579e1..0000000000 --- a/.changeset/5916-record-path-stage-state-accessible-name.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/i18n': patch ---- - -`record:path` now announces each stage's state, not just its label (WCAG 2.2 SC 1.4.1) - -The lifecycle path distinguished travelled, upcoming and lost-terminal stages with -colour plus a `✓`/`✗` glyph, and both glyphs are `aria-hidden` decoration. -`aria-current="step"` marked the current stage and nothing else, so a screen-reader -user heard a run of identically-announced items — and a rejected stage announced -exactly like an ordinary stage the record had not reached yet. - -Each stage now carries an accessible name composing its (already picklist-localized) -label with its state, from five new `detail.pathStage*` keys translated in all ten -locale packs. The glyphs stay decorative and the readout's `role="listitem"` / -`aria-current` semantics are unchanged. - -The name is composed into `aria-label` rather than visually-hidden text because -`listitem` takes its name from the author only: text placed inside a stage computes -to an empty accessible name, so the visually-hidden shape would have looked right in -the markup and delivered nothing to the accessibility tree. diff --git a/.changeset/5924-live-headroom-invariant.md b/.changeset/5924-live-headroom-invariant.md deleted file mode 100644 index 11faf78eb2..0000000000 --- a/.changeset/5924-live-headroom-invariant.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Build tooling and CI only — `scripts/check-eager-closure-budget.mjs`, its unit -test, and a comment in `.github/workflows/performance-budget.yml`. Nothing ships -from this change. - -The console eager-closure gate stated a binding constraint on its own -sensitivity — the headroom above the measured payload must stay SMALLER than the -89 KiB regression the gate exists to catch — and then checked it between two -constants frozen in the same module. That assertion is an arithmetic fact about -the file, true regardless of what the console weighs. The closure shrank 706,013 -gzipped bytes below the pinned baseline without the ceiling following it down, -the live headroom reached 8.6x the regression size, and the check that was -supposed to notice stayed green throughout: a demonstrated +158,006-byte eager -regression, 1.7x the incident this gate was built for, passed with a green tick. - -`evaluateHeadroomSensitivity` now derives that headroom from the report the gate -just read, for the aggregate ceiling and for each of the three per-chunk -ceilings, and treats a ceiling that has drifted more than one regression above -its own measurement as an ERROR (exit 2) rather than a size failure — it is a -verdict about the gauge, the same asymmetry a budgeted chunk absent from the -report already carried. The constant-vs-constant assertions stay as a secondary -guard. - -The aggregate ceiling is re-baselined downward as the decision this records: -`MAX_EAGER_CLOSURE_GZIP_BYTES` 4,086,000 to 3,345,000 over a `BASELINE` moving -4,005,911 (`4c1623c0c`) to 3,299,898 (`48e53814e`). Headroom goes from 8.63x the -regression size to 0.49x. Lowering a ceiling toward reality is a tightening: no -build that passed before and measures under the new line fails after it. The -floor is unchanged — a ceiling is never put below a measured figure. diff --git a/.changeset/5926-empty-action-visible-when.md b/.changeset/5926-empty-action-visible-when.md deleted file mode 100644 index 4599bf9b70..0000000000 --- a/.changeset/5926-empty-action-visible-when.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/components': minor ---- - -Route a `data-table`'s `emptyAction` node through `SchemaRenderer`, so a `visibleWhen` -authored on it is actually evaluated (objectui#5926 gap 1). - -`visibleWhen` is not a per-block concern in this platform. It is enforced **once, -generically**, in `packages/react/src/SchemaRenderer.tsx`: `shouldHide` tests `visibleWhen` -ahead of the hoisted `visible` (objectui#5454), sets `_hidden`, and the `_hidden` early -return fires **before** the registry dispatches. A block renderer cannot ignore the gate, -because it never sees the node. - -`emptyAction` was the one authored-node exception in the tree. The empty-state CTA slot -resolved the registry **directly** — `ComponentRegistry.get(node.type)` — and mounted the -result itself, so the node never passed through `SchemaRenderer` and its `visibleWhen` was -**never evaluated**. `@objectstack/spec` accepts the key (`SchemaNodeSchema` carries -`visibleWhen`, and `data-display.zod.ts` types `emptyAction` as a `SchemaNode`), so an -author wrote a gate, the platform took it, and nothing enforced it — declared-not-enforced, -the same class objectui#5401 / #5505 closed for `record:alert`, one level down. - -Measured on the branch point, mounting a `data-table` with no rows so the empty state is -actually reached: an `emptyAction` carrying -`visibleWhen: { dialect: 'cel', source: 'features.can_create == true' }` under an ambient -scope of `features.can_create = false` **rendered**, and so did the bare-string spelling of -the same predicate. Both now render nothing. The three must-show cases were pinned in the -same file and were green before and after — an `emptyAction` whose predicate resolves -**true**, one declaring **no** `visibleWhen`, and one whose predicate names an unbound root -(the central gate fails soft to visible, and this slot now gives the same answer as every -other node rather than a private one). - -The fix is a **route**, not a new check: no `visibleWhen` test was added to `data-table.tsx`. -A local check on this slot would have been a fourth evaluator, which is the drift -`page:tabs`' item-level predicate already records on this card. The slot now mounts its -authored node exactly the way the `empty` renderer's `action` slot always has. - -**Behaviour change worth declaring, beyond the gate itself.** No declaration moves and no -new key is accepted, but two host-observable answers change on this slot: - -- An `emptyAction` whose `visibleWhen` resolves false stops rendering. That is the fix. -- An `emptyAction` whose `type` is missing or names an unregistered component now gets the - platform's uniform "unknown component type" report instead of rendering as silent - nothing. Malformed metadata gets one answer across the tree rather than a private one - here — but a page that shipped a typo'd `emptyAction` type went from invisible to visibly - reported. diff --git a/.changeset/5927-zod-mirror-group-a-widenings.md b/.changeset/5927-zod-mirror-group-a-widenings.md deleted file mode 100644 index aae2128dd3..0000000000 --- a/.changeset/5927-zod-mirror-group-a-widenings.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`@object-ui/types/zod` now accepts seven spellings its own TypeScript declarations already declared** - -Seven keys across five hand-written zod mirrors refused values the published TS -types invite and the renderer implements — `declared !== enforced` on a published -validator. The mirrors are widened to their declarations. Nothing is narrowed and -nothing previously accepted is rejected, so this is additive for every author and -every host: schemas that parsed before still parse. - -The newly-accepted spellings, so a host can search for them: - -| schema (`@object-ui/types/zod`) | key | now also accepts | -|---|---|---| -| `ButtonGroupSchema` | `variant` | `secondary`, `destructive`, `ghost`, `link` | -| `ButtonGroupSchema` | `size` | `icon` | -| `ObjectChartSchema` | `chartType` | `column`, `horizontal-bar`, `donut` | -| `FormSchema` | `validationMode` | `onTouched`, `all` | -| `SelectSchema` | `defaultValue`, `value` | `boolean` | -| `DataTableSchema` | `selectable` | `'single'`, `'multiple'` (alongside `boolean`) | -| `ViewSwitcherSchema` / `ViewTypeSchema` | `defaultView`, `activeView`, `views[].type` | `chart` | - -**Each one was decided by measuring the renderer, not by matching the declaration.** -Widening a mirror to its declaration is only correct where the running code -implements the missing spelling; where a spelling is dead, the right fix is to -withdraw it from the declaration (ADR-0049 enforce-or-remove), not to teach the -validator to accept something that renders nothing. The read sites: -`buttonVariants`' `cva` map (`components/src/ui/button.tsx`) carries all six -variants and all four sizes; `AdvancedChartImpl` normalizes `column` to `bar`, -maps `horizontal-bar` to a real `BarChart` layout and gives `donut` its own inner -radius; `useForm({ mode })` hands `validationMode` straight to react-hook-form, -whose `isOnTouch` / `isOnAll` branches implement `onTouched` and `all`; -`toControlValue` / `matchOptionValue` (#3090) round-trip a boolean option value -with its type intact; `resolveSelectionMode` implements `'single'` as -replace-on-select with no select-all header, distinct from `'multiple'`; and -`chart` is a rendered view type with its own `case` in both `ListView` and -`ObjectView`. - -Consumer-visible type effect: `z.infer` of these schemas widens accordingly. -Widening an input contract cannot break a caller that was already passing a -narrower value, but code that exhaustively switches on the inferred union — e.g. -a `switch` over `chartType` with no `default` — will want the new arms. - -Refs objectui#5927 (group A of the 17 measured mirror drifts). The remaining -classes are rulings rather than edits and stay in the `KnownDrift` ledger. diff --git a/.changeset/5928-classname-style-props-rename.md b/.changeset/5928-classname-style-props-rename.md deleted file mode 100644 index 4099c4e346..0000000000 --- a/.changeset/5928-classname-style-props-rename.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`@object-ui/types/zod`: the zod const `StylePropsSchema` is renamed to `ClassNameStylePropsSchema` (objectui#5928). **The old name is gone** — there is no deprecated alias and no second spelling. Import `ClassNameStylePropsSchema`. - -**What moves on the published surface.** `StylePropsSchema` is removed from `@object-ui/types/zod`; the same object is exported under the new name with the same accept set, so nothing that parsed before parses differently and nothing refused before is accepted now. The break is the name alone: an import of `StylePropsSchema` no longer resolves. - -**Why the name had to move.** The const declares exactly two keys — `className` and `style`, the CSS passthrough attributes a node exposes. The TypeScript `StyleProps` (`base.ts`) is the Tailwind-SCALE vocabulary: `padding`, `margin`, `gap`, `backgroundColor`, `textColor`, `borderWidth`, `borderColor`, `borderRadius`. Measured on this branch's base with an AST read of both files: 2 keys against 8, sharing ZERO keys. In this package the `…Schema` suffix otherwise means "runtime mirror of the like-named declaration", so the shared name asserted a mirror relationship that does not exist — and building objectui#5684's parity registry by name pairing duly put the two together and reported drift on a pair that has no counterpart at all. - -**Where the non-pair is recorded now.** `zod-mirror-parity.test.ts` keys its existing `EXCLUSIONS` entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to `ClassNameStylePropsSchema`. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for. - -**No deprecation window, deliberately.** No consumer of the old name exists in this repository. Measured on this branch's base: `StylePropsSchema` had exactly three references — the definition, the barrel line, and the guard's own exclusion key — all three inside `packages/types` (lit control on the same query shape: `BaseSchema` matches 251 tracked files). A staged retirement would need named external-consumer evidence, and there is none. diff --git a/.changeset/5934-retire-onsuccess-callback-channel.md b/.changeset/5934-retire-onsuccess-callback-channel.md deleted file mode 100644 index d358562f7e..0000000000 --- a/.changeset/5934-retire-onsuccess-callback-channel.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/types': minor -'@object-ui/components': patch ---- - -BREAKING (`@object-ui/core`): `ActionRunner`'s legacy `ActionDef.onSuccess` -chained-callback channel is retired — `onSuccess` now has exactly the meaning the -contract declares (objectui#5934, maintainer ruling 2026-08-31). - -(The bump is `minor` by this repo's release model — objectui's major is pinned to -the `@objectstack` family major, and its own breaking changes ship as `minor` with -the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This -paragraph is that spelling-out: the break below is real and consumer-visible.) - -- **What breaks, by specifier**: `import type { ActionDef } from '@object-ui/core'` — - `ActionDef['onSuccess']` was `ActionDef | ActionDef[]` (chained callbacks the runner - dispatched through `executeChain` after a success). It is now derived from the pinned - spec: `ActionSchema.onSuccess`'s closed strict `{ navigate: string, openIn?: 'self' | - 'newTab' }` block. Code that assigned a callback `ActionDef` (or an array of them) to - `onSuccess` no longer compiles, and at runtime a callback-shaped value gets NO reading — - no handler dispatch, no navigation, the action's own result untouched. `onFailure` is NOT - changed: the spec declares no such key, so it keeps its one runner-native meaning. -- **Why this is safe to take**: the channel was unreachable from validated metadata — - `@objectstack/spec` (17.2.0 pin) strict-refuses a callback shape inside `onSuccess` at - parse (`invalid_type` on `navigate` + `unrecognized_keys`), so no published/saved - metadata could ever carry one — and a producer census with a positive control found zero - producers outside the channel's own test pins. Migration for an out-of-repo consumer that - drove the channel programmatically: put the follow-up actions in `chain` (the runner's - declared chaining key, unchanged), or author the spec's `onSuccess` navigation block. -- `@object-ui/types` (minor): `UIActionSchema` now declares `onSuccess`, derived from the - spec's `ActionSchema.onSuccess` — the renderer view spells the key the four action - surfaces forward, so the forwards type-check. -- `@object-ui/components` (patch): the four action renderers forward `onSuccess` without - the `as any` casts (no behavior change — same key, same value, now typed). diff --git a/.changeset/5935-one-icon-resolver-seam.md b/.changeset/5935-one-icon-resolver-seam.md deleted file mode 100644 index 2399eee189..0000000000 --- a/.changeset/5935-one-icon-resolver-seam.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/app-shell': minor -'@object-ui/plugin-detail': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor ---- - -Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). - -Seven modules resolved authored icon names into lucide's runtime `icons` record, each -with its own copy of the logic: **three different tokenisers** (`split('-')` on five of -them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` -rename on only **four** of the seven. The same authored name therefore rendered on one -surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened -with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, -and the other six call it. - -**The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it -was measured rather than chosen.** Its regression set is empty three independent ways: -against the authored population, against a maximally-pessimistic every-authored-name x -every-surface cross-product, and against a bound-free differential over 8,298 spellings -derived from all 1,767 live record keys — each with a discrimination control that fired -in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface -pairs in that last reading, stripping two surfaces of every snake_case and -space-separated spelling they resolve today. - -**What changes for you — all of it widening, none of it removal.** No name that resolved -before stops resolving: no key of lucide's record contains `_`, whitespace or `-` -(measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the -wider one produces the same key. Sixteen name-surface pairs start resolving where they -rendered a fallback or nothing before: - -- `layout_dashboard` and `building_2` (and every other snake_case or space-separated - spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, - `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and - the related list. -- `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried - no rename map. `Home` is not a live record key, so this could only ever be a widening. - -**What does NOT change: what each surface draws when a name does not resolve.** The seam -answers `name -> component`, returning `null`, and decides nothing else (maintainer -ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at -the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning -(objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, -`ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` -and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and -then dropped once the tree was measured to have four such behaviours rather than two: a -lookup function is the wrong place to publish a presentation decision. - -`resolveIcon` is newly exported from `@object-ui/components`, which is the only surface -this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: -its census goes from seven sites to one, and its normalisation stops being a -widest-common approximation of three disagreeing resolvers — so the under-reporting that -gate disclosed at objectui#5932 is closed rather than merely bounded. diff --git a/.changeset/5936-registration-icon-meta-dead-surface.md b/.changeset/5936-registration-icon-meta-dead-surface.md deleted file mode 100644 index 40b9ba57f7..0000000000 --- a/.changeset/5936-registration-icon-meta-dead-surface.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Test and tooling only; no published behaviour changes. - -objectui#5936 asked for the consumer of a component registration's `icon` meta -before the lucide icon gate was extended to that population. Three first-party -populations were measured (objectui, objectstack, cloud) and none has one, so -the gate is **not** extended — adjudicated 2026-09-04. This records that reading -in the gate's own header and retires the membership half of the `ui:icon` local -pin, which had been kept on the premise that the palette lives outside this repo. - -Why the EMPTY frontmatter, when PR #7590 an hour earlier declared a real `patch` -for the same card: that change edited two published packages' RENDERER source, -so consumers of the tarball could observe it. This one is confined to -`packages/components/src/__tests__/` and to `scripts/`, and it changes no -runtime code path in any package. - -⚠️ The empty declaration rests on "test and tooling only", NOT on the -"no consumer anywhere" premise. That premise is objectui#5936's ADR-0049 -retirement premise and this change deliberately does not lean on it: the -published `ComponentMeta.icon` key is untouched, and whether it should itself be -retired stays a maintainer decision. diff --git a/.changeset/5936-retired-icon-spellings.md b/.changeset/5936-retired-icon-spellings.md deleted file mode 100644 index a240fd3b85..0000000000 --- a/.changeset/5936-retired-icon-spellings.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/plugin-detail': patch ---- - -Correct two retired lucide spellings on component-registration `icon` meta -(objectui#5936, the behaviour-neutral slice). - -`ui:page` declared `icon: 'Layout'` and `record:alert` declared -`icon: 'AlertTriangle'`. lucide retires a spelling by dropping it from the runtime -`icons` record while keeping the deprecated named export, so both names still -import and still type-check while resolving to nothing through any resolver that -reads that record. They are now the live keys `panels-top-left` and -`triangle-alert`. - -**Behaviour-neutral by identity, in every resolution world.** The retired export -and the live record entry are the SAME OBJECT — measured against the installed -lucide 1.31.0, not asserted: `Layout === icons['PanelsTopLeft']` and -`AlertTriangle === icons['TriangleAlert']` are both true. So the repair cannot -substitute one glyph for another; it can only turn a name that resolves to -nothing into one that resolves to the glyph it always meant. - -- Through a record-reading resolver (`renderers/action/resolve-icon.ts`), the old - spellings resolve to `null` and the new ones to that shared object. -- Through the dynamic surface (`iconNames`, 2025 names, retired aliases included), - both spellings already resolved, and to the same glyph. -- The kebab spellings were chosen because they are the only ones live on BOTH - surfaces: `PanelsTopLeft` and `TriangleAlert` are record keys but are absent - from `iconNames`, which is kebab-case only. - -Nothing is retired and no gate is extended here. `check-lucide-icon-record-names.mjs` -deliberately does not judge a registration's `icon` meta, and it stays that way — -its verdict is unchanged by this diff (182 names judged, green, both before and -after). Whether the registration `icon` meta itself should be retired is the open -half of objectui#5936 and is a maintainer decision under ADR-0049. diff --git a/.changeset/5940-detail-collection-decline-to-fetch.md b/.changeset/5940-detail-collection-decline-to-fetch.md deleted file mode 100644 index 2703d95554..0000000000 --- a/.changeset/5940-detail-collection-decline-to-fetch.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`object-master-detail-form` declines to fetch a detail collection whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. - -`childObject` is REQUIRED on `MasterDetailDetailConfig` and is what every downstream read is keyed -on — `deriveDetail(d.childObject, …)`, the child-schema cache, and the FK scope of each child -fetch. But a detail entry reaches the renderer straight off an authored schema, so a malformed one -arrives with the key `undefined`, and the resolve effect asked the data layer for it anyway. -Measured: mounting the block with a detail entry that carries no `childObject` issued -`getObjectSchema(undefined)` — a real backend receives a query for an object literally named -`undefined`, and whatever it returns becomes the console's problem. - -The resolve effect now skips such an entry and warns, leaving it in place so the grid card shows -its config hint and the row-state array stays index-matched. This is the choice `RelatedList` -already makes for the same class of missing key (*"has no referenceField/parentId — refusing to -fetch all rows"*), and the sibling child-schema-cache effect in this same component already spelled -it `.filter(Boolean)`; the three now agree. A detail collection that names its child object fetches -exactly as before. diff --git a/.changeset/5942-retire-mobile-component-config.md b/.changeset/5942-retire-mobile-component-config.md deleted file mode 100644 index e9b1486b86..0000000000 --- a/.changeset/5942-retire-mobile-component-config.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/mobile': minor ---- - -**Removes a published export.** Retire the `MobileComponentConfig` type -(objectui#5942, ADR-0049 enforce-or-remove). The name is deleted from -`@object-ui/types` and from `@object-ui/mobile`, which re-exported it — after -this release `import type { MobileComponentConfig }` from either package is a -compile error, not a deprecation warning. - -`MobileComponentConfig` published a four-key "mobile component schema -extension" — `responsive`, `gestures`, `pullToRefresh` and `infiniteScroll` — -and nothing read it. Re-measured on current `main` before anything was deleted: -the type had exactly four code mentions repo-wide — its own declaration, one -doc-comment cross-reference, and the two barrel re-exports. It had **no mount -point at all**: no type mounted it as a property, nothing extended it, and no -renderer, hook or adapter annotated, cast to or imported it. A sweep of the -example apps and the `objectstack` sibling checkout found zero authors. Every -read-shape probe returned zero against a control lit in the same run. - -That makes it stricter than the usual case: not merely a surface whose values -were unimplemented, but a container with no path by which any authored value -could reach a renderer. objectui#4919 removed its last member -(`mobileOverrides`), which is what left the container itself inert. - -Removed outright rather than kept as a `?: never` tombstone, on this package's -own discriminator: a tombstone steers authors to a named live replacement key -(`crud.ts` `confirm` to `confirmText`; `data-display.ts` `hoverable`/`striped` -to `data-table`), or keeps loud a key the docs taught as working. Neither -applies — the whole interface goes, so there is no surviving object to hang a -`never` key on, and no documentation ever described it -(`skills/objectui/guides/mobile.md` teaches the hooks, never this type). Same -zero-pull, no-successor shape as `MobileOverrides` (objectui#4919) and -`AccordionItem.icon` / `ToggleGroupItem.icon`. - -## Upgrading - -**No behaviour changes and there is nothing to migrate at runtime.** An object -authored against this type did nothing before and does nothing now; what -changes is that the contract no longer claims otherwise, so the mistake -surfaces at authoring time instead of silently type-checking. - -- **You imported the type only** (the only thing that was possible — nothing - accepted it as a value): delete the import. If you kept a local config object - annotated with it, drop the annotation; the object was never passed anywhere - that read it. -- **You actually wanted the behaviour:** it exists, and it is not being - retired. It lives in `@object-ui/mobile` as React hooks, which is where the - working code always was — `useResponsive` / `ResponsiveContainer` for - `responsive`, `useGesture` for `gestures`, `usePullToRefresh` for - `pullToRefresh`. `infiniteScroll` has no hook; it was never implemented in - any form. See `skills/objectui/guides/mobile.md`. -- **You want a declarative mobile config surface:** that re-enters deliberately - as designed product surface on its own card, with the renderer that reads it - landing in the same change as the declaration — not by restoring this - declaration. - -**Do not follow the compiler's suggestion.** TypeScript reports the removal from -`@object-ui/types` as TS2724 and appends `Did you mean 'ComponentConfig'?`. That -is a lexical near-match, not a migration target: `ComponentConfig` is the -renderer **registration** record (`{ type: string; component: T }`, extending -`ComponentMeta`) and has nothing to do with mobile configuration. The import -from `@object-ui/mobile` gets a plain TS2305 with no suggestion at all. - -Marked `minor`, not `major`, per this repo's version-alignment rule, which -reserves `major` for following `@objectstack` across a major (AGENTS.md -版本号策略) — the same classification objectui#4919's identically breaking type -removal used. **Breaking for TypeScript consumers of the name only.** - -Follow-up, deliberately not widened into this change: `MobileResponsiveConfig` -and `GestureConfig` were consumed only by this container and are now -zero-consumer published types themselves. Filed as objectui#7519 for triage. diff --git a/.changeset/5944-preview-page-source-query-params.md b/.changeset/5944-preview-page-source-query-params.md deleted file mode 100644 index 287a5f7a82..0000000000 --- a/.changeset/5944-preview-page-source-query-params.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change: the ADR-0080 preview harnesses' page sources are now held to -`object-ui/no-unprefixed-query-params` by that rule itself, run over the source -strings ESLint structurally cannot reach inside a template literal -(objectui#5944). No published behaviour changes — the new file is a test under -`apps/console/src/__tests__/`, and `eslint-rules/` is a repo-local plugin -directory rather than a workspace package. diff --git a/.changeset/5945-asarray-contract-envelope.md b/.changeset/5945-asarray-contract-envelope.md deleted file mode 100644 index 1b97458922..0000000000 --- a/.changeset/5945-asarray-contract-envelope.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`asArray` in the two app-shell `find()` readers now reads exactly what -`QueryResult` declares — the `records` and `items` arms are removed -(objectui#5945). - -`useApproverDirectory.ts` and `views/metadata-admin/AssignedUsersSection.tsx` -both normalised a `find()` answer with - -```ts -Array.isArray(res) ? res : res?.records ?? res?.items ?? res?.data ?? []; -``` - -`QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` — -alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`. -`records` and `items` are not members of it, and both were tried *before* the -one that is. That is AGENTS.md #0.1 in miniature: a tolerant reader that lets a -non-conforming producer keep working, so the wrong shape is never rejected -anywhere and hardens into a second de-facto contract. The same -`records`/`items` confusion was live in three other places that objectui#5458 -had to fix, each reading a key no adapter returns — a helper that quietly -accepted all three spellings is why nobody found out two of them were wrong. - -**What stops being accepted.** A `find()` answer shaped `{ records: [...] }` or -`{ items: [...] }` now reads as **no rows** at these two seams instead of -silently resolving. Both call sites degrade rather than throw: the approver -directory reports the reference unresolved (falling back to the prettified -machine name, and staffing as probed-empty), and the assigned-users section -renders its empty state. - -**Nothing produces those shapes today**, which is why this is a removal rather -than a migration. Measured across the repo, per arm: - -- **`records`** — no producer at the `DataSource.find()` seam. - `ObjectStackAdapter.normalizeQueryResult` maps the server's `records`/`value` - envelope to `data` before returning, so the spelling exists only *below* the - adapter, on the wire and in the client SDK. The two remaining `records` - producers in the repo are on different seams: `ViewDataProvider` returns its - own `ResolvedData` interface, which declares `records` legitimately, and is - not a `QueryResult`. -- **`items`** — no producer at any seam. Every `items` in the repo is the - unrelated UI-schema key (dropdown menus, timeline, accordion). - -**The bare-array arm is kept**, because it is live: fakes at these seams answer -with a plain array (`AssignedUsersSection.test.tsx` is one). It is pinned in the -same tests as the deletions, so the live and the dead shapes cannot drift into -each other. diff --git a/.changeset/5954-aggregate-capability-header.md b/.changeset/5954-aggregate-capability-header.md deleted file mode 100644 index f42b20df39..0000000000 --- a/.changeset/5954-aggregate-capability-header.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Comment-only correction of the "WHY THIS EXISTS" header in -`packages/data-objectstack/src/aggregate-capability.test.ts`: it claimed -`@objectstack/client`'s `analytics.query()` never throws on a non-2xx. The -installed `@objectstack/client@17.2.0` does throw — `ObjectStackClient.fetch` -throws on `!res.ok` before `analytics.query` reaches its own `res.json()` — -and every test in the file depends on that throw reaching the adapter's -`catch`. No behaviour change; test assertions are unchanged (objectui#5954). diff --git a/.changeset/5956-5957-record-path-a11y-residue.md b/.changeset/5956-5957-record-path-a11y-residue.md deleted file mode 100644 index d57e248f10..0000000000 --- a/.changeset/5956-5957-record-path-a11y-residue.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/i18n': patch ---- - -`record:path` finishes localizing and de-colouring its accessible names — the two residues -objectui#5916 named and deliberately left behind (objectui#5956, objectui#5957). - -**The list's own label was English on a localized surface, and the other one named nothing.** -Both the desktop and the mobile `role="list"` row did -`aria-label={schema.aria?.label || 'Record path'}`, so a zh/ja/ar session heard `Record path` -for the list while every stage inside it announced in the session locale — one control -speaking two languages at once. The fallback is now `detail.pathLabel`, translated in all ten -packs; the `schema.aria.label` author override still wins ahead of it. - -The lost-terminal alt group was a different defect wearing the same clothes: its -`aria-label="Alternative terminal stages"` sat on a bare `div`, which has the `generic` role, -and browsers expose no accessible name on a generic element. That string reached nobody — -inert, not merely untranslated — so translating it would have shipped copy to ten packs that -no user can hear. It is removed rather than given a role that takes a name, on three -measurements: nothing is lost (it was never announced), it would be redundant (every stage -inside already announces `closed lost` in the session locale after objectui#5916, in the one -place `role="list"` can carry it), and it would fork the two rows (the mobile row renders one -flat list with no alt group, so a named group would make one control expose two structures by -viewport). - -**An unreached goal terminus was distinguished by hue alone.** `railClass` paints it -`bg-emerald-500/30` where a plain upcoming stage gets `bg-muted` — the renderer's own note -calls this "a faint emerald so the goal is legible" — while both announced the identical -`{{stage}}, upcoming`. Two stages ahead of the record painted differently and read the same: -the WCAG 2.2 SC 1.4.1 class objectui#5916 closed, on the one distinction it left behind, and -reachable without authors opting in because `classify()` finds `won` through the `WON_TOKENS` -heuristic as well as an explicit `terminal: 'won'`. New key `detail.pathStageWonUpcoming` -(`{{stage}}, goal stage, not reached`), translated in all ten packs. - -Scoped to the UNREACHED goal, which is a measurement of the stylesheet rather than a -preference: a reached goal terminus paints `bg-primary` when current and `bg-emerald-500` when -completed, byte-identical to any other current or completed stage. Naming it apart would hand -a screen reader a distinction the screen does not make — the mirror image of the defect — so -it is one new key, not a pair, and a test pins that decision so it cannot drift into a fourth -state unnoticed. - -Both new keys also land in `DETAIL_DEFAULT_TRANSLATIONS`, which -`defaults-maps-mirror-en-pack` compares against the `en` pack key by key, so neither can fork -between a provider-mounted console and a provider-less embed. No existing `en` value changes, -so no pack is asked to follow an edit. diff --git a/.changeset/5965-vscode-summary-codeql-claim.md b/.changeset/5965-vscode-summary-codeql-claim.md deleted file mode 100644 index 8af3fbd59a..0000000000 --- a/.changeset/5965-vscode-summary-codeql-claim.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Doc-only removal in the (private, unpublished) `object-ui` VSCode extension package: -`packages/vscode-extension/SUMMARY.md` is deleted. The file asserted a security-scan -outcome that nothing ever produced — "CodeQL扫描通过 / 无安全漏洞 / 无已知问题" -("CodeQL scan passed / no security vulnerabilities / no known issues") — while this -repository has never had a CodeQL workflow, default-setup or otherwise. `CONTRIBUTING.md` -already states the opposite as the repo's canonical fact: it "does **not** run -static-analysis security scanning of its own source code (CodeQL or equivalent)". - -The claim was not one line. It recurred as a release-checklist tick (`- [x] 安全扫描通过`) -and twice in the conclusion, alongside a matching `- [x] 单元测试通过` in a package that -carries zero test files — the document was a frozen one-off session status report -("代码完成度: 100%", "扩展已准备好发布到VSCode Marketplace"), not living documentation. -Nothing in the repository referenced it and it never shipped: the package is -`private: true`, and `.vscodeignore` excludes `*.md` except `README.md`, so it was absent -from the VSIX. Its durable reference content (commands, snippets, configuration, project -structure) is already carried by `README.md`, `DESIGN.md`, and `PUBLISHING.md`. - -Also lowers two `objectui#4938` population floors in `scripts/__tests__/check-doc-links.test.ts` -by exactly this one deletion (packages/* 12→11, combined 15→14), so they keep failing if -the link scanner stops walking the tree. - -No published source, behaviour, or shipped artifact changes. diff --git a/.changeset/5972-loading-group-per-language-uniformity.md b/.changeset/5972-loading-group-per-language-uniformity.md deleted file mode 100644 index 5942bec9cc..0000000000 --- a/.changeset/5972-loading-group-per-language-uniformity.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The merged `Loading…` group now reads one way per language (objectui#5972). - -objectui#3878 converged every pack on the typographic ellipsis, and in doing so **merged** -the ASCII `Loading...` group into the U+2026 `Loading…` group. Nobody re-measured the -wording afterwards. Re-derived on today's tree by flattening all ten packs and comparing -exact values, the group is 10 keys — `lookup.loading`, `common.loading`, -`fields.recipient.loading`, `grid.import.historyLoading`, `grid.bulk.loading`, -`detail.loading`, `report.loading`, `dashboard.loading`, `auth.device.loading`, -`approvalsInbox.loadingMore` — and `de` rendered them four ways, `ko` two and `ar` two, -while en/zh/ja/fr/es/pt/ru were already unanimous. - -Three packs move, translation copy only — no key is added or removed, no `en` value -changes, and every value keeps its U+2026: - -- **de** → `Wird geladen…` on `detail.loading`, `report.loading` (were `Laden…`) and - `approvalsInbox.loadingMore` (was `Lädt…`). The passive is both the group majority and - the pack's dominant register for in-flight states generally. -- **ko** → `로딩 중…` on `fields.recipient.loading`, `grid.bulk.loading`, - `grid.import.historyLoading` and `approvalsInbox.loadingMore` (were `불러오는 중…`). - Majority, and it matches the pack's own pattern: `불러오는 중` is what `ko` uses when the - string names the thing being loaded, the bare form is `로딩 중`. -- **ar** → `جارٍ التحميل…` on `common.loading` and `detail.loading` (were `جاري التحميل…`). - This one is an orthography normalization rather than a wording choice: `جارٍ` is the - indefinite منقوص participle with tanwīn on the rāʾ, `جاري` the yāʾ-retaining form. - -`de` `auth.device.loading` deliberately stays `Lade…`. It is the one member whose outlier -spelling is coherent with its own screen: `DeviceAuthPage` renders that namespace's three -in-flight states together and `de` writes all three in the same first-person voice -(`Genehmige…`, `Ablehne…`, `Lade…`), the other two being outside this group. Converging it -alone would manufacture a fresh same-screen inconsistency, so it is reported as a fork and -pinned as a named exemption instead. - -`packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts` gains the per-language -uniformity pin beside the glyph rule that created the group. The pin derives the group -from `en` and asserts its membership and per-language value counts **before** asserting -uniformity, so it cannot pass by matching nothing. diff --git a/.changeset/5993-button-shared-icon-resolver.md b/.changeset/5993-button-shared-icon-resolver.md deleted file mode 100644 index 3d17aa1e88..0000000000 --- a/.changeset/5993-button-shared-icon-resolver.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`ui:button` resolves its authored `icon` through the shared `resolveIcon` instead of a -byte-equivalent copy of it (objectui#5993). - -`renderers/form/button.tsx` carried its own `toPascalCase`, its own `iconNameMap` holding -the single `Home -> House` entry, and its own index into lucide's runtime `icons` record — -the same algorithm as `renderers/action/resolve-icon.ts`, but not the same function. The -`action:*` family, `complex/data-table.tsx` and both menu renderers already import the -shared one. The hazard was drift, not rendering: an alias added to `resolve-icon.ts` to -absorb a lucide retirement (the objectui#5586 / #5622 mechanism) reached every one of those -sites and silently missed `ui:button`, which would have gone on resolving the retired -spelling to nothing while the rest of the repo resolved it correctly. - -**No behaviour changes, and that is measured rather than asserted.** The two -implementations were compared over 3547 names — every one of lucide's 1767 record keys in -both spellings, plus kebab-case probes (`arrow-right`, `dollar-sign`, `user-plus`), the -`Home` alias, retired spellings and `undefined`: 3539 identical by object identity, 8 -differing only in the nullish flavour returned for a miss (the copy indexed the record and -got `undefined`; the shared resolver `?? null`s it), zero genuine forks. That one -difference cannot reach the DOM — `Icon` is consumed at exactly two sites, both -`{!isLoading && Icon && }` truthiness tests, and React renders nothing for -`null` and `undefined` alike. Icon identity, `h-4 w-4` sizing, `iconPosition`, the loading -state and the `Loader2` spinner are unchanged, and are pinned by -`renderers/form/__tests__/button-shared-icon-resolver.test.tsx`. - -Because behaviour is unchanged, the usual red-before ablation does not exist for this -change and none was manufactured. The one row in that suite that discriminates is -structural: it spies on the shared module and fails when the glyph does not come out of it, -which is red on the copy and green on the import. - -`scripts/check-lucide-icon-record-names.mjs` drops `form/button.tsx` from -`DECLARED_RECORD_READERS` in the same commit — that gate rediscovers record readers from -source on every run and fails on drift in both directions, so the removal is verified by -the gate rather than declared. It is also what now guards the dedupe: a re-inlined copy -would be discovered as an undeclared record reader and fail. The census entry for the -`button` *type* stays, its resolver re-pointed at `resolve-icon.ts`, so `ui:button`'s -authored icon names are still judged against the live record. - -`renderers/basic/icon.tsx` keeps its own copy deliberately and is untouched: `ui:icon` -draws a `SquareDashed` placeholder and warns on an unresolvable name (objectui#5631), which -the shared resolver does not do. diff --git a/.changeset/5996-eager-closure-missing-chunk.md b/.changeset/5996-eager-closure-missing-chunk.md deleted file mode 100644 index 922af1c22b..0000000000 --- a/.changeset/5996-eager-closure-missing-chunk.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Build tooling only — `apps/console/vite.config.ts`, which is not published -source (`@object-ui/console`'s `files` list carries `dist`, `plugin.*` and -`README.md`), so nothing ships from this change. - -`emit-eager-closure-report`'s `writeBundle` hook read every member of the eager -closure off disk with an unguarded `fs.readFileSync`. The closure walk seeds -itself from `chunk.imports`, and rolldown lists a chunk's EXTERNAL imports in -that array beside the file names of real chunks — so a bare specifier vite could -not resolve joins the closure under its own name and is then read as a path. The -result was a bare `ENOENT` from `node:fs`, several frames from the cause, naming -neither the plugin nor the unresolved import. - -The read is now guarded by an existence check that `this.error()`s with the -missing name, the chunk that imports it, and the diagnosis. The failure stays -loud — that direction is correct, and the two counter-probes above it exist to -keep it that way; only its message changes. diff --git a/.changeset/5998-record-path-one-classification.md b/.changeset/5998-record-path-one-classification.md deleted file mode 100644 index 0f17f158a1..0000000000 --- a/.changeset/5998-record-path-one-classification.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:path` now derives ONE stage classification and hands it to both of its rows, so a -stage can no longer paint and announce two different ways depending on viewport width -(objectui#5998). - -The renderer draws a desktop row (`hidden sm:flex`) and a mobile row (`flex sm:hidden`) from -the same `stages[]`, and each used to compute its own `terminal` from that array. -`renderStage` passes the same `terminal` to `railClass` and — since objectui#5957 — to -`stageAriaLabel`, so any disagreement surfaced in the colour and in the accessible name at -once. The rows disagreed on two axes: - -Mid-path goal. `WON_TOKENS` matches `完成`, an ordinary word rather than a Salesforce-style -`closed_won` value, so a path like `草稿 → 完成 → 已归档` classified index 1 as `won`. -Desktop declined it, because only the last forward stage can be the goal terminus; mobile -marked it `bg-emerald-500/30` and announced `goal stage, not reached`. - -The lost slice. Desktop renders `stages.slice(firstLostIdx)` as a visually separated alt -group and hardcoded `terminal: 'lost'` on every member of that positionally-defined group, -while mobile classified each stage on its own. A plain stage after a `lost` one -(`草稿 → 失败 → 已归档`) therefore painted destructive and announced `closed lost` on -desktop and plain on mobile; a `won`-classified stage in the same position drew `'lost'` -from one row and `'won'` from the other. - -Both rows now index a single `stageTerminals` array: `lost` is a property of the stage -itself, `won` is the goal terminus and so is the last forward stage or nothing, and the -positional grouping is a layout concern that no longer overrides what a stage is. Behaviour -is narrowed on both axes and never widened — no stage gains a `terminal` on either row that -it did not already carry there, and a goal terminus that really is last keeps its faint -emerald rail and its `goal stage, not reached` name on both rows. diff --git a/.changeset/6001-drop-redundant-filter-casts.md b/.changeset/6001-drop-redundant-filter-casts.md deleted file mode 100644 index 6598af17b3..0000000000 --- a/.changeset/6001-drop-redundant-filter-casts.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Test-only change; nothing published changes. `ValueDataSource.test.ts` drops the ten -`as any` casts on its AST-array `$filter` fixtures -(`packages/core/src/adapters/__tests__/ValueDataSource.test.ts`, objectui#6001). - -The casts were written when `QueryParams.$filter` was declared as the MongoDB-style -record alone, so an author writing an ObjectQL AST array had to push the type out of the -way. objectui#3909 / PR objectui#5999 replaced that declaration with the union the data -sources always accepted, and all ten literals — the nine AST shapes plus the degenerate -empty array — now assign bare. No replacement assertion, no widening, no helper, and no -runtime assertion touched: `pnpm --filter @object-ui/core type-check` is green. - -One claim on the card did **not** survive measurement, and the correction is worth -keeping. The card justified the deletion partly by predicting that the un-cast fixtures -would become compile-time evidence — that narrowing `$filter` back to the record form -would turn this file red. It does not. Dropping the `| FilterArray` arm locally, -rebuilding `@object-ui/types` so the change reached the `dist/data.d.ts` that -`packages/core/tsconfig.test.json` actually resolves, and re-running the type-check -leaves the file green with zero errors — exactly as PR objectui#5999's own doc comment -says it must, since `Record` already accepts arrays structurally. A positive -control under the identical mutation (a slot that genuinely excludes arrays) does turn -these ten lines red, so the zero is a measurement rather than a blind apparatus. - -The deletion is therefore worth making for the smaller, true reason: `as any` at a site -where the value is already legal is a no-op that reads as a live constraint, and a reader -who trusts it concludes the array form is illegal here. diff --git a/.changeset/6002-dashboard-widget-strict.md b/.changeset/6002-dashboard-widget-strict.md deleted file mode 100644 index 22737b1e9d..0000000000 --- a/.changeset/6002-dashboard-widget-strict.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Breaking for authored metadata:** `DashboardWidgetSchema` (the zod validator -in `@object-ui/types/zod`) is now `.strict()` — an undeclared widget key -REFUSES the parse with zod's `unrecognized_keys` issue naming every offending -key, instead of being silently deleted. - -Before this change the schema was a plain `z.object()`: a widget carrying -`zzcanary` / `categoryField` / `aggregate` parsed five-keys-in, three-keys-out, -verdict ACCEPT — the same "dropped without a word" failure the schema's own -docstring records from the pre-derivation hand copy, still live for every key -no contract declares (objectui#6002). Maintainer ruling 2026-08-25, Route 1 -two-step: objectui#6150 declared the 13 genuinely-consumed keys first (landed -as PR #6945), then this flip makes a stale or mistyped key loud everywhere the -contract is consulted (`objectui validate`, `safeValidateSchema`, the catalog -gate) instead of only inside one catalog test. - -**Who is affected — a widget authoring a key outside the declared surface:** -the retired pre-ADR-0021 inline analytics shape (`object` / `categoryField` / -`valueField` / `aggregate`) is the canonical case — it used to validate clean -with all four keys deleted; it now refuses with the keys named. The spec's -tombstoned keys (`actionUrl` / `actionType` / `actionIcon` / `aria` / -`responsive`) keep their specific removal messages — they are declared -`z.never()` members, so they do not degrade to a generic unknown-key error. - -**Not affected:** a `metric-card` COMPONENT node in a dashboard's widget slot. -Its props (`value` / `icon` / `trend` / `trendValue` — registry inputs, not -widget keys) stay legal: per the 2026-08-14 ruling (objectstack#8593) a -component node is owned by objectui's own passthrough `BaseSchema`, and -`DashboardComponentSchema`'s widget slot now routes component-enum types there -before the strict widget schema is consulted. The legacy -`{ id, component, layout }` envelope also still parses. The repo-wide corpus -preflight (575 JSON files, every dashboard-bearing doc fence, all designer -emit paths) measured **zero** newly-refused widgets. diff --git a/.changeset/6004-grid-column-emit-boundary.md b/.changeset/6004-grid-column-emit-boundary.md deleted file mode 100644 index c714459cc1..0000000000 --- a/.changeset/6004-grid-column-emit-boundary.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -fix(plugin-grid): type ObjectGrid's column emit against the `TableColumn[]` slot it fills - -`generateColumns()` had no return annotation and all four of its call sites cast -to `any`, so nothing checked what this producer wrote into -`DataTableSchema.columns: TableColumn[]`. - -Annotating it is not enough, and that is the substance of the change. Measured on -this program: `generateColumns(): TableColumn[]` raises **zero** diagnostics — the -emit literals reach the annotation through `.map()`, which strips the freshness -that excess-property checking depends on, so even an undeclared key written out -longhand is accepted. Underneath that sits the reason the annotation could not -bite at all: `objectSchema` is `useState`, and an `any` spread into an object -literal collapses the **entire** literal to `any`. - -So the fix has three parts: name the four inference locals so `any` stops at the -boundary, carry ADR-0049 `?: never` tombstones **derived** from -`keyof ListColumn` (never hand-listed, so a future spec key is refused by -default), and drop the `any` at every call site — including a fourth the card's -census missed and a fifth (`const generatedColumns: any[]`) inside the producer. - -Key verdicts: `headerIcon`, `pinned` and `wrap` are HELD and now declared at the -seam; `options` is RETIRED — nothing on either side of the seam read it, and -every value it carried still reaches its consumer through the field metadata the -cell closure captures and the object schema the inline editor reads. `type` stays -objectui#5853's and `name` is not emitted here at all. - -No rendering change. diff --git a/.changeset/6011-tosortitems-order-spelling.md b/.changeset/6011-tosortitems-order-spelling.md deleted file mode 100644 index 246ac6fe48..0000000000 --- a/.changeset/6011-tosortitems-order-spelling.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-view': minor ---- - -**Breaking (shipped as `minor` per AGENTS.md §版本号策略).** The published `toSortItems` -export now reads its sort direction from **`order`** only. The retired spelling is -**`direction`** — named here so that a host still writing it can find this entry by -searching the old key (objectui#6011). - -```diff - import { toSortItems } from '@object-ui/plugin-view'; - -- toSortItems([{ field: 'created_at', direction: 'desc' }]); -+ toSortItems([{ field: 'created_at', order: 'desc' }]); -``` - -**What changed, exactly.** `toSortItems` folded `s.order || s.direction || 'asc'`: two -spellings for one key, silently preferring the canonical one. It now folds -`s.order || 'asc'`. Everything else about the helper is unchanged — `id` is still -preserved when present and minted with `crypto.randomUUID()` otherwise, `field` still -defaults to `''`, and a non-array draft still yields `[]`. - -**The failure mode if you do not migrate is silent.** A draft entry spelled -`{ field: 'created_at', direction: 'desc' }` used to produce -`{ field: 'created_at', order: 'desc' }`; it now produces -`{ field: 'created_at', order: 'asc' }` — the documented default for an entry that names -no direction. Nothing throws and nothing warns: the `SortBuilder` row renders, and it -renders **ascending**. If you have a studio inspector draft, a persisted view body, or any -other producer that still writes `direction`, grep for the key and re-spell it to `order`. - -**Why the tolerant read went rather than staying.** objectui#4869 ruled that a spelling the -sink does not recognise gets ruled into the contract or rejected at the producer, never -absorbed by a tolerance layer. objectui#5293 retired the same word on -`ObjectViewProps.views[].sort` and shipped it as a `minor`; this entry finishes the job on -the sort family's public surface, so `order` is now the one spelling repo-wide and declared -equals enforced. The scope note in the objectui#5293 entry — that this export was *not* -retired by that change — described that release's scope correctly and is superseded here. - -`SortUI` is untouched. Its own file-local `toSortItems` is a different symbol, and -`direction` is the key `SortUISchema` legitimately declares. diff --git a/.changeset/6014-tree-grid-field-formatting.md b/.changeset/6014-tree-grid-field-formatting.md deleted file mode 100644 index 94d9c492ed..0000000000 --- a/.changeset/6014-tree-grid-field-formatting.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/plugin-tree': patch ---- - -`ObjectTree` formats its cells the way the flat table does: a lookup column renders the -referenced record's display name and a select column renders its translated option label, -instead of a raw record id and the raw stored value (objectui#6014). - -Reported against the built-in 业务单元 (`sys_business_unit`) page, whose 「组织架构」 tree tab -showed the manager column as a bare user id and the type column as `department`, while the -flat-table tab on the same page — over the same expanded records — showed the user's name -and 「部门」. - -The card carried its own control, and it pointed at the fetch rather than the formatter. The -tree treated "the host passed inline `data`" as "I do not need the object schema" and skipped -`getObjectSchema`, but its record-fetch branch prefers a live object dataSource over any -inline data. On the one mount shape `ListView` actually uses — `objectName` + a dataSource + -its own pre-fetched `data` — the tree therefore issued its OWN query with -`buildExpandFields(undefined)` → `[]` → no `$expand` at all, and had no field definitions to -format cells from. Both reported symptoms fall out of that single gap, which is why the flat -tab was unaffected and why the tree's existing tests (inline data, no dataSource — a path -that never runs the tree's own fetch) could not see it. - -Three changes, all inside `packages/plugin-tree`: - -- The object schema is fetched whenever the dataSource can serve one, not only when no host - passed inline data. The guard inside the fetch already no-ops without a dataSource, so the - pure inline/static path is unaffected. -- Records are no longer fetched until that schema has settled — settled, not necessarily - successful, so a rejected or inapplicable schema fetch can never block the tree. This also - removes a wasted first query whose lookup columns came back as bare ids and were painted - for a moment before the real query landed. -- Cell values route through a field-aware formatter that delegates both decisions rather than - re-deciding them: option labels through the `translateOptions` seam `ObjectGrid` already - uses for the flat tab (so both tabs read one `fieldOptions.*` i18n key, with the same - exact-then-case-insensitive match and `humanizeLabel` fallback as `SelectCellRenderer`), and - expanded references through `getRecordDisplayName`, the unified display-name resolver - (ADR-0079), with the family judged by `isExpandableFieldType` — the same predicate that - decided what to put in `$expand`. - -No new exports and no new package dependencies: both resolvers were already published from -`@object-ui/core`, and `translateOptions` was already reachable through the `useSafeFieldLabel` -hook this component calls for its column headers. - -One visible consequence beyond the report: an expanded record that comes back with no name-ish -field now reads as ADR-0079's `Record #` floor — the string every other surface shows for -it — rather than as the bare id. diff --git a/.changeset/6023-recent-item-type-keys.md b/.changeset/6023-recent-item-type-keys.md deleted file mode 100644 index f65580588d..0000000000 --- a/.changeset/6023-recent-item-type-keys.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -All ten locale packs now define `home.recentApps.itemType.report` and -`home.recentApps.itemType.metadata` (objectui#6023). `RecentItem['type']` is a six-member -union and `useTrackRouteAsRecent` writes both of these at runtime — `metadata` on any -`/metadata//` route visit, `report` on any `/report/` visit — but the -packs defined only four of the six, so the Recently Accessed and Starred cards labelled -those two items from the call sites' inline `defaultValue` instead of from the pack. - -Ten packs missing the same member is full parity, so no pack-vs-pack gate could see it, -and the prefix rule only ever asked whether `home.recentApps.itemType` resolved, which it -did. What made it quiet rather than loud is the `defaultValue`: English readers saw a -plausible `Report` / `Metadata` (lowercase in the rail) rather than a raw key, and the -other nine locales saw those English words — objectui#3517's mechanism for hiding a -missing key for months. - -The nine translations were taken from each pack's own existing rendering of the same word -rather than composed: the singular `Report` that `appDesigner.navReport`, -`appDesigner.navTypeReport` and `search.badgeReport` already carry (ar `تقرير`, de -`Bericht`, es `Informe`, fr `Rapport`, ja `レポート`, ko `보고서`, pt `Relatório`, ru -`Отчёт`, zh `报表`), and the `Metadata` that `layout.metadata.label` already carries (ar -`البيانات الوصفية`, de `Metadaten`, es `Metadatos`, fr `Métadonnées`, ja `メタデータ`, ko -`메타데이터`, pt `Metadados`, ru `Метаданные`, zh `元数据`). No new vocabulary was invented -for any locale. - -The two matching entries in `scripts/i18n-call-site-key-baseline.json`'s `missingMembers` -are cleared, since that list is a ratchet: a baselined entry whose defect is gone fails the -build too. The union itself is untouched — narrowing `RecentItem['type']` is the other -resolution the gate accepts, and it would have been a lie about data both call sites -demonstrably write. diff --git a/.changeset/6024-catalog-own-plugin-pin-generalized.md b/.changeset/6024-catalog-own-plugin-pin-generalized.md deleted file mode 100644 index 3d85725589..0000000000 --- a/.changeset/6024-catalog-own-plugin-pin-generalized.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Tests and test-config only, no shipped code touched: the catalog "entries use their own -plugin" pin in `examples/schema-catalog/test/catalog-gallery-render.test.tsx` is -generalized from the two hand-enumerated categories objectui#5113 and objectui#5856 left -it at (`plugin-view`, `plugin-grid`) to all thirteen `plugin-*` catalog categories. - -The `category -> type` map is now DERIVED from the `register()` calls, by reusing -`deriveRegistryKeys` from `scripts/check-doc-component-types.mjs` and joining its -registration sites on the `packages/` that owns them — the same derivation -`scripts/regenerate-known-schema-types.mjs` already consumes. The value is a SET, which -`plugin-charts` requires (its entries author `chart` and `bar-chart`), and the rule stays -per entry rather than per category. - -objectui#5113's RENDER half (a record that exists only in the gallery data source reaches -the screen) does not transfer to the eleven categories that author their data inline, so -it is replaced for them by a MOUNT half rather than degraded to the STRUCTURE half: -substituting every own-plugin node with an inert probe must put the probe's marker on -screen, which a stray node, a `type` that is not a node, and a node behind a satisfied -`hidden` cannot do. Data provenance is kept, unweakened, for the entries that bind to an -object the gallery fixture serves — derived per entry from `objectName`, not declared. The -gate states its own coverage split so breadth cannot be read as depth: 5 entries carry -structure + mount + data, 34 carry structure + mount, and `plugin-editor` / `plugin-map` -lose nothing to their `EXCLUSIONS` because the probe replaces the renderer that needs -Monaco and WebGL2. - -The derivation earned its keep on its first run: the filing's hand-written table read -`plugin-form form`, and `form` is registered by `packages/components`, not by -`@object-ui/plugin-form`. Both `plugin-form` entries are the third instance of -objectui#5113's class, ledgered against objectui#6167 with a case asserting they still -fail, so the ledger cannot rot green. - -`examples/schema-catalog/tsconfig.test.json` gains `allowJs` (with `checkJs` off) and a -repo-root `rootDir`, the same answer `tsconfig.scripts.json` reached in objectui#3494 for -importing a plain-JS CI helper into a typed pin test. diff --git a/.changeset/6025-declare-plugin-grid-in-gallery-host.md b/.changeset/6025-declare-plugin-grid-in-gallery-host.md deleted file mode 100644 index abfbccde32..0000000000 --- a/.changeset/6025-declare-plugin-grid-in-gallery-host.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Docs-site host and catalog test only: `apps/site/app/components/registerCatalogBlocks.ts` -now declares `@object-ui/plugin-grid` instead of inheriting `object-grid` from -`@object-ui/plugin-view`'s `import { ObjectGrid }` (`ObjectView.tsx:37`). Nothing renders -differently — measured: with exactly the eleven packages the host used to carry, -`object-grid` and every `@object-ui/plugin-form` key already resolved — which is the point. -A component import in another package was the only reason two catalog tiles could draw, and -a refactor of `object-view` that stopped it from drawing a grid itself would have turned -them into OBJUI-001 panels with the cause several files from the symptom. - -`catalog-gallery-render.test.tsx` gains the judge that can tell the two apart: every -`plugin-*` category with catalog entries must be loaded BY NAME in the host's import list, -derived from the categories rather than enumerated, and leaning on the existing parity case -that ties `HOST_PACKAGES` to the host file's literal imports. `ComponentRegistry.get()` -cannot judge this — it is truthy in both worlds. diff --git a/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md b/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md deleted file mode 100644 index 069e67899a..0000000000 --- a/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Tests only; no published behaviour changes. - -Adds an offline assertion that the shipped -`packages/components/src/ui/sidebar.tsx` contains no Tailwind **v3** -`[--var]` arbitrary values, wired into the existing -`scripts/__tests__/shadcn-local-patches.test.ts` suite (no new workflow). - -The Tailwind v4 custom-property migration (`925051db6`) is an *undeclared* -local edit — prose in `shadcn-components.json` (`localEdits`), with nothing in -`scripts/shadcn-local-patches.mjs` re-applying it — so a `--force` sync drops -it. It is the only one of `sheet`/`sidebar`'s four undeclared edits that no -gate catches: the v3 spelling compiles, renders, and emits invalid CSS that -browsers silently discard. The assertion closes that silence from the cheap -side rather than declaring ~20 class-string anchors. - -Asserted against the file the repo **ships**, not a vendored fixture — the -regression being guarded is "a forced sync overwrote the shipped file", which a -fixture-based check would pass straight through. The block also guards the -inverse (the correct v4 `(--var)` spellings must not trip it) and pins the -tolerated/refused counts, and its docblock records the other three undeclared -edits with the gate that catches each. diff --git a/.changeset/6038-production-predicate-fault-warning.md b/.changeset/6038-production-predicate-fault-warning.md deleted file mode 100644 index 8110de9e00..0000000000 --- a/.changeset/6038-production-predicate-fault-warning.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/react': patch -'@object-ui/components': patch ---- - -A node-gate visibility predicate that FAULTS now says so in a production build, once per -distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the -silence is no longer an accepted property"). Observability only — no verdict moves. - -`SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated -resolves to the same answer as one that said yes, so a gate that stops biting looks -exactly like a gate the author got right. The diagnostic that names it (objectui#5454 / -objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection -channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition` -implements that by evaluating **twice** — too expensive to ship for every predicate of -every node. - -**What production actually printed before, measured per dialect on the built evaluator** -— the card's premise held for one dialect of three, and the other two failed in opposite -directions: - -| dialect | production console, before | -|---|---| -| bare string | **nothing** | -| `{ dialect: 'cel' }` envelope | one generic line, deduped per source | -| `${…}` template | one generic line **per evaluation**, never deduped | - -So the dialect objectstack#11254 measured a live gate breaking on was the silent one, -while the template dialect was the console flood the ruling's rate-limit clause exists to -prevent. - -**The fix reports the fault the evaluator already detected, at the same number of engine -calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s -`ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the -canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer -up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason -passback), and supplying it transfers reporting to the caller so one fault stays one -line. Pinned: the CEL branch performs the same number of record reads with the passback -as without it, and strictly fewer than the `throwOnError` probe. - -`SchemaRenderer` passes it in production and reports through the **same** reporter the dev -branch uses — same message, same severity, same dedupe `Set`, same key. Development and -production now print the identical line for the identical fault; the `__DEV__` gate no -longer decides *whether* a fault is reported, only *how* it is detected. - -`page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same -reporter and the same rate limit. It swallowed the identical fault under a different -helper, and it was the worse of the two: the node gate at least reported in development, -while a faulting item predicate was silent in *both* builds on a gate whose false verdict -removes an entire tab, header and panel. - -**Rate limit:** deduped per (node type, gate key, predicate source) — never per render and -never per node instance. A two-hundred-row list of one broken predicate is one line; a -second distinct predicate source still gets its own line. Both halves are pinned, because -a test that asserts only "a warning was emitted" is equally green on an implementation -that emitted fifty, and one that asserts only "exactly one" is equally green on an -implementation that suppresses everything. - -**Not changed by this card, deliberately:** the fail-open semantics themselves; the -objectui#5687 adapter-only `data.*` report, which stays development-only under its own -2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against -the wrong object); and the `/forms/:name` scope wiring of objectui#6262, which lands in -its own PR. - -`reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`, -`UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported -from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one -reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one -authored predicate would be entitled to one line per package instead of one line. diff --git a/.changeset/6041-designer-reference-key.md b/.changeset/6041-designer-reference-key.md deleted file mode 100644 index 803ca1328c..0000000000 --- a/.changeset/6041-designer-reference-key.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/plugin-designer': patch -'@object-ui/app-shell': patch ---- - -The field designer now reads and writes a lookup field's relationship target under the -spec's spelling `reference` (objectui#6041), in both directions. - -`referenceTo` is not in `FieldSchema`'s accept set. Measured against the installed -`@objectstack/spec` 17.2.0, through the whole object document that -`PUT /api/v1/meta/object/:name` validates: - -``` -ObjectSchema.safeParse({ …, fields: { rel: { type: 'lookup', label: 'Owner', - referenceTo: 'user' } } }) - => success = false - => unrecognized_keys at ["fields","rel"] keys=["referenceTo"] - "Did you mean `referenceTo` -> `reference`?" -``` - -so authoring a lookup field through the designer returned a hard 422 `INVALID_METADATA`, -and — because the key is then stored — blocked **every subsequent save** of that object, -with nothing in the UI to say which key did it. - -The read direction was broken symmetrically and is the half that would have survived a -write-only fix: `toDesignerField` read `raw.referenceTo` while a spec-parsed server sends -`reference`, so every already-saved lookup field loaded into the designer with an **empty -reference box**. Both wire-bound payload shapes move — `FieldMetadataPayload` -(`MetadataService.toFieldPayload`) and `ServerFieldSchema` -(`MetadataFieldsPage.fromDesignerField`). - -`referenceTo` also joins `RETIRED_FIELD_KEYS`. Renaming the emit sites alone does not -unblock an object whose stored fields already carry the misspelling: `carryOver` spreads -the previous server def verbatim, so the key would ride straight back out to the same 422. -The designer's in-memory `DesignerFieldDefinition` keeps `referenceTo` — that is the -internal prop name every other UI surface in this repo already uses (`LookupField`, -`filter-builder`, `ObjectChart`, `ListView`, `UserFilters`), it reaches no wire-bound -shape, and the parity gate classifies it as `uiOnly` rather than a violation. - -No behavioural change for a half-filled draft: the spec's prose calls `reference` -"required for relationship types", but that is not enforced by the zod parse at 17.2.0 — -`{ type: 'lookup', label: 'L' }` parses green at field level and through `ObjectSchema`, -and `undefined` is dropped by `JSON.stringify` under either spelling, so the wire bytes -are identical before and after. diff --git a/.changeset/6043-retire-designer-formula-control.md b/.changeset/6043-retire-designer-formula-control.md deleted file mode 100644 index 00f6f3a85e..0000000000 --- a/.changeset/6043-retire-designer-formula-control.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-designer': minor -'@object-ui/app-shell': minor ---- - -The Field Designer no longer offers a formula-expression textarea, and no designer write -path emits a `formula` key (objectui#6043). - -**This is a behaviour change on an authoring surface: a control is removed.** A field's -`type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the -palette — but the expression itself is no longer authored here. Authors write formula -expressions in metadata-admin's field inspector, where they are checked. - -The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against -the installed `@objectstack/spec` 17.2.0: - -``` -FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) - => success = false - => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?" -``` - -so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because -the key was then stored, it blocked **every later save of that object**, not just the one -that introduced it. - -**The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges -the key name and never the expression LANGUAGE — measured, it accepts -`expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the -empty string is refused. Spec `expression` is CEL rooted at `record` -(`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity` -— bare field refs, which under the scope formulas bind evaluate to null silently. A rename -would therefore have converted a loud, immediate 422 into a formula that saves clean and -then quietly computes nothing, which is strictly worse than the bug it appears to fix. - -Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType` -inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and -app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a -dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a -feature, not this fix. `returnType` is likewise not authored here: it is only derivable by -inferring the CEL result type, and with no expression control there is nothing to infer -from. - -`formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already -carrying the key is stripped clean on its next save instead of staying blocked forever — -which matters more than usual here, because with the control gone an author would -otherwise have no way left to clear it. It is dropped rather than migrated to `expression`, -for the same reason the rename was refused. A `expression` authored in metadata-admin is -**not** touched: it is a real `FieldSchema` key and rides through the designer's -round-trip untouched. - -Also removes the now-unreachable `formula` read/write from -`views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for -the key that neither the card nor the parity gate named. - -The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s -`KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that -left a stale entry behind would be as red as a new offender. diff --git a/.changeset/6044-designer-system-key.md b/.changeset/6044-designer-system-key.md deleted file mode 100644 index 09a3c92462..0000000000 --- a/.changeset/6044-designer-system-key.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-designer': patch ---- - -The field designer now reads the system-field marker under the spec's spelling `system`, -and never hands `isSystem` back to the metadata API (objectui#6044). - -`isSystem` is not in `FieldSchema`'s accept set. Measured against the installed -`@objectstack/spec` 17.2.0: - -``` -FieldSchema.safeParse({ type: 'text', label: 'L', isSystem: true }) - => success = false - => unrecognized_keys keys=["isSystem"] "Did you mean `isSystem` -> `system`?" -``` - -Two defects, one misspelling, and they are two different sites. - -**The read was dead** — the quieter and worse half. `toDesignerField` read `raw.isSystem` -while a spec-parsed server sends `system`, so the flag was always `undefined`. Nothing went -red, because the flag is optional and `undefined` is a valid "not a system field". But it is -load-bearing: `FieldDesigner` refuses to delete a system field and disables its name and -type inputs, so with the read dead `organization_id`, `created_at` and friends presented as -ordinary editable, **deletable** business fields. - -**The write had no emit site at all.** `fromDesignerField` never names `isSystem`; its only -route out is the verbatim `...carryOver(prev)` spread, so a stored misspelling round-tripped -back to `PUT /api/v1/meta/object/:name` as a hard 422 `INVALID_METADATA` that blocks every -later save. The repair is a `RETIRED_FIELD_KEYS` tombstone rather than a renamed line — and -it is deliberately paired with the read fix, never a substitute for it: stripping alone would -close the 422 and fossilize the dead detection. The spec spelling `system` is not stripped, -so a server-injected flag rides through untouched and feeds the read. - -`app-shell`'s `FieldMetadataPayload` never declared the key, so `toFieldPayload` had nothing -to fix. The designer's in-memory `DesignerFieldDefinition` keeps `isSystem`: it reaches no -wire-bound shape and the parity gate classifies it as `uiOnly`. diff --git a/.changeset/6045-field-payload-sort-order.md b/.changeset/6045-field-payload-sort-order.md deleted file mode 100644 index c18dc4bd9b..0000000000 --- a/.changeset/6045-field-payload-sort-order.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/types': minor ---- - -The field metadata payload no longer emits `sortOrder`, the key `FieldSchema` refuses by -name (objectui#6045). Field-level sibling of objectui#6223, same objectui#5761 family. - -Measured against the installed `@objectstack/spec` 17.2.0, whose `FieldSchema` accept set -is 71 keys: - -``` -FieldSchema.safeParse({ type:'text', label:'L' }) => success = true (control) -FieldSchema.safeParse({ type:'text', label:'L', sortOrder: 3 }) => unrecognized_keys ["sortOrder"] - -FieldSchema.safeParse({ type:'text', label:'L', sortable: true }) => success = true (control) -FieldSchema.safeParse({ type:'text', label:'L', sortable: 3 }) => success = false -``` - -The control is what makes that a key-by-key result rather than a schema refusing -everything, and the `sortable` pair is what shows the near-spelling is a *different -concept* — a boolean ("whether field is sortable in list views"), not this key's spec -name. - -**The resolution was deletion, not a rename**, which is objectui#4687's shape rather than -objectui#6041's. The spec has no field-level ordering key at all: it models field order by -**declaration order** in the object's `fields` record, so a designer that wants explicit -ordering reorders that record rather than carrying an index. There was nothing to map onto, -and nothing was invented to map onto. - -**It was latent, and that is confirmed on today's tree.** Neither of the two sites that -construct a `DesignerFieldDefinition` — `FieldDesigner`'s create/update handlers and -`MetadataFieldsPage.toDesignerField` — ever named the key, so `toFieldPayload` emitted -`sortOrder: undefined` and `JSON.stringify` dropped it. The key never reached the wire. It -was one reorder feature away from doing so, which is the objectui#4644 shape: a hard 422 -`INVALID_METADATA` that blocks every subsequent save of the object, with nothing in the UI -to say which key caused it. - -Removed in one go from the wire shape (`FieldMetadataPayload`), its writer -(`toFieldPayload`) and the UI model (`DesignerFieldDefinition`), so no declaration is left -behind that no writer fills and no schema accepts. - -**Breaking for TypeScript consumers**: `sortOrder` is gone from `DesignerFieldDefinition` -(`@object-ui/types`) and from `FieldMetadataPayload` (app-shell), so code that set either -stops compiling. - -Two keys share this spelling and are untouched, which is why the census was on the *shape* -— a field-metadata payload key `FieldSchema` refuses — rather than on the identifier: the -**object-level** `sortOrder` (`ObjectSchema`'s, removed from the object wire shape by -objectui#6223 and deliberately kept on the `ObjectDefinition` UI model) and the -**saved-view** `sortOrder` in `ObjectView`, which is per-view display order on a different -document entirely. - -The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes -with the fix — that ledger ratchets in both directions, so an entry left behind for a -resolved key is as red as a missing one. diff --git a/.changeset/6050-gantt-navigation-basepath.md b/.changeset/6050-gantt-navigation-basepath.md deleted file mode 100644 index e7b6c13c0a..0000000000 --- a/.changeset/6050-gantt-navigation-basepath.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -The package README stops documenting a `navigation` key the spec refuses, and the corrected example is now parsed by the schema that validates it. - -`README.md`'s record-navigation override read -`{ mode: 'page', basePath: '/console/apps/.../campaign' }`. `basePath` is not a -member of the spec's `NavigationConfig`, and nothing consumes it: -`useNavigationOverlay` — where a gantt's `navigation` lands — builds no URL out -of the config, and `ObjectGantt` calls the hook with no `onNavigate`, so a -page-mode click falls through to the host's `onRowClick`. The destination route -is owned by the host and was never authorable through this key, under any -spelling. - -That made the snippet worse than inert. `NavigationConfigSchema` is a strict -object with no passthrough, so the undeclared key did not fall away quietly — it -rejected the **whole** config with `unrecognized_keys`, taking down the -`mode: 'page'` the sentence was actually teaching. An author who copied the -documented snippet got a rejected navigation config and no page navigation, which -is the copy-the-snippet-get-rejected shape objectui#5057 / #5012 named on other -keys. - -The example is corrected to `{ "navigation": { "mode": "page" } }` — the shape -the sentence demonstrates — and the prose now says who owns the destination route -and points at `@objectstack/spec`'s `NavigationConfigSchema` for the member list -instead of restating it, matching the derivation `ObjectGanttSchema.navigation`'s -doc comment (objectui#5903) adopted for the same concept. - -`view` is **not** substituted for `basePath`. It is a declared member, but it -names a form view (the spec: *"Name of the form view to use for details"*) and is -forwarded to `onNavigate` as the action argument — it is not a route, so putting -it where `basePath` stood would have replaced an invented key with a wrong one. -It is documented for what it does. - -No gate in this repo could have caught the original defect, and that is why the -fix ships with a measurement rather than a re-reading: `check-doc-snippet-types` -compiles `ts`/`tsx` fences and `check-doc-component-types` reads `type` literals, -and both are structurally blind to a metadata key in a README — the former's own -header records schema-key validity as "a different question … left unruled on -purpose". `src/readme-navigation-example.test.ts` closes that hole for this -example by EXTRACTING the fence from the README on every run and parsing it -against `NavigationConfigSchema`, with a control asserting the same parse still -rejects an undeclared key by name, so the green cannot come from a schema that -accepts everything. - -`tsconfig.test.json` names `node` in `types` for that test to compile, and its -comment — which had recorded that no test in this package touches a Node global -— is corrected rather than left standing. diff --git a/.changeset/6051-gantt-flat-config-declared-keys.md b/.changeset/6051-gantt-flat-config-declared-keys.md deleted file mode 100644 index 7aa10e1dc3..0000000000 --- a/.changeset/6051-gantt-flat-config-declared-keys.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -`ObjectGanttSchema` declares the flattened `GanttConfig` face `ObjectGantt` -actually reads (objectui#6051). `getGanttConfig` has two branches: when -`startDateField` and `endDateField` are both present at the TOP level it builds -its config from top-level keys and returns early; otherwise it reads the `gantt` -block. Everything the first branch reads was undeclared — and unlike -objectui#5903's ten, none of it was hidden behind a cast. `BaseSchema` carries -`[key: string]: any` (objectui#5155's structural ceiling) and the helper's -parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as -`any` with no syntax anywhere to grep for. That is also why the census here is an -AST enumeration and not a compile-and-observe: an index signature absorbs every -literal name, so annotating the parameter compiles clean while enforcing nothing. - -**27 keys join the declared surface, each additive and each with a live read -site.** 24 flattened `GanttConfig` members — `colorField`, `borderColorField`, -`dependenciesField`, `parentField`, `typeField`, `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `tooltipFields`, `baselineStartField`, -`baselineEndField`, `groupByField`, `resourceView`, `assigneeField`, -`effortField`, `capacity`, `quickFilters`, `autoZoomToFilter`, `timeSegments`, -`interactions`, `exportFileName`, `timeZone`, `dependencyTypes` — plus the three -query keys the fetch path reads, `staticData`, `filter` and `sort`. Nothing is -declared that the renderer does not consume. - -**`GanttConfig` itself gains nine members and is a published type**, exported by -name from `packages/types/src/index.ts`: `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, -`timeZone`, `exportFileName`, `interactions`. The entry file's diff is empty only -because the export list already named the type — the widening happened at the -declaration. - -**The 28th measured key, `gantt` (the block face), is deliberately NOT declared** -— see the closing section. - -The 24 are DERIVED from `GanttConfig` rather than restated, so the flat spelling -cannot fork from the block spelling, and the invariant is pinned in the type -system: every key of `GanttConfig` must be declared at the node's top level. -Making that derivation possible moved nine members — `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, -`timeZone`, `exportFileName`, `interactions` — out of `plugin-gantt`'s -package-private `GanttConfigEx` and into `@object-ui/types`' `GanttConfig`. They -are a MOVE, not new vocabulary: the `gantt` block already honoured all nine, and -a type private to the plugin could be referenced by neither authoring face. - -Both halves move together, as in objectui#5903: the TS declaration and its zod -mirror gain the same 27 keys at the same requiredness (all optional), the -spec-modelled ones taken from `GanttConfigSchema.shape` by reference, so the -`zod-mirror-parity` ratchet stays at zero drift for this pair and no `KnownDrift` -or `UnmirroredDeclared` entry is added. The mirror builds the flat face and the -`gantt` block from one field map, so they are one schema expressed twice. - -Accept-set change, stated plainly. All 27 keys are additive — every one is -optional, and nothing previously legal loses its slot. What changes is that a -**declared** key is now type-validated, so `capacity: 'one'` and -`summaryExtent: 'parent'` are refused where they used to parse green. An -**undeclared** key is still accepted — `BaseSchema` is `.passthrough()`, so this -bought no rejection of misspellings. There is no narrowing anywhere in this -change. - -**`gantt` is severed on purpose (objectui#6475), not overlooked.** It is the 28th -key of the measured residue and a genuine read — `getGanttConfig`'s second branch -honours it in full — but it is the one key whose declaration would NOT have been -additive. It has no mirror entry today, so a block rides through `.passthrough()` -unvalidated; declaring it as `GanttConfig` means it gets parsed against the spec's -`GanttConfigSchema`, which REQUIRES `startDateField`, `endDateField` and -`titleField`, and `ObjectGanttSchema` reaches the CLI's `validate` / `check` -through `AnyComponentSchema`. A published CLI's refusal behaviour is decided on -its own card, where reviewers can see what they are approving; objectui#6475 -carries the full measurement, including the case FOR enforcing it (the renderer -already feeds that block to `GanttConfigSchema.safeParse` and warns, so enforcing -restores declared = enforced rather than inventing a contract). Today's behaviour -is pinned in the test file so the omission is a measured state, not a silent gap. -`packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts` pins both -halves so neither can be misread. - -Which face WINS is unchanged and was not decided here: the flat branch is checked -first and returns early, so a node carrying both spellings still renders the flat -one. (`plugin-map` had the opposite precedence ruled on in objectui#5018; no -equivalent ruling exists for gantt.) diff --git a/.changeset/6052-gantt-export-filename-i18nlabel.md b/.changeset/6052-gantt-export-filename-i18nlabel.md deleted file mode 100644 index 5fffb4af6a..0000000000 --- a/.changeset/6052-gantt-export-filename-i18nlabel.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -`ObjectGantt`'s export filename resolves a locale-map `label` instead of stringifying it -(objectui#6052). A gantt authored as -`{ "type": "object-gantt", "objectName": "task", "label": { "en": "Shift Plan", "zh-CN": "排班计划" } }` -exported its PNG/PDF as `[object Object]-20260825-1030.png`. - -`BaseSchema.label` is `string | I18nLabel` since #4580's revised Q1-A ruling — `I18nLabel` -being the spec's INLINE locale MAP — and the `exportFileName` chain handed that value -straight to `String(...)`. It now goes through `resolveI18nLabel` from `@objectstack/spec/ui`, -the producer's own resolver for that vocabulary, against the display locale the file already -reads via `useDisplayLocale()`. A zh-CN audience gets `排班计划-.png`, an en audience -`Shift Plan-.png`, and a plain-string label is unchanged. - -The next link in the same chain, `objectSchema?.label`, is deliberately left alone: that is -the DATA object's label, declared `z.string().optional()` on the spec's `ObjectSchemaBase`, -which is a `strictObject` — a locale map there is rejected by the producer rather than -resolved by the consumer, and wrapping it would be accepting a second vocabulary at a read -site. No filename sanitisation is added either; `GanttView` already strips -filesystem-hostile characters downstream, and a resolved map entry goes through the same -strip a plain string does. diff --git a/.changeset/6058-zod-mirror-unmirrored-ledger.md b/.changeset/6058-zod-mirror-unmirrored-ledger.md deleted file mode 100644 index ff4311f11e..0000000000 --- a/.changeset/6058-zod-mirror-unmirrored-ledger.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only: `packages/types`' zod-mirror-parity guard now compares the UNION of the -mirror's keys and the declaration's, and ledgers the newly visible half separately -(objectui#6058; the stale prose counts objectui#6141 measured are corrected in the same -file). No published type, mirror or runtime behaviour changes — nothing in `src/` outside -`__tests__/` is touched, so there is nothing to release. diff --git a/.changeset/6059-publish-omit-server-resolved-defaults.md b/.changeset/6059-publish-omit-server-resolved-defaults.md deleted file mode 100644 index 9c403d943d..0000000000 --- a/.changeset/6059-publish-omit-server-resolved-defaults.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-form': minor -'@object-ui/console': patch ---- - -Publish the create-payload rule from `@object-ui/plugin-form`'s entry, so a -second form renderer can call it instead of composing it by hand -(objectui#6059). - -Newly importable from `@object-ui/plugin-form` — two functions, nothing else: - -```typescript -import { omitServerResolvedDefaults, isRequiredInForm } from '@object-ui/plugin-form'; -``` - -- `omitServerResolvedDefaults(values, objectSchema)` — drop the keys a CREATE - payload must leave to the producer: a field whose declared `defaultValue` is a - runtime instruction (`NOW()` / `current_user`, or a CEL envelope) and whose - submitted value is empty. `ObjectQL.applyFieldDefaults` resolves a declaration - only for a field that arrives absent or null, so submitting a blank stores - `''` and silently defeats it. **Create-only** — the caller keeps the mode gate. -- `isRequiredInForm(field, isCreateForm)` — the `required` a form should - enforce, given the mode. Published as the pair's other half on purpose: - excusing a server-owned field from `required` and then submitting the key - anyway is not half a fix, it is no fix. - -Both are pure functions over plain data (no React, no registry). The rest of -`schemaDefaults.ts` — `seedCreateValues`, `schemaDefaultValues`, -`isSeedableDefault`, `isCreateFormMode`, `SeedContext` — stays module-private, -and `isRuntimeDefault` stays `@object-ui/core`'s to publish. - -No behaviour change. The console's `FormPage` now calls the published helper -instead of composing `isRuntimeDefault` + `isMissingForRequired` locally; its -create payload is decided identically before and after, pinned against the -deleted implementation over the full matrix of default shapes, value spellings -and both modes. diff --git a/.changeset/6063-kanban-resolvedisplay-dead-guard.md b/.changeset/6063-kanban-resolvedisplay-dead-guard.md deleted file mode 100644 index 56fc560050..0000000000 --- a/.changeset/6063-kanban-resolvedisplay-dead-guard.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -`ObjectKanban`'s `resolveDisplay` drops an unreachable relation-type guard, and the rule -that survives it is pinned (objectui#6063). - -The helper that builds card descriptions ended: - -```ts -const isLookup = isExpandableFieldType(def); -if (isLookup && isOpaqueId(raw)) return undefined; -if (isOpaqueId(raw)) return undefined; -return raw; -``` - -The second line subsumes the first for every input — same `raw` (a `const`, unmodified -between the two lines), same predicate, and `OPAQUE_ID_RE` carries no `g`/`y` flag, so -repeated `.test()` on it is stateless. `isLookup` was computed, branched on and discarded. - -**No behaviour changes on any board.** The card named a second reading — that the -unconditional line was the mistake and only relation columns were meant to suppress -id-shaped values — and it was rejected on evidence rather than by tidying first: the -helper's own docblock declares both clauses, the same predicate is already applied with no -type gate to the incoming `description` a few lines down, and `objectDef` is optional at -that read, so a type gate would suppress nothing on exactly the boards whose object schema -is thin or absent. That reading is now a red test, not a comment. - -Deleting the branch also deleted this path's read of `@object-ui/core`'s -`EXPANDABLE_FIELD_TYPES`, so objectui#5874's identity pin for this face is re-anchored onto -the read that is live — `buildExpandFields`, on every fetch — where the membership delta is -observable on the wire (`$expand`) as well. That pairs the identity pin with the -behavioural counter-probe #5874 had to record as missing. diff --git a/.changeset/6067-component-meta-derive-from-canonical.md b/.changeset/6067-component-meta-derive-from-canonical.md deleted file mode 100644 index d524f38c01..0000000000 --- a/.changeset/6067-component-meta-derive-from-canonical.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`ComponentMeta` at the registry is now DERIVED from the one declaration in -`@object-ui/types` instead of restating it, and `tags` / `description` reach the -registration surface (objectui#6067). - -## The convergence - -`packages/core/src/registry/Registry.ts` declared its own `ComponentMeta`: thirteen -keys, of which nine were restated from `@object-ui/types`' `base.ts`, four were -registry-only (`tier`, `namespace`, `skipFallback`, `labelling`), and `tags` / -`description` were **absent** — although both are declared on the canonical type and on -the `ComponentMetaSchema` zod mirror. Two of the three authorities agreed and the -registration surface did not, so those two keys were unwritable at exactly the -declaration most component registrations import. That is the same two-key delta -objectui#5893 had just closed inside `@object-ui/types`, arriving a third time on a -third declaration, and objectui#5671 had already made the identical move for the sibling -type `ComponentInput` in this very file. - -It is now: - -```ts -export type RegistryComponentMetaExtras = { - tier?: 'public' | 'internal'; - namespace?: string; - skipFallback?: boolean; - labelling?: 'control' | 'group' | 'display'; -}; - -export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras; -``` - -`RegistryComponentMetaExtras` is newly exported from `@object-ui/core`. - -**What changes for a consumer: `tags` and `description` become writable on the registry's -`ComponentMeta`. Nothing narrows.** No key is removed, no key is renamed, and no key's -type changes, so no existing registration stops compiling — verified by type-checking all -37 workspace consumers of `@object-ui/core` (`pnpm --filter '...@object-ui/core'`), which -is why this is a widening rather than the contract break a rename would have been. All -four registry-only keys have live consumers, and they are still declared here. - -This is `minor` under this repository's policy that its own breaking changes never declare -`major` (`scripts/check-changeset-no-major.mjs`); nothing here is breaking in any case. - -## Converge rather than rename, and why the four keys did not move - -The alternative dispositions were to rename the type so the name stops claiming a mirror, -or to move the four registry keys onto `@object-ui/types`' `ComponentMeta` and re-export -it outright the way objectui#5671 handled `ComponentInput`. - -Renaming was rejected because it cannot be done without a break: `@object-ui/core` is -published, `ComponentMeta` is exported from it, and dropping the name would break every -external consumer — while keeping it as an alias would leave the mirror claim standing -under a second spelling, which fixes nothing. - -Moving the four keys was rejected because `skipFallback` and `namespace` are registration -mechanics — they describe how the registry keys an entry, not what a component is — and -`@object-ui/types`' `ComponentMeta` is the general, plugin-facing, AI-facing type. The -extension keeps them where they are read, under their own named type, while the eleven -shared members exist in exactly one place and can no longer drift. - -## Pinned by key set, not by assignability - -Every member of both shapes is optional, so `extends` is mutually **true** across the -diverged pair — an assignability assertion is green on the defect and would not have -caught it. Measured on the emitted `.d.ts` of both packages, before and after: - -| reading | before | after | -|---|---|---| -| `Core extends Canonical` | `true` | `true` | -| `Canonical extends Core` | `true` | `true` | -| `Exclude` | `"tags" \| "description"` | `never` | -| `Exclude` | the four registry keys | the four registry keys | - -The new pin asserts the third row and names the fourth explicitly; the assignability pair -is kept beside it, labelled, as the control that shows what it cannot see. A source-level -assertion that the canonical members are not restated locally covers the remaining failure -mode — a member-identical copy, which every `keyof` comparison stays green on and which is -how the copy this replaces began. diff --git a/.changeset/6074-check-ts-stale-migration-comment.md b/.changeset/6074-check-ts-stale-migration-comment.md deleted file mode 100644 index 9789b66adb..0000000000 --- a/.changeset/6074-check-ts-stale-migration-comment.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Comment-only fix in `packages/cli/src/commands/check.ts`: the note beside the -structural marker gate promised that "the corpus migration repays" the recall -debt — the real schema files the checker skips because their root carries no -structural key. No such migration exists and none can be written. The marker it -would stamp is the `$schema` URL the 2026-08-20 ruling declined to mint, and -`OBJECTUI_SCHEMA_URL` / `pointsAtObjectUi()` were deleted before objectui#5334 -merged. The same file already states, twenty lines earlier, that there is -deliberately no `$schema` arm and no URL to declare — so the file contradicted -itself. - -The note now says what is true: the recall debt is real and unpaid, the -`$schema` route was ruled against, a corpus-wide `$schema` sweep is a measured -no-op on the skipped-file count, and the arm can be added later without -invalidating a single file because matching would be host-based. It points the -next reader at the tracking card and its blocker instead of at a dead route. - -No behaviour change and nothing to release — `objectui check` judges, skips and -prints exactly what it did before. diff --git a/.changeset/6075-check-validity-recogniser-recall.md b/.changeset/6075-check-validity-recogniser-recall.md deleted file mode 100644 index 7aeccd03d2..0000000000 --- a/.changeset/6075-check-validity-recogniser-recall.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/cli': minor ---- - -`objectui check` recognises a schema by validating it, and reports broken ObjectUI files instead of filing them as foreign ones. - -A file with a root `type` was judged only when its root carried an ObjectUI -structural key (`children`, `body`, `className`, …). Leaf schemas carry only -their own vocabulary, so nothing checked them: measured on this repository, 475 -files were eligible, 166 were judged and 309 were skipped. - -The command now has a second recogniser arm — the document validates as an -ObjectUI component schema under `@object-ui/types`' own Zod union — which the -maintainer's 2026-08-25 ruling selected over shipping a JSON Schema artifact to -point a `$schema` URL at. It admits 209 of those 309 files. The structural arm -still runs first, so recognition costs nothing for files that already had a -marker, and `package.json` is still never judged: `"type": "module"` names no -component the protocol models. - -Validity alone would have answered two different questions with one word. -A broken ObjectUI schema fails validation exactly as a foreign file does, so a -two-bucket report would have filed it as "not ObjectUI" — and the symptom of -that is an absence: the file simply stops being mentioned. Measured, that bucket -is not empty: 54 files land in it and 53 of them are real corpus content. - -So files the recogniser refuses are split. When the root `type` names a -component this build registers, the file is **listed by name** as ObjectUI -content that did not validate, pointing at `objectui validate ` for the -reason — either the document is off-spec or its component type is not modelled -by `@object-ui/types`. Everything else is counted as skipped, as before. The -printed explanation now describes both arms, and only unreadable JSON still -makes the command exit non-zero. diff --git a/.changeset/6081-release-lane-drop-changeset-prediction.md b/.changeset/6081-release-lane-drop-changeset-prediction.md deleted file mode 100644 index 3d99006fab..0000000000 --- a/.changeset/6081-release-lane-drop-changeset-prediction.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI-only change: the `lane` job in `.github/workflows/changeset-release.yml` no longer -predicts whether `changesets/action` will see a pending changeset. Its bash mirror of -the action's file scan and the `pending_changesets` output it fed are deleted, and the -release job's clear step now runs on every push run — `if: github.event_name == 'push'`. - -This supersedes the mirror that `5775-lane-changeset-reader-mirror.md` describes, and -with it `scripts/__tests__/changeset-release-lane-mirror.test.ts`. The mirror could not -be kept honest: `changesets/action` is used at a moving tag whose bundled -`@changesets/read` is a different major from the installed one, and it is a minified -chunk nothing here installs, so no test could execute it and no lockfile recorded it. -The `changesets/action@v1` ref pin that lived in that test file is re-homed, not -dropped — it now has its own file, -`scripts/__tests__/changeset-release-action-ref-pin.test.ts`, and covers both `uses:` -lines instead of one. - -No published package changes and no release behaviour changes: the clear step is -idempotent and a measured no-op on a tree with nothing pending (`Removed 0`, exit 0, -byte-identical tree), so running it unconditionally on the push lane does what the -prediction was there to arrange. The `github.event_name == 'push'` test is retained -deliberately — dropping it too would empty the tree on the refresh lane and fossilise -the version PR silently. diff --git a/.changeset/6089-hook-selftests-step-names-drop-counts.md b/.changeset/6089-hook-selftests-step-names-drop-counts.md deleted file mode 100644 index d519e4ab50..0000000000 --- a/.changeset/6089-hook-selftests-step-names-drop-counts.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -CI-only change: no published package's `src/` changed. - -Drops the hard-coded case counts from the two step names in -`.github/workflows/hook-selftests.yml` (`guard-main-checkout-bash self-test -(100 cases)` / `guard-shared-stash self-test (32 cases)` → the bare names). -Nothing derived those numbers and nothing re-checked them, and a step name has -no runtime behaviour, so a stale count could only ever show a wrong number on -the checks page. Each run's own tail already prints `N passed, N failed`, and -`guard-shared-stash.sh`'s header carries its count with the recipe to -re-derive it. The `Cost` note keeps its counts, restated as a dated -measurement (`2026-08-24` @ `53dc89db8`) — history, not a live claim — and a -new header section records why the two are treated differently. What the -workflow runs is unchanged: same job, same steps, same `run:` commands. diff --git a/.changeset/6092-entry-guard-sweep.md b/.changeset/6092-entry-guard-sweep.md deleted file mode 100644 index e63819ad6d..0000000000 --- a/.changeset/6092-entry-guard-sweep.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Tooling only, no package released: every `scripts/**` entry guard now goes through one predicate. The 29 hand-typed guards `check-entry-guard.mjs` baselined (nine distinct spellings across 28 `.mjs` files, plus `shadcn-sync.js`) are converted to `isEntrypoint(import.meta.url)` and `KNOWN_HAND_TYPED_GUARDS` is empty. Twenty-eight of them were silently wrong: reached through a symlink they compared two different paths, answered `false`, and did nothing — exit 0 with no output, which a CI wrapper holding `result.status` reads as a pass (objectui#6092). diff --git a/.changeset/6105-tombstone-refusal-message.md b/.changeset/6105-tombstone-refusal-message.md deleted file mode 100644 index 6e21b25b79..0000000000 --- a/.changeset/6105-tombstone-refusal-message.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Static-table retirement tombstones now refuse with their remediation text -(objectui#6105). - -The nine ADR-0049 tombstones on `StaticTableColumnSchema` (`minWidth`, `align`, -`fixed`, `type`, `sortable`, `filterable`, `resizable`, `editable`, `cell`) -already refused an authored value at the right path — but the carefully written -`.describe()` string never reached the author, because `.describe()` is schema -METADATA. What an author saw was zod's own `Invalid input: expected never, -received string`: which key is wrong, nothing about why it was retired or what -to write instead. Loud refusal is the ruled outcome; half its payload was being -dropped. - -One shared mechanism carries the text into both channels. `retirementTombstone()` -(`zod/tombstone.zod.ts`) takes the guidance string ONCE and writes it to both -`z.never({ error })` — the parse-time issue message — and `.describe()` — the -generated JSON-Schema and docs surface, unchanged. One string, so the two cannot -drift. - -Authoring `align: 'right'` on a static table column now reports `RETIRED -(objectui#5474) — never read by the static table; use data-table, or a -cellClassName like text-right`. - -The accept set is untouched: same `success`, same issue `path`, same issue `code` -(`invalid_type`) for all nine, measured member-by-member before and after. Only -the message differs. diff --git a/.changeset/6108-sort-axis-convergence.md b/.changeset/6108-sort-axis-convergence.md deleted file mode 100644 index b031228521..0000000000 --- a/.changeset/6108-sort-axis-convergence.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/plugin-detail': patch ---- - -The last three sort-axis consumers read the platform's per-column sortability signal instead -of re-deriving it from the field's type (objectui#6108, inheriting objectstack#10235 ruling A -through objectui#5729's landed contract). ListView's toolbar sort picker and both of -RelatedList's sort entry points — the embedded table's column headers and the `data-list` -sort-button row — now go through `isPlatformSortableField`, the same spelling the grid header -adopted; their `UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType` re-derivations are -deleted. - -The re-derivation was not wrong about `formula`: the platform computes its own projection from -the same `@objectstack/spec` storage fact, which is why the drift went unnoticed across two -cards. It parts company on everything the projection encodes as ABSENCE — an unknown name, a -dotted path a caller can put in a related list's `columns`, an unprovisioned audit column — -where a type read finds no field definition, answers "sortable", and offers a control the -runtime meets with `400 INVALID_SORT`. It parts company again on any refusal that carries no -`reason: virtual-type`, and it cannot follow the platform in the other direction either: a -field the platform now DOES order by stays withheld forever on its type alone. - -Two behaviours are deliberately unchanged. The relational carve-out stays separate from the -signal — the projection answers `sortable: true` for a `lookup` because the platform can order -by the stored foreign key, while the UI withholds because that order means nothing beside a -column of names — so a relational column does not get its sort back. And ListView's picker -still lists a field the CURRENT sort already names, which is the only way to remove a sort the -server refuses outright; that exception now covers platform-refused fields, not just formulas. - -A deployment that served no `sortability` key at all is a different case from "nothing is -sortable": that branch keeps the type read as a compatibility floor, so behaviour on a backend -older than objectstack#10235 (or an inline/mock data source) is byte-identical to before. diff --git a/.changeset/6110-predicate-scope-unbound-evaluators.md b/.changeset/6110-predicate-scope-unbound-evaluators.md deleted file mode 100644 index 4be26dce28..0000000000 --- a/.changeset/6110-predicate-scope-unbound-evaluators.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': minor -'@object-ui/plugin-form': minor ---- - -⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on -the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read -this before upgrading if any of your form metadata gates on the session user. - -objectui#6010 bound the host predicate scope on the five authored-predicate call -sites in the components form renderer, so `current_user` (plus the ADR-0068 -`user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn` -there. Two other authored-predicate evaluators were still passing `undefined` -for that argument, so the same authored text meant two different things -depending on which surface opened the form (objectui#6110): - -- **`apps/console`'s form renderer**, on the authed internal route - `/forms/:name`. The internal route is a runtime record surface by ADR-0089 - D1's own words (*"runtime record surfaces bind `record` + `current_user`"*), - and its `visibleWhen` metadata is the same `*.view.ts` FormView the - object-view chain renders — so a role gate authored once behaved differently - depending on which route opened the form. -- **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`), - the gate that re-checks the whole declared field set at final submit because - `allowSkip` can jump past a step. Its docstring promises *"the same verdict - from all three rather than a second, divergent dialect"*, and since #6010 it - was the divergent one. - -**Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails -OPEN: a field on screen is what you get when the predicate resolves TRUE, when -the scope was never bound so the predicate faulted, *and* when the predicate is -broken. Those worlds were indistinguishable, so an app that authored a -`current_user` gate saw the field render and had no way to tell the rule was -inert. After this change the predicate is evaluated for real, and fields and -sections that have always been visible will disappear for the users the rule -excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user` -requiredWhen that has been silently not applying will now start holding submits. - -In the wizard the change is a fix in the user's favour as well: a required field -the wizard HID from this user was still counted as visible by the submit gate, -so the submit was refused on a control the submitter could neither see nor fill -in. - -**Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in -your form-view and object metadata that names `current_user`, and confirm each -predicate says what you actually want evaluated against `record` + -`current_user`. - -**The public anonymous form `/f/:slug` is deliberately unchanged.** It is -mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is -no authenticated principal, and no provider is mounted above it — so its scope -is empty and a `current_user` predicate authored on a public form still faults -and still fails open, exactly as before. Nothing new is declared to say so: the -two routes are told apart by which component mounts them. - -`@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider` -user normalisation, so every console surface that mounts the provider publishes -the same `current_user` shape rather than re-deriving it. diff --git a/.changeset/6111-formsection-visiblewhen.md b/.changeset/6111-formsection-visiblewhen.md deleted file mode 100644 index d17c7fa6e8..0000000000 --- a/.changeset/6111-formsection-visiblewhen.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-form': minor -'@object-ui/types': minor ---- - -⚠️ **Behaviour change: an authored `FormSection.visibleWhen` that has been doing nothing -will now START HIDING SECTIONS.** Read this before upgrading if any of your metadata -authors a section predicate. - -`@objectstack/spec` declares `FormSection.visibleWhen` and this repo's spec bridge maps it -through, but every plugin-form layout renders a section header as a virtual -`section-divider` pseudo-field and none of them copied the predicate onto it. On the -object-view chain — the create/edit modal, the drawer, the split form, and the full-page -record form — the key was declared, mapped, carried, and then dropped one hop before -anything could evaluate it. The section rendered unconditionally, with no diagnostic -(objectui#6111). - -**Why nobody noticed, and why the fix is felt as a regression.** `visibleWhen` fails OPEN: -a section that renders is what you get when the predicate resolves TRUE, when the predicate -never arrives, *and* when the predicate faults. Those three worlds were indistinguishable, -so an app that authored a section predicate saw its section render and had no way to tell -that the rule was inert. Every such app has been running with the rule switched off, and -some will have been authored — or simply grown used to — that state. After this change the -predicate is evaluated for real, and sections that have always been visible will disappear -for the users the rule excludes. - -This is the intended ADR-0089 contract being delivered, not a new capability: the key was -already declared, already documented, and already honoured by the console form renderer. -The object-view chain was the one that silently ignored it. - -**Before upgrading**, audit any `sections[].visibleWhen` in your form-view metadata and -confirm each predicate says what you actually want, evaluated against `record` + -`current_user`. A predicate that was written speculatively, or left behind after a rework, -now takes effect. - -**Measured scope of the hide.** The predicate gates the section's HEADER row. The renderer -treats `section-divider` as presentational and holds no association between it and the -fields that follow it, so a false predicate removes the heading and the section's fields -keep rendering. The console renderer (`apps/console`) drops the whole `
`, fields -included. That divergence is real, is pinned honestly by this change's tests rather than -implied away, and is filed separately — it needs a renderer-side grouping contract, not -another line in a layout. - -Two hops were dropping the key and both are repaired: `ObjectForm` rebuilds each section -key by key when it delegates to Split/Drawer/Modal (and `ModalForm`'s own `groups` map does -it again), so a key those maps did not copy never reached the layout at all; and the six -`section-divider` synthesis sites across the four layout files. - -`@object-ui/types` gains the matching `ObjectFormSection.visibleWhen` declaration. diff --git a/.changeset/6120-doc-snippet-dependency-resolution.md b/.changeset/6120-doc-snippet-dependency-resolution.md deleted file mode 100644 index 01f3621a10..0000000000 --- a/.changeset/6120-doc-snippet-dependency-resolution.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Doc-snippet gate tooling only, no published package source changed. - -`check-doc-snippet-types` compiles every covered snippet as its own module at the -repository ROOT. Workspace packages resolved there — it builds `paths` from each -package's own `exports` — but a THIRD-PARTY specifier did not: under pnpm, a -workspace package's own dependency is not hoisted to the root, so a snippet that -imports `lucide-react` failed `TS2307` even though `@object-ui/layout` and -`@object-ui/components` both declare it and any reader who installs those -packages gets it. Five correct blocks across `content/docs/layout` were red on -nothing but that, which blocked the whole group from being brought under the -gate. The snippets were right; the resolution environment was the gap. - -The gate now derives `paths` for the specifiers each imported package DECLARES in -its own `dependencies`, resolved from inside that package's directory — the -environment a real consumer has. Deliberately narrow, and it fails closed: -`dependencies` only (not peers, not devDependencies), only packages a covered -document actually imports, only the bare specifier (no subpath wildcard), and a -dependency shipping no types is left unresolvable rather than approximated. The -repository's own manifests are untouched — declaring `lucide-react` at the root -to buy a snippet its coverage would change what this repo claims to need in order -to satisfy a checker. - -A fourth self-control (`undeclared`) now runs on every invocation and keeps that -narrowness honest: a module importing `@floating-ui/react-dom` — installed here -as a transitive of Radix's popper, declared by no package a covered document -imports — MUST still produce `TS2307`. Widen resolution past the declarations and -that control goes green, which is the only way to notice that the gate has become -a rubber stamp no snippet can fail. diff --git a/.changeset/6121-report-authoring-face.md b/.changeset/6121-report-authoring-face.md deleted file mode 100644 index 5168b98592..0000000000 --- a/.changeset/6121-report-authoring-face.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The report authoring face declares what its own examples author (objectui#6121, -maintainer ruling 2026-08-25, Option A — fix the type producer, not the docs). - -- `ReportComponentSchema.exportConfigs` is now - `Partial>` instead of a TOTAL - `Record`. Configuring ONE export format no longer forces an author to declare - all five (`pdf`, `excel`, `csv`, `html`, `json`). The published runtime twin - was never total — `z.record(z.string(), ReportExportConfigSchema)` in - `@object-ui/types/zod` has all keys optional — so the TS declaration had been - stricter than the validator that actually judges authored JSON. This is a pure - relaxation: every literal that type-checked before still does. - -- `ChartDataSeries` gains the optional per-series family override `type` - (`'bar' | 'line' | 'area'`), with the same key added to its zod twin - `ChartDataSeriesSchema`. The renderer already reads it — - `normalizeChartSchema`'s `normalizeSeries` in `@object-ui/plugin-charts` - resolves the family as `str(raw.chartType) ?? str(raw.type)` — so `type` was - the author spelling of an override the type refused to declare. The union is - the three families that read honours, deliberately NOT the wider `ChartType`: - a wider union would advertise an override the normalizer drops in silence. - - NOT only a relaxation on the runtime side, and this is the half a consumer - needs before taking the bump. `ChartDataSeriesSchema` is a stripping - `z.object`, so a stored series carrying a non-family `type` — `type: 'pie'`, - say, copied from `@objectstack/spec`'s `ChartSeries`, whose `type` IS the - full `ChartType` — used to PARSE, with the unrecognised key dropped in - silence; it now FAILS. `ChartDataSeriesSchema` feeds `ChartSchema.series`, - so a consumer running `safeParse` over stored chart JSON newly gets - `invalid_value` at `series.N.type` where it previously got nothing. (Checked - against the package's own zod 4.4.3, both directions, with `type: 'line'` - and a series carrying no `type` as controls: both still parse.) What to do - about it: the rejected value never had an effect — `normalizeSeries` honours - exactly the three families and drops every other one with no error, no - warning and no output key — so the failure surfaces an override that was - already inert. Drop the `type` from the stored series, or, if the whole - chart really is that family, move it to the chart's own `chartType`, which - still takes the full `ChartType`. The TS side is widening-only; only the - published validator newly rejects. - -Both changes carry pins in -`packages/types/src/__tests__/report-schema-authoring-face.test.ts`: the -widenings fail if either is narrowed back, and the rejection above is pinned -openly as `rejects a family the normalizer would silently drop`. - -`ReportComponentSchema.dataSource` is NOT changed here — measuring the authorable -shape against the report runtime's actual read, which the ruling requires, -produced a fork the ruling did not cover. It is escalated on objectui#6121. diff --git a/.changeset/6121-retire-report-data-source.md b/.changeset/6121-retire-report-data-source.md deleted file mode 100644 index 18a52fabd8..0000000000 --- a/.changeset/6121-retire-report-data-source.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The two report data-source keys are retired on both faces (objectui#6121, -maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half; -ADR-0049 enforce-or-remove). - -**The accept set of a published validator moves** (`@object-ui/types/zod`): - -- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value - parsed green and was then read by nobody. -- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node - type no renderer is registered for at all. - -Both now carry `retirementTombstone(...)`: an authored value is refused at the -key's own path with `code: 'invalid_type'` and a message that names the key, says -why it is retired and points at the spelling that runs. Nothing that used to be -refused parses green. - -**The TypeScript face** — both keys become `?: never` rather than being deleted, -so an author who still writes one gets a `tsc` error at the authoring site -instead of a silently stripped key. They were annotated `DataSource` / -`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which -no JSON document can author; that mis-annotation is the defect objectui#6121 was -filed for, since every example on `content/docs/core/report-schema.mdx` authored -a config object against it. - -**Why this is a retirement and not a rename.** No read site consumed either key: -`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop -or from `SchemaRendererContext`, never off `schema.dataSource`, and the live -9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences -measured zero in this repo and in the sibling `objectstack` checkout, whose -report metadata binds `dataset` throughout — the ruling's own deprecation-window -exit criterion. A stored document that still carries the key now fails loudly at -`safeParse` instead of being accepted and ignored; drop the key, and bind the -report through `dataset`. - -The replacement binding key the ruling names (`data?: ViewData`) is deliberately -NOT declared here, and is escalated on objectui#6121: `data` is already a live -key on `ReportComponentSchema` — the report ROW array, read by -`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding -under that name would put two authoring contracts on one key inside one -renderer. - -Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`: -the `never` twins, the named refusals with their issue envelope, the `.describe()` -metadata channel, and controls that a report without the key still parses. diff --git a/.changeset/6122-action-prop-renames.md b/.changeset/6122-action-prop-renames.md deleted file mode 100644 index d99824e289..0000000000 --- a/.changeset/6122-action-prop-renames.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Docs only, publishes nothing: five `content/docs/components` reference pages -annotated an event prop as `string | ActionConfig` — a type `@object-ui/types` -has never exported. Each site was resolved individually against the freshly -built `packages/types/dist/*.d.ts`, and in every case the shipped slot is a -plain function callback that the page had also named wrongly: - -| page | documented | shipped slot | declaration | -| --- | --- | --- | --- | -| `form/command.mdx` | `onSelect` | `onChange?: (value: string) => void` | `form.d.ts:1376` | -| `form/radio-group.mdx` | `onValueChange` | `onChange?: (value: string \| number) => void` | `form.d.ts:394` | -| `form/date-picker.mdx` | `onDateChange` | `onChange?: (date: Date \| undefined) => void` | `form.d.ts:642` | -| `form/combobox.mdx` | `onValueChange` | `onChange?: (value: string) => void` | `form.d.ts:1324` | -| `feedback/toast.mdx` | `actionLabel` + `onAction` | `action?: { label: string; onClick: () => void }` | `feedback.d.ts:137` | - -The `string |` half of each annotation goes with the name, and that is the -substance rather than a tidy-up: objectui#4453 narrowed the runtime to -`typeof === 'function'`, so an authored string handler is **dropped** -(`packages/plugin-calendar/src/calendar-view-renderer.tsx`). A reference page -promising `string | Fn` is exactly what makes an AI author emit a handler that -publishes, validates, and silently does nothing. - -No type was minted to make the prose true. Measured with a throwaway re-fence -probe: `TS2304: Cannot find name 'ActionConfig'` **10 → 5** across the -`components` group, and the 5 that remain are objectui#6132's, untouched here. - -Part of objectui#6122. diff --git a/.changeset/6124-handler-keys-json-refusal.md b/.changeset/6124-handler-keys-json-refusal.md deleted file mode 100644 index 641c7a7691..0000000000 --- a/.changeset/6124-handler-keys-json-refusal.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/types": minor ---- - -`@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124) - -The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else. - -Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click. - -**Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so. - -**TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key. - -Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card. diff --git a/.changeset/6124-view-handler-keys-are-event-names.md b/.changeset/6124-view-handler-keys-are-event-names.md deleted file mode 100644 index bc5a710fdf..0000000000 --- a/.changeset/6124-view-handler-keys-are-event-names.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/types': patch ---- - -The three view handler keys are declared as EVENT NAMES, not callbacks. - -`ViewSwitcherSchema.onViewChange`, `FilterUISchema.onChange` and -`SortUISchema.onChange` were described as "change callback" on both the zod -mirror and the TS interface. They are not callbacks: the string an author -writes is the NAME of a `CustomEvent` the renderer dispatches on `window` — -`new CustomEvent(schema.onViewChange, { detail: { view } })` and its two -siblings. - -**What an author feels.** Nothing they write breaks — the type is still -`string`, so no accept set moves and no existing document changes verdict. -What changes is the two places this contract is published — the zod mirror's -`describe()` text and the TS JSDoc — which now tell them what the string is -FOR, and what to listen for: - -```json -{ "type": "sort-ui", "fields": [{ "field": "name" }], "onChange": "myapp:sort-changed" } -``` - -```js -window.addEventListener('myapp:sort-changed', (e) => e.detail.sort); -``` - -Previously "Sort change callback" invited the two readings the runtime does not -support — a function (unwritable in JSON) or a handler expression (dropped at -runtime) — with no hint that the working form is an event name. - -The correction also protects the capability. A handler-key census that buckets -by declared TYPE cannot tell an event name from the unsupported -handler-expression dialect, and on that reading these three had been swept in -for retirement, which would have deleted working behaviour. A new pin -(`plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx`) now holds -both halves — that each key is DECLARED on the authorable surface, and that the -authored string reaches `new CustomEvent(...)`. diff --git a/.changeset/6126-fields-unresolvable-imports.md b/.changeset/6126-fields-unresolvable-imports.md deleted file mode 100644 index db0de73c8a..0000000000 --- a/.changeset/6126-fields-unresolvable-imports.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Docs only, publishes nothing: two `content/docs/fields` snippets referenced -identifiers the snippet program cannot resolve, so both pages were held out of -objectui#5867's batch 3 (objectui#6126). Neither is fixed by making the compiler -happy — each was narrowed to what ObjectUI actually owns. - -`auto-number`'s Sequence Management block called `db.transaction` on a `db` -declared nowhere in the workspace and exported by nothing (TS2304, plus TS7006 -on the `tx` that fell out of it). Declaring an ambient `db` would have had the -renderer's documentation mint a backend contract it does not own, so the block -is now the metadata the backend actually reads — a literal annotated with the -exported `AutoNumberFieldMetadata`, carrying `format` and `starting_number` — -and the transactional sketch is prose: one counter per object-and-field pair, -incremented in the same transaction that inserts the record. The page now also -says the thing it never said, which is that ObjectUI never allocates a value at -all and renders a placeholder until the saved record comes back. - -`object`'s Backend Validation block opened `import Ajv from 'ajv'`, and `ajv` is -declared by no `package.json` in this repository and resolves from nowhere -(TS2307). Adding it as a dependency to satisfy a checker was refused, so the -block keeps the ObjectUI half — an `ObjectFieldMetadata` literal whose `schema` -is the JSON Schema a server validates against — and the Ajv call sequence, which -was Ajv's documentation rather than ObjectUI's, is a prose sentence naming it as -one option among any JSON Schema validator. The section now states the fact a -reader most needs: `ObjectField` checks JSON syntax only and never enforces -`schema`, so structural validation is the server's. - -Both pages join the compile population with the batch's own classifier: the four -`plaintext`-fenced blocks whose first line starts with `import` or `interface` -are now `ts`. The gate's blocks-to-compile count rises from 206 to 210 — exactly -those four — with diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and -the covered/ungated and declared-fragment sets unmoved. diff --git a/.changeset/6127-location-map-fence-split.md b/.changeset/6127-location-map-fence-split.md deleted file mode 100644 index 49578b3076..0000000000 --- a/.changeset/6127-location-map-fence-split.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Docs only, publishes nothing: `content/docs/fields/location.mdx`'s *Integration -with Maps* fence welded two different examples into one block — an import plus a -JSX element (which needs `tsx`), immediately followed by a bare metadata object -literal at statement position (which `tsx` reads as a labelled statement, then -fails on the commas). Measured standalone with the repo's own TypeScript on -`origin/main`: **2 syntactic diagnostics as `ts`, 5 as `tsx`** — it parsed under -neither fence language, which is why `location.mdx` was excluded from objectui#5867 -batch 3 by measurement (objectui#6127). The fence is now **split in two**, each half -fenced for what it actually is: a `tsx` block holding the widget example, made -self-contained so it compiles (it renders `LocationField` with a typed -`LocationFieldMetadata` and a state-held coordinate pair, instead of spreading an -undeclared `props`), and a `jsonc` block holding the `object-map` metadata node — -which is a schema-key question `check-doc-snippet-types` explicitly says it does -not answer. Connecting prose numbers the two halves so the section still reads as -one example. The page's *Field Schema* block, the other fence triage's classifier -calls code, is re-fenced `plaintext` → `ts` in the same pass; the two genuinely -prose fences (`{` and `// Valid coordinates`) are left alone. - -Accounting, stated because splitting a fence breaks the plain "blocks-to-compile -rises by exactly the batch size" identity this card family has used across three -batches: **1 block re-fenced, plus 1 fence split into 2 of which 1 half is -TypeScript, so blocks-to-compile rises by 1 + 1 = 2** — measured 206 → 208, with -diagnostics at 0, declared fragments unmoved at 111, and the covered/ungated sets -unchanged. The page's fence count rises 4 → 5. Both blocks also stop rendering as -unstyled plaintext and pick up TypeScript and JSON-with-comments highlighting, -which is reader-visible. diff --git a/.changeset/6131-sidebar-nav-navgroup-annotation.md b/.changeset/6131-sidebar-nav-navgroup-annotation.md deleted file mode 100644 index bb475295e3..0000000000 --- a/.changeset/6131-sidebar-nav-navgroup-annotation.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the "Complete Example" block in -`content/docs/layout/sidebar-nav.mdx` now annotates its grouped-navigation array as -`NavGroup[]`, importing the type in the same block (objectui#6131). - -Without the annotation the array's `badgeVariant: 'destructive'` widens to `string`, -and `SidebarNav`'s declared `badgeVariant?: 'default' | 'destructive' | 'outline'` -does not accept it — so the whole array fails to assign to -`items: NavItem[] | NavGroup[]`. TypeScript then reports the union's *other* branch, -which is why the message reads "missing the following properties from type 'NavItem': -title, href" and looks at first like a wrong data shape. The data shape was always -right; the annotation was missing. A reader who copies this block into an annotated -position, or assigns it anywhere typed under `strict`, hits the same TS2322. - -Measured on this branch against the built `dist/*.d.ts`, with the block temporarily -re-fenced as `ts` under an `EXIT` trap so it joins the compile population (the fence -itself belongs to objectui#5867's lane and is deliberately left as `plaintext` here): - -- before the annotation, `Semantic phase: 224 of 224 block(s) judged, 1 failed`, gate - exit 1, on `content/docs/layout/sidebar-nav.mdx:235:7 TS2322`; -- after it, `Semantic phase: 224 of 224 block(s) judged, 0 failed`, gate exit 0. - -Removing the annotation again reproduces the identical TS2322, so it is load-bearing -rather than decorative. Declared fragment count is unmoved at 111, no -`FRAGMENT_MARKER` is added, and the covered/ungated sets are untouched. - -The annotation is written so that it **resolves**: the type is imported in the same -block, because every block compiles in isolation. An annotation naming a type the -block cannot see errors on the annotation itself, at which point TypeScript stops -checking the literal underneath and the TS2322 disappears — the false-green shape -already recorded for `guide/theming` in `scripts/check-doc-snippet-types.mjs`. - -This clears the last `content/docs/layout` block that stays red for a reason of its -own, so the layout group is type-clean ahead of objectui#5867 re-fencing it. diff --git a/.changeset/6132-undeclared-action-props.md b/.changeset/6132-undeclared-action-props.md deleted file mode 100644 index dac7317f6c..0000000000 --- a/.changeset/6132-undeclared-action-props.md +++ /dev/null @@ -1,59 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the last five `content/docs/components` reference -pages annotating an event prop as `string | ActionConfig` are corrected. These -are the five objectui#6122 measured as having **no declared slot at all** (the -other eleven were renamed in #6130/#6142), and the maintainer ruled Option A on -2026-08-25: documentation follows the shipped types. - -Two different remedies, because the two cases are not the same defect: - -**Deleted — nothing declares them, at any level:** - -| page | documented prop | shipped type | -| --- | --- | --- | -| `feedback/sonner.mdx` | `action?: { label; onClick }` | `SonnerSchema` declares no `action` (`packages/types/src/feedback.ts:204`) | -| `basic/button-group.mdx` | `onValueChange` | `ButtonGroupSchema` declares only `type`/`buttons`/`variant`/`size` (`packages/types/src/navigation.ts:335`) | - -**Redirected — the real slot is one level down, on the item:** - -`overlay/context-menu.mdx`, `overlay/dropdown-menu.mdx` and `overlay/menubar.mdx` -documented a menu-level `onSelect`. None of the three menu schemas declares any -event slot (`DropdownMenuSchema` declares `onOpenChange` and nothing else). The -handler is declared on the **item**, and that declaration is real: - -```ts -// packages/types/src/overlay.ts:330-357 (jsdoc elided except on onClick) -// built: packages/types/dist/overlay.d.ts:334 -- the line objectui#6132 cited -export interface MenuItem { - label: string; - icon?: string; - disabled?: boolean; - /** - * Click handler - */ - onClick?: () => void; - shortcut?: string; - children?: MenuItem[]; - separator?: boolean; -} -``` - -declared at `packages/types/src/overlay.ts:346` and mirrored in Zod at -`packages/types/src/zod/overlay.zod.ts:136` -(`onClick: z.function().optional().describe('Click handler')`). `MenuItem` is the -element type of `DropdownMenuSchema.items`, `ContextMenuSchema.items` and -`MenubarMenu.items`, so all three pages redirect to the same declaration rather -than losing the capability. - -The `string |` half goes with the name in every case: objectui#4453 narrowed the -runtime to `typeof === 'function'`, so an authored string handler is dropped. A -reference page promising `string | Fn` is what makes an AI author emit a handler -that validates, publishes, and silently does nothing. - -No type was minted to make the prose true, and no fence moved: the five pages -hold 10 `plaintext` fence markers before and after, so objectui#5867's -SHRINK-ONLY declared population is unchanged. - -Part of objectui#6132 (maintainer ruling of 2026-08-25, Option A). diff --git a/.changeset/6138-fields-schema-block-parity-pr1.md b/.changeset/6138-fields-schema-block-parity-pr1.md deleted file mode 100644 index ac0d7216fe..0000000000 --- a/.changeset/6138-fields-schema-block-parity-pr1.md +++ /dev/null @@ -1,50 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the `Field Schema` block on every -`content/docs/fields` page declared an interface of its own, so -`check-doc-snippet-types` compiled it **vacuously** — a self-declared interface -with no imports type-checks no matter what it says, because nothing in the block -refers to anything the packages export (objectui#6138). The gate reported those -pages green and structurally could not see whether the documented shape matched -the shipped one. This is batch 1 of 2: the shared page plus four converted -pages, proving the mechanism end to end before it is applied to the rest. - -Each converted `Field Schema` block is now a literal **annotated** with that -field type's exported `*FieldMetadata`, so the sealed type's excess-property -check judges every documented key. The page becomes structurally incapable of -teaching a key the type does not have. - -Measured before converting: all 26 pages diverged from their exported type, and -on 24 of them the divergence was **entirely** `FieldWidgetComponentProps` -members — a real, exported, reader-facing surface filed under a heading that -says "Field Schema". Deleting them would have deleted correct API, so the shapes -are separated instead of one being dropped: a new `content/docs/fields/widget-props.mdx` -documents `FieldWidgetComponentProps` once, with a gate-compiled example and the -type named as the source of truth, and the field pages link to it. That page -carries no hand-maintained key list — the type has 76 members and a prose -restatement of a declared surface is the defect class objectui#6086 is open for. - -Three documentation defects the conversion forced out into the open, each a page -teaching something no shipped type declares: - -- `date.mdx` documented the range bounds as `min` / `max`; `DateFieldMetadata` - declares `min_date` / `max_date`, and the sibling `datetime.mdx` already - documented that spelling. Two adjacent reference pages taught two spellings of - one concept and one of them did not exist. The docs are corrected; the type is - not touched. -- `textarea.mdx` named `TextAreaFieldMetadata`; the export is - `TextareaFieldMetadata` (lowercase `a`). A name that must resolve, so it now - does. -- `rich-text.mdx` named `RichTextFieldMetadata`, which does not exist at all. The - page's own block says `type: 'markdown' | 'html'`, so it resolves against the - existing `MarkdownFieldMetadata` / `HtmlFieldMetadata` pair rather than a - minted type. It also documented `toolbar`, `preview`, `minHeight` and - `maxHeight`, which `RichTextField` reads nowhere (`minHeight` / `maxHeight` - have zero occurrences in `packages/fields/src`), and `rows`, which it does read - through an `as any` while neither metadata type declares it — filed as - objectui#6140 with that measurement in it. - -The gate's blocks-to-compile count rises from 225 to 227 — the new page's two -blocks, the conversions being one-block-for-one-block — with diagnostics at 0, no -new `FRAGMENT_MARKER` declarations, and the declared-fragment count unmoved at 111. diff --git a/.changeset/6138-fields-schema-block-parity-pr2.md b/.changeset/6138-fields-schema-block-parity-pr2.md deleted file mode 100644 index 2bcd93ad29..0000000000 --- a/.changeset/6138-fields-schema-block-parity-pr2.md +++ /dev/null @@ -1,49 +0,0 @@ ---- ---- - -Docs only, publishes nothing: batch 2 of 2 for objectui#6138 — the remaining -**22** `content/docs/fields` pages have their `Field Schema` block converted from -a self-declared interface to a literal **annotated** against that field type's -exported `*FieldMetadata`, each linking the shared -`content/docs/fields/widget-props.mdx` page batch 1 added. - -A self-declared interface with no imports type-checks no matter what it says, so -`check-doc-snippet-types` reported these pages green while being structurally -unable to see whether the documented shape matched the shipped one. An annotated -literal is judged by the compiler instead: the page becomes incapable of teaching -a key the type does not have. - -That property depends on the annotated types being **sealed**, so it was measured -rather than assumed. All 26 types this batch annotates against — the 22 field -metadata types plus `SelectOptionMetadata`, `GridColumnDefinition`, -`UploadedFileMetadata` and `LookupColumnDef` — carry no index signature, and a -nonsense key on each produces `TS2353`. The same control run against the -components lane's `BaseSchema` / `ButtonSchema` produces **zero** diagnostics, -which is what an open type does and why that lane cannot use this mechanism -(objectui#6143). - -Documentation defects the conversion forced out, each a page teaching something -no shipped type declares: - -- `grid.mdx` documented a per-column `editable` flag and a string `width`. - `GridColumnDefinition` declares neither — `width` is a number of pixels — and - no widget reads a column-level `editable`. Both are corrected. -- `formula.mdx` documented `return_type` as taking `currency`; the shipped union - is `'text' | 'number' | 'boolean' | 'date' | 'datetime'`. -- `lookup.mdx` documented option keys `_id` and `name`; the widget matches - options by `value` and labels them by `label`, and those two keys are read off - **records**, not options. -- `user.mdx` documented the value as a user object; the field stores the user's - id and the picker resolves the rest from `sys_user`. - -Two undeclared-but-consumed keys were found by checking each divergence against -its renderer, and are filed rather than deleted or documented as metadata: -`dependsOn` on select and `description` on a lookup's static options -(objectui#6153, the same class as objectui#6140). The location field's stored -`{ latitude, longitude }` value shape is declared by no exported type at all -(objectui#6154), so that page describes it in prose and points at the card. - -The gate's blocks-to-compile count rises from 248 to 249 — 21 conversions are -one-block-for-one-block and `lookup.mdx` becomes two blocks (data-source-backed -and static-option) — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the declared-fragment count unmoved at 111. diff --git a/.changeset/6140-field-metadata-rows-option-description.md b/.changeset/6140-field-metadata-rows-option-description.md deleted file mode 100644 index 9df2d42596..0000000000 --- a/.changeset/6140-field-metadata-rows-option-description.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/fields': patch ---- - -Declare the two consumed-but-undeclared field-metadata keys ruled on -objectui#6140 / objectui#6153 (maintainer 2026-08-25, Option A), and de-cast -the widget reads they legalize: - -- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` (`@object-ui/types`) - — the inline-editor height `RichTextField` has always read through an - `as any` (default 8), following the `TextareaFieldMetadata` precedent. NOT a - spec key: `@objectstack/spec` `FieldSchema` refuses `rows` BY NAME - (`unrecognized_keys`) on all four of textarea/markdown/html/richtext, so it is - an objectui render hint that must not be written into authored object - metadata. The four inert editor keys (`toolbar`/`preview`/`minHeight`/ - `maxHeight`) stay deliberately undeclared and are pinned so. -- `SelectOptionMetadata.description` — secondary option text `LookupField` - searches on authored static options and emits from `recordToOption`. NOT a - spec key either: `@objectstack/spec` `SelectOptionSchema` is strict over - exactly `{label, value, color, default, visibleWhen}` and refuses - `description` BY NAME, and `FieldSchema` routes `options` through that schema, - so the key must never reach authored object metadata. -- `RichTextField` and `TextAreaField` (`@object-ui/fields`) now read their - metadata through the declared types instead of `field as any` (the spec-face - `maxLength` dual-read in `TextAreaField` stays as a documented structural - read). Behaviour unchanged; `rows` and option `description` are now legal to - author in an objectui **annotated literal** — never in an object document sent - to the platform. - -Both spec attributions above were corrected in place before release -(objectui#7537): as first written this changeset claimed each key was "aligned -with" a `@objectstack/spec` schema member that does not exist. Re-measured on -`@objectstack/spec@17.2.0`, each refusal is paired with a control that accepts -the same payload minus the key. Same correction as objectui#7014 / PR #7510 made -to the published JSDoc; the package bumps and the declared behaviour are -unchanged. diff --git a/.changeset/6143-components-schema-corrections.md b/.changeset/6143-components-schema-corrections.md deleted file mode 100644 index c23ec20ba2..0000000000 --- a/.changeset/6143-components-schema-corrections.md +++ /dev/null @@ -1,38 +0,0 @@ ---- ---- - -Docs only, publishes nothing: four `content/docs/components` reference pages -taught props the shipped types do not have. Each correction was resolved -against a freshly built `packages/types/dist/*.d.ts` **and** against the zod -schemas that are the ruled enforcement boundary, so both layers agree: - -| page | documented | shipped | declaration | -| --- | --- | --- | --- | -| `feedback/toast.mdx` | `variant?: 'default' \| 'destructive'` | `variant?: 'default' \| 'success' \| 'warning' \| 'error' \| 'info'` | `feedback.d.ts:123`, `zod/feedback.zod.js:59` | -| `form/radio-group.mdx` | `direction` | `orientation?: 'horizontal' \| 'vertical'` | `form.d.ts:377`, `zod/form.zod.js:263` | -| `form/combobox.mdx` | `searchPlaceholder`, `emptyText` | neither is declared | `form.d.ts:1283`, `zod/form.zod.js:378` | -| `form/command.mdx` | `CommandItem.shortcut` | `CommandItem` declares only `value`, `label`, `icon` | `form.d.ts:1329`, `zod/form.zod.js:76` | - -`toast.mdx` is the one that mattered most: `'destructive'` is not a member of -the union, it is the Shadcn vocabulary a reader arrives with, and it is the -value most likely to be copied verbatim into an authored schema. - -Each removal was checked against the renderer before it was made, because a -key a renderer genuinely reads is an undeclared capability rather than a doc -error. None of these four is read: `renderers/form/combobox.tsx` contains -neither `searchPlaceholder` nor `emptyText`, `renderers/form/command.tsx` -contains no `shortcut`, and `renderers/feedback/toast.tsx` contains no -`'destructive'`. The same sweep's genuinely-consumed keys went to -objectui#6150 instead of being edited away. - -`emptyText` is removed from `ComboboxSchema` only. `CommandSchema` really does -declare it (`form.d.ts:1368`), so `command.mdx` keeps it. - -No type, export or union member was minted, nothing was re-fenced, and no -block changed shape — these blocks stay `plaintext` and stay outside the -compile population. `check:doc-snippets` reports the same numbers before and -after: 248 blocks to compile, 111 declared fragments, 178 covered / 44 -ungated. The import mechanism this card originally proposed is sequenced -behind objectui#5155 and is deliberately not attempted here. - -Part of objectui#6143. diff --git a/.changeset/6150-undeclared-but-consumed-keys.md b/.changeset/6150-undeclared-but-consumed-keys.md deleted file mode 100644 index 3f5b0bf80a..0000000000 --- a/.changeset/6150-undeclared-but-consumed-keys.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Declare the 13 renderer-read keys that no shipped type declared (objectui#6150) - -**This is a published-surface change on `@object-ui/types` and its `zod` mirrors, -and it moves the accept set in TWO directions.** Read the next two paragraphs -before reading the list — they are what the change actually is. - -**Key membership is NOT widened — it was never narrow.** All eight touched mirrors -extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy -through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight). -So before this change every one of the 13 keys already parsed green and already -SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing -that parsed before stops parsing because a key became known. - -**Value enforcement IS widened, which in the value dimension is a NARROWING.** For -the 12 keys that gained a zod mirror entry, the value is now validated against the -declared type: `{ type: 'text', content: 42 }` parsed green before and is refused -now, at `content`. That is the point of declaring them — `declared === enforced` — -but it is a behaviour change for documents that carried a wrong-typed value under -one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any -type is still admitted unexamined on all eight mirrors, pinned per mirror. - -The 13, each with the renderer read site the declaration records: - -| type | key | declared as | read at | -|---|---|---|---| -| `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` | -| `CarouselSchema` | `opts` | `Record` | `complex/carousel.tsx` — `opts={schema.opts}` | -| `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` | -| `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class | -| `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` | -| `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` | -| `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` | -| `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED | -| `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker | -| `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` | -| `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` | -| `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` | -| `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` | - -These compiled before only because `BaseSchema` ends with `[key: string]: any` -(objectui#5155), so the docs page was the single place in the repo recording each -capability, and the one place with no mechanical guard. - -Three declarations are deliberately not what "declare what is read" would produce -on its own, and each says so in its own doc comment: - -- `CarouselSchema.opts` stays an OPEN bag rather than the docs page's - `{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so - narrowing it to two keys would refuse authored documents that work today. -- `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it - required; the renderer substitutes a placeholder, so trigger-less documents are - legal today. -- `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a - value, so it cannot appear in an authored JSON document; objectui#6152 ruled - that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` - instead, and it is (the first pair to sit there without also sitting in - `UnmirroredDeclared`, so that file's two counts move with it). - -Two of the 13 declare a SECOND spelling for a slot that already had one — -`TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because -that is what the renderers read. Retiring either spelling is an ADR-0049 -enforce-or-remove question and is deliberately not decided here. Declaring `nodes` -also does not by itself make a `nodes`-only tree-view document legal: `data` stays -required on both faces. diff --git a/.changeset/6151-stack-schema-omit-collapse.md b/.changeset/6151-stack-schema-omit-collapse.md deleted file mode 100644 index 92f2ab2c8a..0000000000 --- a/.changeset/6151-stack-schema-omit-collapse.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`StackSchema` now SHIPS the members it declares (objectui#6151). Its emitted declaration -carried one property — `type` — where it was meant to carry twenty-five. - -The interface was written `extends Omit`: "everything `FlexSchema` has, -with a different `type`". That spelling erases every named member. `Omit` of a type over a -key set is `Pick` over `Exclude` of `keyof` that type, and `keyof` a type carrying a string -index signature is `string | number` — the literal member names are absorbed. `FlexSchema` -inherits `BaseSchema`'s `[key: string]: any` (objectui#5155), so excluding `'type'` from -`string | number` still leaves `string | number`, and the `Pick` rebuilt a type holding the -index signature and none of the named members. Measured against the built `dist`: -`FlexSchema` declared 25 properties, `StackSchema` declared 1. - -Nothing errored, which is why it survived four releases: the index signature keeps every -absent key assignable and readable as `any`. The cost fell entirely on the tools that READ -the declaration. Editor completion on a `stack` node offered `type` and nothing else — no -`gap`, no `align`, no `justify`, no `children`. And a docs-versus-type sweep read -`stack.mdx` as documenting keys that do not exist: objectui#6143 flagged `gap`, `children` -and `className` there as divergences when the docs were right and the type was wrong. - -Fixed at the mechanism rather than by restating the members. The six flex/stack members now -live in a new exported interface, `FlexLayoutProps`, which does NOT inherit `BaseSchema`, -and `FlexSchema` and `StackSchema` each extend `BaseSchema` and `FlexLayoutProps`. No -`Omit` crosses the index signature any more, and the members are declared once rather than -duplicated. Extending `FlexSchema` directly was measured unavailable: an interface may -narrow an inherited property only to a subtype, and `'stack'` is not a subtype of -`FlexSchema`'s `type: 'flex'` (TS2430). - -`FlexSchema` is unchanged — its six member declarations moved byte-identically, and its -emitted member set is the same 25 names before and after. The only declaration whose shape -changes is `StackSchema`, which goes from 1 property to the same 25. - -**The one way this can newly error**, and why it ships as `minor`: keys on a `stack` node -were previously answered by the index signature as `any`, so `gap: 'large'` type-checked. -`gap` is now `number | undefined` and that line is a `tsc` error. Every value this newly -rejects is one the renderer never honoured — `stack.tsx` feeds `gap` to a Tailwind numeric -scale — so the change reports a defect that was already there rather than removing a -capability. All three in-repo packages that name `StackSchema` or `FlexSchema` -(`@object-ui/components`, `@object-ui/core`, the schema-catalog example) type-check green -unchanged. - -Guarded by `packages/types/src/__tests__/stack-schema-emitted-members.test.ts`, which -asserts against the EMITTED declaration rather than the source. That distinction is the -whole point: a source-level assertion passes on the broken code, because the index -signature answers for the missing key with `any`. The guard emits declarations with the -package's own tsconfig and asserts (1) `StackSchema` declares exactly what `FlexSchema` -declares, and (2) no member of the `LayoutSchema` union has lost any of `BaseSchema`'s -named members — so the next heritage clause that collapses under the index signature reds -for the whole class, not just for this one interface. diff --git a/.changeset/6158-radio-group-orientation.md b/.changeset/6158-radio-group-orientation.md deleted file mode 100644 index 51472aa062..0000000000 --- a/.changeset/6158-radio-group-orientation.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`radio-group` now renders the `orientation` its own type has always declared (objectui#6158). - -`RadioGroupSchema.orientation` was declared in two layers and read by none. The shipped TS -type carries `orientation?: 'horizontal' | 'vertical'` with `@default 'vertical'` -(`packages/types/src/form.ts:383`) and the zod mirror carries the matching -`z.enum(['horizontal', 'vertical'])` (`packages/types/src/zod/form.zod.ts:282`), while -`packages/components/src/renderers/form/radio-group.tsx` contained neither the string -`orientation` nor `direction` and forwarded only `defaultValue`, `className`, the -form-control DOM whitelist and the designer props. - -The consequence was measurable rather than cosmetic: every radiogroup root the library -rendered came back byte-identical on that axis — no `data-orientation`, no -`aria-orientation` — so the docs page's `## Layout Options` section demonstrated a -distinction the product could not make, and its horizontal demo rendered vertically. An -author reading the shipped type had every reason to write `orientation: 'horizontal'` and -no way to discover it was inert. - -The key is now forwarded to the underlying Radix `RadioGroup`, which accepts it natively -with the same two-value vocabulary and puts it on the root as `aria-orientation` and -`data-orientation`; the layout utilities follow it so the visible difference the docs -promise is real. This restores declared = enforced **without widening the acceptance -set** — no new key is accepted, and no spelling outside the declared enum becomes legal. - -Two behaviour notes for anyone already shipping radio groups: - -- The declared `@default 'vertical'` is now actually applied instead of being left to - Radix's own `undefined`. A group that never authored the key keeps the vertical stack it - already rendered, and additionally announces `aria-orientation="vertical"` — the - announced orientation now agrees with the rendered one rather than being absent. Arrow - key roving focus narrows to Up/Down for those groups, which is the correct pairing for a - vertical stack. -- Author `className` still wins: the orientation layout utilities compose first and the - authored class last, so tailwind-merge resolves every conflict in the author's favour. - -Registry meta `inputs` for `radio-group` gains `orientation` in the same change — it was -the third surface that omitted the key, and leaving it out would have kept the designer -palette disagreeing with the type. diff --git a/.changeset/6165-home-item-type-label-parity.md b/.changeset/6165-home-item-type-label-parity.md deleted file mode 100644 index ea059e0160..0000000000 --- a/.changeset/6165-home-item-type-label-parity.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Home renders one agreed label for an item kind that has no translation key. - -The rail (`HomeContinue`), `RecentApps` and `StarredApps` all resolve the same -`home.recentApps.itemType.*` label, and each spelled the lookup itself. They had -drifted: the rail fell back to the bare kind (`report`) where both card surfaces -fell back to the capitalised one (`Report`) — two spellings of the same word on -one screen. All three now resolve through a single `recentItemTypeLabel` helper, -so the fallback cannot drift apart again. - -User-visible: the rail's label for an unkeyed kind changes from `report` to -`Report`. Every kind shipping today carries a key, so no label changes for them; -this is about the next kind added, and any host passing a kind the locales do -not carry. diff --git a/.changeset/6166-zh-breadcrumb-reports.md b/.changeset/6166-zh-breadcrumb-reports.md deleted file mode 100644 index 99da771680..0000000000 --- a/.changeset/6166-zh-breadcrumb-reports.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The zh pack's `console.breadcrumb.reports` renders 报表, the noun the rest of the pack -already uses for the report feature (objectui#6166). - -**The authority for this edit is the maintainer ruling of 2026-08-25, not an occurrence -count.** The card was filed explicitly as a native-speaker call and explicitly forbade -resolving it by normalising to the majority: 报表 (a tabular/data report) and 报告 (a -written/narrative report) are not interchangeable, so a pack that spells one key -differently from its siblings is evidence of a majority and never, on its own, evidence of -a mistake. The maintainer ruled that no deliberate narrative-report distinction was -intended here and that the breadcrumb names the same report feature the rest of the pack -calls 报表. - -The render context corroborates the ruling and closes the confidence gap triage recorded -when it declined to decide this itself. `console.breadcrumb.reports` labels the -`routeType === 'report'` **list** route in app-shell's `AppHeader` — a structural sibling -of the `dashboards`, `pages` and `system` segments beside it — and drilling through it -appends a metadata report definition, the same feature named by -`console.commandPalette.reports`, `console.nav.navReport`, `search.typeReports`, -`search.badgeReport` and `search.reportNotFound`. Nothing narrative renders beneath it. - -A comment at the key records that this was **ruled** rather than counted, and states the -报表/报告 distinction it was ruled against, so the next reader measuring pack consistency -neither re-files it nor quietly restores 报告 after reading the render context and -disagreeing. That comment is half of the deliverable; the value change alone would leave -the decision unrecorded, which is the failure mode the card was most concerned about. - -**Scope: one key, one pack.** This is not a licence to normalise vocabulary across the ten -packs — the card names that hazard explicitly and says it would need its own ruling, and -this ruling grants nothing beyond the single key it names. No `en` value changes, so no -other pack is asked to follow, and no other pack was touched. diff --git a/.changeset/6167-plugin-form-catalog-entries.md b/.changeset/6167-plugin-form-catalog-entries.md deleted file mode 100644 index 0a31d99a68..0000000000 --- a/.changeset/6167-plugin-form-catalog-entries.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Docs and catalog fixtures only, no published package source touched: the two `plugin-form` -catalog entries are now real `object-form` nodes instead of hand-built `form` schemas. -`content/docs/plugins/plugin-form.mdx` mounted `basic-form` and `contact-form` under -`PluginLoader plugins={['form']}` while both authored a root `type: "form"` with `input` / -`select` / `checkbox` / `textarea` fields — every one of those registered by -`@object-ui/components`, none of them by `@object-ui/plugin-form`, whose own keys are -`object-form`, `embeddable-form`, `form-analytics`, `object-master-detail-form`, -`record:line_items` and `view:form`. They are replaced by `object-form-record` (one `users` -record, fields derived from the object's metadata, `fields` / `columns` shaping the grid) -and `object-form-tabbed-sections` (the same record with `formType: 'tabbed'`, so the -declared `sections` become tab panels of one form). - -The two hand-built forms are legitimate `@object-ui/components` examples filed under the -wrong plugin, so they are re-seated into `components-form-form` as `basic-form` and -`demo-request-form` rather than deleted — deleting a catalog entry moves corpus-wide -counters (`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx` and in -`form-control-dom-leak-5632.test.tsx`, and the `className`-carrying layout node and `stack` -node floors in `layout-props-conversion.test.tsx`), and a counter that moves because a -fixture was deleted is indistinguishable later from one that moved because coverage -regressed. All of them are unchanged, with no floor edited. - -`apps/site/app/components/registerCatalogBlocks.ts` now declares `@object-ui/plugin-form`, -and its header no longer claims the import list is "exactly the packages that census -resolves to". The two `OWN_PLUGIN_DEBT` lines in `catalog-gallery-render.test.tsx` are -deleted, so both entries are held to the pin like every other one, and the pin's -provenance instrument now reads the fixture record out of a form control's value as well as -out of the tile's text — a form puts its record in `input.value`, where `textContent` -cannot see it. diff --git a/.changeset/6169-chatbot-authoring-face-type.md b/.changeset/6169-chatbot-authoring-face-type.md deleted file mode 100644 index ad7987201b..0000000000 --- a/.changeset/6169-chatbot-authoring-face-type.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-chatbot': minor ---- - -`ChatbotSchema` names the `chatbot` node's local-display and legacy -auto-response keys — a new, additive published surface (objectui#6169, the -#6172 family ruling: every component node has exactly one named, importable -authoring-face type). - -`ChatbotSchema` (`@object-ui/types`) now declares ten keys that previously -existed ONLY inside an anonymous inline intersection local to -`packages/plugin-chatbot/src/renderer.tsx`'s `chatbot` registration, invisible -to anything outside that one file: - -- `showTimestamp`, `userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, - `assistantAvatarFallback`, `maxHeight` — display fields. -- `autoResponse`, `autoResponseText`, `autoResponseDelay` — the local - auto-response (demo/playground) fields, already live via a real consumer - (`packages/app-shell/src/console/ai/AiChatPage.tsx`). -- `onSend?: (content: string, messages: ChatMessage[]) => void` — the - send-callback, now typed against the published `ChatMessage` shape rather - than the plugin's internal runtime message type. - -Each was read-site-censused before being declared (renderer.tsx and/or -`useObjectChat.ts` reads every one); none were dead, so none took the -ADR-0049 retirement route. `disabled` — also present in the original -intersection — is NOT redeclared: it is already `BaseSchema.disabled` -(`boolean | string`), read generically for every node type, and redeclaring -it here would have narrowed away the inherited expression-string case. - -**What an external consumer can now do that they could not before:** import -`ChatbotSchema` from `@object-ui/types` and get these ten keys with real, -checked types — previously any reference to them required either duplicating -the anonymous type by hand or falling back to `any`. The Zod mirror -(`@object-ui/types/zod`) gained the same ten keys in lockstep, so a `chatbot` -node parsed through it is now validated on these keys rather than silently -passed through unchecked (`BaseSchema`'s Zod mirror is `.passthrough()`). - -`packages/plugin-chatbot`'s `chatbot` registration (`renderer.tsx`) now types -its `schema` prop as `ChatbotSchema` directly, dropping the anonymous -intersection. No behavior change: `renderer.tsx:87`'s -`body: schema.requestBody` forwarding — the subject of the already-merged -#6193 — is untouched, and the render function reads the exact same keys it -already read. - -This is additive (new optional keys on an interface that already carried a -`[key: string]: any` index signature, and a new Zod-validated subset of -previously-passthrough keys), so it ships as `minor` even though it changes -published type surface: objectui's major is pinned to `@objectstack`'s -(`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes -ship as `minor` with the break spelled out — there is no break here to spell -out, only a widening from anonymous-and-unchecked to named-and-validated. - -Out of scope, deliberately: the `chatbot-enhanced` and `chatbot-floating` -registrations' own anonymous intersections (different key sets, a decision -for a separate card in the same family), and the `surface` row on -`content/docs/plugins/plugin-chatbot.mdx`'s Properties table, which names a -key no registration in this package currently reads (filed separately). diff --git a/.changeset/6170-timeline-schema-declared-keys.md b/.changeset/6170-timeline-schema-declared-keys.md deleted file mode 100644 index da89bd6eca..0000000000 --- a/.changeset/6170-timeline-schema-declared-keys.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-timeline': minor -'@object-ui/types': minor ---- - -`TimelineSchema` now declares the presentational keys the timeline renderer actually reads -(objectui#6170, maintainer ruling 2026-08-25 — the same family rule adopted on -objectui#6172: the exported type aligns to the measured authored + read set). - -Before this, `TimelineSchema` declared `events` (required), `orientation` and `position`, -and nothing else. `TimelineRenderer` is annotated `schema: TimelineSchema` and reads nine -keys off that node — `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`, -`rowLabel`, `scale`, `timeScale` — and **none** of the three that were declared. The docs -property table and the registration's own designer `inputs` had agreed with the renderer -all along; only the exported type disagreed. It was invisible to `tsc` because `BaseSchema` -carries `[key: string]: any`, so every undeclared key resolved as `any` and the annotation -constrained nothing. - -The most visible casualty was the docs page's own TypeScript example, which did not -compile: `Property 'events' is missing in type '{ type: "timeline"; variant: string; items: -… }' but required in type 'TimelineSchema'`. The page taught an authoring form its own -published type refused. - -**Declared now** (TS interface and the `@object-ui/types/zod` mirror together): `variant`, -`items`, `dateFormat`, `scale`, `timeScale`, `rowLabel`, `minDate`, `maxDate`. `onItemClick` -is deliberately left undeclared — it is a runtime slot `ObjectTimeline` installs, and this -package keeps callback-shaped keys off the authored surface. - -**`scale` is the canonical axis key.** It is `@objectstack/spec`'s `ui/TimelineConfig.json` -spelling and the one `resolveTimelineScale` reads first (`scale ?? timeScale`). The designer -now offers it, with all six buckets: `hour` / `quarter` / `year` have rendered correctly -since objectui#2942 but were offered by neither the designer (which listed three) nor the -exported type (which listed none), so they were authorable and undiscoverable. `timeScale` -stays as a deprecated alias so stored JSON keeps working; retiring it is routed separately. - -**`events` is now optional.** It was required, which is why the documented authoring form -did not type-check. That widening is the only non-additive change here — strictly more -programs compile and strictly more input parses than before. `events`, `orientation` and -`position` remain declared and remain read by nothing; a timeline authored with `events` -still renders an empty rail. Their removal is a breaking narrowing of a published type and -is routed through ADR-0049 enforce-or-remove as its own change, not smuggled into this one. - -Accept-set note for consumers: keys that previously resolved as `any` are now typed, so a -value the renderer never implemented — `variant: 'diagonal'`, `dateFormat: 'medieval'`, -`scale: 'fortnight'` — is a type error and a Zod rejection where it used to pass silently. -Nothing that renders today stops rendering. `BaseSchema`'s index signature is untouched, so -an undeclared key is still accepted by both halves (objectui#5155 / objectui#6269 own that -ceiling). diff --git a/.changeset/6172-kanban-trio-rename.md b/.changeset/6172-kanban-trio-rename.md deleted file mode 100644 index 6d08f05e39..0000000000 --- a/.changeset/6172-kanban-trio-rename.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/types': minor ---- - -One authority for `KanbanSchema` / `KanbanColumn` / `KanbanCard`: the bare names -now belong to `@object-ui/plugin-kanban` (objectui#6172, closing the -cross-package half of objectui#6155). - -**Breaking, deliberately — six published names are removed from -`@object-ui/types`.** Per this repo's own rule a breaking change ships `minor`, -with the break spelled out here. - -| removed from | old name | new name | -| --- | --- | --- | -| `@object-ui/types` | `KanbanSchema` | `DeclarativeKanbanSchema` | -| `@object-ui/types` | `KanbanColumn` | `DeclarativeKanbanColumn` | -| `@object-ui/types` | `KanbanCard` | `DeclarativeKanbanCard` | -| `@object-ui/types/zod` | `KanbanSchema` | `DeclarativeKanbanSchema` | -| `@object-ui/types/zod` | `KanbanColumnSchema` | `DeclarativeKanbanColumnSchema` | -| `@object-ui/types/zod` | `KanbanCardSchema` | `DeclarativeKanbanCardSchema` | - -Nothing else moved: every member, every optionality and the Zod mirror's whole -accept/reject behaviour are byte-for-byte the shape they were. `SchemaRegistry` -still maps `'kanban'`, `ComplexSchema` still carries the arm, and -`safeValidateSchema` accepts and refuses exactly what it did before. - -**Migration.** `import type { KanbanSchema } from '@object-ui/types'` becomes -either of two things, and which one you want is the whole point of the rename: - -- authoring a board that a **registered renderer** will draw — import the bare - name from `@object-ui/plugin-kanban`, which is unchanged; -- annotating or validating the **declarative** shape `@object-ui/types` mirrors - in Zod — import `DeclarativeKanbanSchema` (or the Zod - `DeclarativeKanbanSchema` from `@object-ui/types/zod`). - -**Why this direction.** The two declarations were structurally unrelated -dialects sharing three names, and `@object-ui/types` is the declared -zero-workspace-dependency bottom layer, so it cannot re-point at a plugin — -convergence had to remove a name from one side or the other. All four -registered kanban renderers (`kanban`, `kanban-ui`, `kanban-enhanced`, -`object-kanban`) consume the plugin's dialect and none consumes this one, and -objectui#6086 measured what happens when the bare name is the copy no renderer -reads: an IDE or agent auto-import silently authors a board that renders -nothing — a confident empty board rather than an abstention. So the surviving -bare name is the one a renderer honours. diff --git a/.changeset/6172-markdown-kanban-one-authority.md b/.changeset/6172-markdown-kanban-one-authority.md deleted file mode 100644 index cbb6e6a6dd..0000000000 --- a/.changeset/6172-markdown-kanban-one-authority.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/plugin-markdown': minor -'@object-ui/plugin-kanban': minor ---- - -One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn` -inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155). - -The 2026-08-25 family ruling: every exported schema name has exactly one -authority. Two of this card's names are discharged here. - -**`MarkdownSchema` — converged onto `@object-ui/types`.** -`@object-ui/plugin-markdown` declared a second copy of the name. The two -differed on exactly one member — `content`, required in `@object-ui/types` and -optional in the plugin — and that was measured to be drift rather than a real -semantic difference: the plugin's own registration declares the `content` input -`required: true` (pinned by its own test), `MarkdownImplProps.content` is a -non-optional `string`, the Zod mirror spells `z.string()`, and every authored -`type: 'markdown'` node in the repository supplies `content`. The plugin now -re-exports the one authority. - -⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown` -consumers**: `MarkdownSchema['content']` goes from optional to **required**. A -value annotated `MarkdownSchema` that omitted `content` no longer type-checks. -Measured against this repository: zero authored markdown nodes omit it, so -nothing in-tree changed. (`type: 'markdown'` literals that carry no `content` -are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.) -The plugin's face also gains the optional `sanitize` and `components` members -the canonical declaration carries; both are additive, and neither is read by -this renderer, which sanitizes unconditionally. - -`className` is unaffected — it comes from `BaseSchema`, which both copies -extended, so it was always inherited rather than added by the plugin. - -**`KanbanCard` / `KanbanColumn` — the three in-package copies converged to -one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A -TypeScript-AST comparison found them strict-SUBSET copies of `./types` with -nothing typed differently, so their extra members moved onto the one -declaration and both files now re-point at it. - -Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and -`coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every -value that type-checked before still does. Both modules keep their previous -export surface via re-export, so no import path changes. - -The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision -between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here -and is escalated on objectui#6172 — those are two different dialects (`items` -vs `cards`, `labels` vs `badges`), and collapsing them renames a published -name, which needs an authority ruling. diff --git a/.changeset/6175-column-state-persistence.md b/.changeset/6175-column-state-persistence.md deleted file mode 100644 index ae287843cf..0000000000 --- a/.changeset/6175-column-state-persistence.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/plugin-grid': patch ---- - -Column width and order that a user drags in `ObjectGrid` now actually persist -(objectui#6175). Both halves of `saveColumnState`'s only two call sites were dead, so a -drag was written nowhere — not to `localStorage`, not through `onColumnStateChange` to the -host's `dataSource.updateViewConfig`. The saved state was read back correctly forever; it -was simply never written. - -Two independent breaks, one per package: - -- **`@object-ui/components`** — `DataTableSchema` has declared - `onColumnResize?: (columnKey, width) => void` all along, and `data-table.tsx` invoked it - **nowhere**: the resize drag updated the table's local `columnWidths` state and stopped - there. It now reports the settled width once, at `mouseup`. Once, deliberately — the host - turns this callback into a write to shared view config, so a per-`mousemove` callback - would be a write storm. -- **`@object-ui/plugin-grid`** — `ObjectGrid` emitted `onColumnReorder` (singular) while the - renderer invokes the near-duplicate `onColumnsReorder` (with the `s`), a different declared - key with a different signature. The producer now emits the spelling the renderer actually - invokes, mapping the reported `TableColumn[]` to the `accessorKey` order `columnState` - stores. - -**Nothing is retired.** Both spellings remain declared on `DataTableSchema`; -`onColumnReorder` stays declared and stays unwired, exactly as the `RuntimeOnlyDeclared` -ledger in `zod-mirror-parity.test.ts` records it. Which of the two survives is a -declared-surface ruling that stays open and is deliberately not settled here. - -⚠️ Behavioural note for hosts: `onColumnStateChange` now fires where it previously never -did, which means `dataSource.updateViewConfig` is now reached on a column drag. That call -was unreachable by this path before, so any permission gate on that write now sees traffic -it never saw. - -The renderer's resize/reorder gestures, the inbound seeding of `columnState`, and the -declared surface are all unchanged. diff --git a/.changeset/6178-detail-section-header-color.md b/.changeset/6178-detail-section-header-color.md deleted file mode 100644 index 9a1a18887d..0000000000 --- a/.changeset/6178-detail-section-header-color.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`DetailSection` now resolves `section.headerColor` through a lookup of complete -Tailwind class literals instead of building `bg-` + the authored value as a -template literal (objectui#6178). - -Tailwind v4 has no runtime — it builds the stylesheet by scanning source text -for complete class tokens, and this workspace ships no `bg-*` safelist — so the -old expression contributed nothing to the compiled CSS. Measured, not assumed: -compiling `apps/console/src/index.css` with that expression deleted produced a -byte-identical stylesheet (same sha256). An authored value styled the header -only when some other source file happened to author the identical class -literally, which is why both documented examples appeared to work: `bg-muted` -occurs 691 times and `bg-primary/10` 63 times elsewhere in the workspace. That -liveness was accidental and moved with unrelated edits in unrelated packages. - -The shape matches the sibling this repo already solved the same way — -`useRowColor`'s `COLOR_TO_CLASS` in `@object-ui/plugin-grid`: - -- a lookup of literal, tint-only design-system classes: `muted`, `muted/50`, - `accent`, `primary/10`, `secondary/10`, `destructive/10`. Both values the - `@object-ui/types` mirror documents (`muted`, `primary/10`) are in it, so - nothing that rendered before renders differently now; -- a value that is already a complete `bg-*` class is passed through untouched. - This is new — `headerColor: 'bg-muted'` previously produced the meaningless - `bg-bg-muted`; -- anything else contributes no class at all, instead of a fabricated one. - -Behaviour change to be aware of: an undocumented bare suffix outside the -vocabulary (say `headerColor: 'blue-100'`) no longer reaches the DOM as -`bg-blue-100`. It rendered before only where another file happened to author -that exact class; write it as the complete class (`headerColor: 'bg-blue-100'`) -to keep it, on the same terms as any `className` a schema carries. No value is -rejected and the declared type is unchanged. - -`headerColor` remains undeclared on the strict `@objectstack/spec` -`record:details` section schema, which refuses it today on the strength of this -defect (objectstack#11661). Declaring it, and with which vocabulary, is a -separate spec decision. diff --git a/.changeset/6188-line-items-child-object-decline.md b/.changeset/6188-line-items-child-object-decline.md deleted file mode 100644 index 13a4a04f6c..0000000000 --- a/.changeset/6188-line-items-child-object-decline.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`record:line_items` declines to fetch the child schema of a panel whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. - -`childObject` is declared `required: true` on the block's registry entry and typed `string` on -`LineItemsPanelSchema`, but nothing enforces either — `inputs[].required` is designer metadata, and -the block has no spec schema — so a node reaches the renderer straight off an authored schema with -the key `undefined`, and the child-schema effect asked the data layer for it anyway. Measured: -mounting the block through the registry with `childObject` unset issued -`getObjectSchema(undefined)`, and a real backend receives a query for an object literally named -`undefined`. The effect's `.catch` then turned the answer into a null child schema, so the visible -outcome was a silently unsanitized child grid rather than an error. - -The effect now declines and warns, naming the key and what to set it to, and clears the cached child -schema so a later save is never sanitized against a previous object's fields. This is the choice -`RelatedList` already makes for the same class of missing key (*"has no referenceField/parentId — -refusing to fetch all rows"*), and the one `object-master-detail-form` makes on this exact key. A -panel that names its child object fetches exactly as before. diff --git a/.changeset/6190-record-details-heading-slot-converge.md b/.changeset/6190-record-details-heading-slot-converge.md deleted file mode 100644 index 66bc12f1c5..0000000000 --- a/.changeset/6190-record-details-heading-slot-converge.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-detail': minor -'@object-ui/app-shell': minor ---- - -`record:details` section headings converge on the declared `label` slot; the -`title` alias limb is gone (objectui#6190, maintainer ruling 2026-08-31 — -option A, three producers plus the consumer in one change). - -**Breaking for anyone reading `deriveFieldGroupDetailSections`' output — the -emitted key moves from `title` to `label`.** That function is public API -(exported from `@object-ui/plugin-detail`), so this is an output-shape change, -not an internal refactor. A caller that reads `section.title` off its return -value reads `undefined` after this release and must read `section.label`. -`BuildPageOptions.sections` and `ObjectDefLike.sections` declare the same move, -so a caller passing `sections` into `buildDefaultPageSchema` supplies the -heading as `label` too. - -**The retired authoring spelling is `title` on a `record:details` section.** -`RecordDetailsRenderer` read `s.title ?? s.label` — a strict-priority second -spelling of one slot, with byte-identical localization on both limbs, so a -producer emitting both silently disagreed with itself and `title` won. It now -reads `label` only. Nothing an author could publish is affected: `@objectstack/spec` -REFUSES `title` inside a `sections[]` entry (`unrecognized_keys`, pinned by -objectstack#11902), and `@object-ui/types` plus the authoring inspector have only -ever declared `name` / `label` / `columns` / `fields`. The declared and authoring -faces were already converged; only three runtime producers lagged, and all three -move here: - -- `buildDefaultPageSchema`'s `deriveFieldGroupDetailSections` section literal; -- `RecordDetailView`'s re-map of that output through the per-object i18n - convention; -- `RecordDetailView`'s auto-grouped "More details" section, authored in - app-shell and unreachable from the synthesizer. - -**No rendered heading changes.** Every existing producer yields byte-identical -headings, asserted by rendering rather than by inspection — including the -"More details" bucket, which the earlier two-step scope would have degraded to -the literal `details` in every shipped locale while every existing guard stayed -green. A new pin (`RecordDetailView.sectionHeadingsRenderPath-6190.test.tsx`) -walks the tree app-shell actually renders and closes that blind spot. diff --git a/.changeset/6194-line-items-row-fetch-decline.md b/.changeset/6194-line-items-row-fetch-decline.md deleted file mode 100644 index 312e431c29..0000000000 --- a/.changeset/6194-line-items-row-fetch-decline.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`record:line_items` declines to LOAD OR WRITE the rows of a panel whose child object it never resolved, instead of calling `find(undefined, …)` — the sibling site of the child-schema decline, in the same component. - -`LineItemsPanel` read `schema.childObject` at two sites. The first now declines; the row load still -asked the data layer to `find` an object literally named `undefined`, scoped by -`{ [relationshipField]: parentId }`. `load` guarded the *data source* and the *parent id* — the two -things `RelatedList` calls "can I scope this query" — but not the *object being queried*. - -Declining that fetch is not enough on its own, and this is the part worth reading: `load` owns -`loading`, and the panel branched `loading ? "Loading…" : !parentId ? "Save the record first…" : -`. So the moment the fetch declined, an unresolvable panel with a parent id bound fell to the -third branch and showed an **empty editable grid with an Add button, over an object that does not -exist** — a worse outcome than the fetch it replaced. Measured on the pre-fix component: one -keystroke in the grid's always-present ghost row materialised a row, which enabled Save, which -reached `batchTransaction([{ object: undefined, action: 'create', data: { qty: 3, invoice: 'inv-1' } }])`. -The bad *read* was one keystroke away from a bad *write*. - -An unresolvable panel therefore gets its own render branch — a config hint naming `childObject` and -what to set it to, following the precedent `object-master-detail-form` set for this exact key and -`AdvancedChartImpl`'s refusal placeholders. It is checked ahead of `loading`, because nothing is -pending: the schema itself already says the panel can never resolve, so there is no honest moment at -which "Loading…" is true. `save` takes the same one-line guard, for the one route the render branch -cannot close — a schema edited to drop `childObject` while rows are already dirty. - -A panel that names its child object loads, renders and saves exactly as before. diff --git a/.changeset/6208-worktree-recipe-fetch-base.md b/.changeset/6208-worktree-recipe-fetch-base.md deleted file mode 100644 index 71c1fe61df..0000000000 --- a/.changeset/6208-worktree-recipe-fetch-base.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Instruction-file and PreToolUse-hook prose only: no published package's `src/` changed, so -nothing ships. - -The documented worktree recipe branched off the **local** `main` ref, which nothing in the -recipe or its surrounding prose fetches. A worktree created by following it literally starts -as far back as whoever last happened to update that ref — and on a long-lived shared -checkout, nobody has a reason to. `origin/main` alone would carry the same defect one layer -down (it too is a local ref only a fetch moves), so each in-scope site now begins with an -explicit `git fetch origin main &&` and names `origin/main` as the base: - -``` -git fetch origin main && git worktree add ../- -b origin/main && cd ../- && pnpm install -``` - -Four sites repaired — `CLAUDE.md`, `AGENTS.md` §9 多 agent 协作纪律, and the recipe both -worktree guards print when they block an edit (`.claude/hooks/guard-main-checkout.sh`, -`.claude/hooks/guard-main-checkout-bash.sh`), which is the copy an agent is most likely to -run verbatim. The two `-cmp` comparison-tree lines are unchanged: they take an explicit ref -from the caller and are correct as written. - -Ports the repair landed upstream in objectstack#11934 (objectstack#11540); this repo's copy -was deliberately left to this card. Net ±0 lines on every file touched. diff --git a/.changeset/6213-core-adapters-readme-owns-its-directory.md b/.changeset/6213-core-adapters-readme-owns-its-directory.md deleted file mode 100644 index 2f152e2974..0000000000 --- a/.changeset/6213-core-adapters-readme-owns-its-directory.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/data-objectstack': patch ---- - -`packages/core/src/adapters/README.md` now documents the adapters that are actually in that -directory, and the ObjectStack material it carried moved to the package that owns the behaviour -(objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a -README rides every tarball — so this was published documentation describing the wrong package. - -The page had been left behind when the ObjectStack adapter moved out to -`@object-ui/data-objectstack`: its headings, feature list, filter-operator table and -query-parameter table were all about that adapter, and its one-entry "Available Adapters" list -told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`. -`ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and -`emulateBatchTransaction` — the five exports that directory really ships — were named nowhere. - -- **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a - usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the - ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was - always about this directory. -- **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the - filter-operator and query-parameter mapping tables, the AST conversion example and the sorting - example. That material existed **only** in the `core` copy — this package's README documented - query translation as a single feature bullet — so it is ported, not dropped. - -No runtime behaviour changes; the duplicate copy of one package's documentation living under -another package is what goes away. diff --git a/.changeset/6223-object-payload-spec-keys.md b/.changeset/6223-object-payload-spec-keys.md deleted file mode 100644 index e6b7eb8071..0000000000 --- a/.changeset/6223-object-payload-spec-keys.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@object-ui/plugin-designer': minor -'@object-ui/app-shell': minor ---- - -Object-level metadata payloads no longer emit the three keys `ObjectSchema` refuses by -name — **group**, **sortOrder** and **relationships** (objectui#6223). - -Measured against the installed `@objectstack/spec` 17.2.0, whose `ObjectSchema` accept set -is 42 keys: - -``` -const base = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } }; - -ObjectSchema.safeParse(base) => success = true (control) -ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control) -ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control) - -ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"] -ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"] -ObjectSchema.safeParse({ ...base, relationships: [ … ] }) => unrecognized_keys ["relationships"] -``` - -The two controls are what make that a key-by-key result rather than a schema refusing -everything. Each key was resolved on its own, as the objectui#5761 family ruling requires: - -- **group** — the Object Manager's grouping is a UI-only display category. The spec has no - object-level grouping key (`fieldGroups` groups the fields *inside* one object), so the - grouping control and its column stay, and the value is now DERIVED from the spec key that - is accepted (`isSystem`) instead of round-tripped. `MetadataObjectsPage` also strips a - `group` already stored by an earlier build, because its save-back spreads the server - document verbatim and would otherwise keep re-sending it forever. -- **sortOrder** — what populated it was the array index the converter happened to be at, - i.e. the order the list was already in. The declaration is removed from the object - payload. The field-level `sortOrder` is a different key with a different card - (objectui#6045) and is untouched. -- **relationships** — the spec models relationships on the FIELD (`reference` / - `master_detail`, plus object-level `indexes`). The object payload stops declaring and - sending an object-level relationship array; what the designer should author for a - relationship is a data-model question this change does not settle. - -**Breaking for TypeScript consumers of `ObjectMetadataPayload`** (exported from app-shell): -the three properties are gone from the published type, so code that set them stops -compiling. That is the point — setting any of them produced a payload the metadata route -refuses. `ObjectDefinition` (the designer's UI model) is unchanged and still carries all -three. - -The parity gate built for objectui#5761 now has a **second oracle**: every shape in -`PAYLOAD_SHAPES` names the schema that judges it, `ObjectSchema` alongside `FieldSchema`, -and reach is resolved within an oracle rather than across one — `group` is a legal -`FieldSchema` key and a refused `ObjectSchema` key at the same time. That extension found a -fourth object-level key (`enabled`, objectui#6238) and a value-level rejection the key-name -check cannot see (`fields` sent as an array where the spec wants a map, objectui#6240); -both are filed and ledgered rather than fixed here. diff --git a/.changeset/6230-budget-comment-half-status.md b/.changeset/6230-budget-comment-half-status.md deleted file mode 100644 index 3b79f6fff9..0000000000 --- a/.changeset/6230-budget-comment-half-status.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -CI-only: the Bundle Analysis PR comment now names *which* eager-closure half -objected, instead of reporting only that something did. - -`scripts/check-eager-closure-budget.mjs` evaluates three halves and publishes a -verdict for each to `$GITHUB_OUTPUT` — `closure_status` (the aggregate ceiling), -`closure_chunk_status` (the per-chunk ceilings) and `closure_headroom_status` -(ceiling sensitivity). `.github/workflows/performance-budget.yml` passed only -the first into the comment step, so the two others were published and never -read. The step's exit code folds all three into one `budget_status`, which meant -the comment could say a budget objected but not which half — a reader had to -open the job log to learn whether the total grew, one chunk grew, or a ceiling -had stopped measuring anything. - -- Both missing verdicts are now passed into the comment step and rendered. -- The healthy comment is unchanged: the breakdown appears only when a half is - not `pass`, verified byte-for-byte against the previous renderer. -- A drifted ceiling (exit 2) reads as a broken **gauge** rather than a size - failure, and no longer claims "nothing was measured" while showing two - ceilings that passed. -- `render-budget-comment.test.ts` now fails if the checker publishes a - `closure_*` verdict the workflow does not wire through, so a fourth half - cannot repeat this. - -The exit-code mapping is untouched: exit 2 still maps to `budget_status=error`, -any other non-zero to `fail`, and `error` still outranks `fail` across all three -halves. diff --git a/.changeset/6235-mergedsort-wrap.md b/.changeset/6235-mergedsort-wrap.md deleted file mode 100644 index 7c90482826..0000000000 --- a/.changeset/6235-mergedsort-wrap.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView` wraps `table.defaultSort` before handing it to a delegated list view, so a -view whose only ordering is the deprecated key actually sorts (objectui#6235). - -`ObjectGridSchema.defaultSort` is declared a SINGLE `{ field, order }` object — the zod -mirror agrees (`z.object({ field, order })`, not a union) — while the `list-view` node's -`sort` slot is declared `string | SortConfig[]`, imported by reference from the spec's own -`ListViewSchema`. `mergedSort`'s last branch forwarded the bare object into that slot -unwrapped. The three branches ahead of it all produce an array or a string, so this was the -one shape the slot never declared. - -Nothing crashed and nothing warned: every reader of that slot drops an unparseable sort -silently. `ListView.parseSortConfig` and `ObjectGrid.parseSchemaSort` both open -`typeof sort === 'string' ? [sort] : Array.isArray(sort) ? sort : []`, so a bare object -yields `[]`; the shared sink `convertSortToQueryParams` returns `undefined` for it. Both -in-tree hosts feed the slot straight into `ListView` (`app-shell`'s `fullSchema` and -Studio's `renderStudioGridList`), so the symptom was an unsorted list with no error — -while the SAME metadata sorted correctly as a grid, because `ObjectGrid` performs this -lowering for the same pair. - -The wrap is verbatim the one the non-grid fetch path in this same file already applies -(`|| (schema.table?.defaultSort ? [schema.table.defaultSort] : undefined)`), so all three -consumers now agree and no fourth dialect is introduced. The shared sink is deliberately -NOT widened to accept a bare `{ field, order }`: that is the widening the maintainer ruling -of 2026-08-22 rejected on the merits, because the same slot legitimately carries -`$orderby`'s own `Record` map, in which `{ field: 'desc' }` is a legal -ordering by a column literally named `field`. - -Precedence is unchanged — a named view's sort still outranks `table.sort`, which still -outranks `table.defaultSort`. Only the final branch changes shape. - -One behaviour note for hosts writing off-schema metadata: an ARRAY in `table.defaultSort` -was previously forwarded verbatim by this path alone and is now lowered like every other -resolver in the repo, which leaves it unreadable rather than rescuing it. That input is -already refused by the zod mirror and already behaves this way on the fetch path and in -`ObjectGrid`; the canonical slot for an array is `table.sort`. diff --git a/.changeset/6236-section-grouping-contract.md b/.changeset/6236-section-grouping-contract.md deleted file mode 100644 index c1fed244a0..0000000000 --- a/.changeset/6236-section-grouping-contract.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor -'@object-ui/plugin-form': minor ---- - -The section grouping contract (objectui#6236, maintainer ruling 2026-08-27): a -`section-divider` row may now CLAIM its member fields — `FormField.fields: string[]`, the -same membership shape `FormFieldTab.fields` / `FormFieldPane.fields` already model — and -the form renderer then gates the WHOLE group on the divider's own visibility verdict -(`visibleWhen` / `visibleOn` / legacy `condition`). - -Before this, one authored `FormSection.visibleWhen` meant two different things: the -console renderer drops the whole `
` (heading and fields), while the plugin-form -chain's renderer treated `section-divider` as a purely presentational row and hid only -the HEADING, leaving the section's fields rendering (measured in objectui#6111, which -pinned that honestly rather than implying a guarantee it did not deliver). - -Ruled semantics, now pinned in `section-grouping-6236.test.tsx`: - -- **Visibility decides what is DRAWN and nothing else** (console precedent, 2026-08-22 - ruling after #5594) — a hidden section's values still submit. -- **A hidden section's fields skip client-side validation** — a user is never blocked by - an error pointing at a control they cannot see (the objectui#6110 defect shape); the - server-side contract remains the loud floor for genuinely-required data. A section - hiding mid-session also clears its members' stale errors, the way a field's own false - predicate already did. -- **A divider without a claim keeps the old contract** (its predicate gates only the - heading), so existing schemas are untouched. - -Both halves ride the mechanism the field-level predicate already uses (return `null`; -react-hook-form keeps the value and skips the unmounted control), so field-level and -section-level visibility cannot drift apart. The zod mirror (`FormFieldSchema`) declares -the key with the same scope note. - -`@object-ui/plugin-form` wires the producer half: all six `section-divider` synthesis -sites (ObjectForm's stacked simple path, ModalForm's sectioned and derived-fieldGroup -paths, DrawerForm's sectioned and derived-fieldGroup paths, SplitForm's panes) now stamp -the membership claim onto the divider they emit, from the RESOLVED member list — so an -authored `FormSection.visibleWhen` finally hides the whole section on the object-view -chain, matching the console renderer. The #6111 honest pin (`measured scope`) flipped -accordingly: it now pins heading-and-fields hiding together, and every per-layout DENIED -row asserts the claimed member as well as the heading. The derived-fieldGroup sites carry -the claim for uniformity but stay fail-open — the spec `fieldGroups` vocabulary has no -section-predicate slot to author. The tabbed arm's predicate slot (objectui#6237) is -designed to reuse this same grouping contract. diff --git a/.changeset/6237-section-predicate-layout-diagnostic.md b/.changeset/6237-section-predicate-layout-diagnostic.md deleted file mode 100644 index 97d9c70221..0000000000 --- a/.changeset/6237-section-predicate-layout-diagnostic.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -An authored section `visibleWhen` on `formType: 'tabbed'` or `formType: 'wizard'` now -**reports** that the layout cannot honour it, instead of being silently dropped -(objectui#6237). - -`ObjectForm` rebuilds each section key by key when it delegates to a layout, so a key -the map does not copy never reaches a renderer at all. Three of those maps copy -`visibleWhen` (`split` / `drawer` / `modal`, objectui#6111) and the flat arm carries it -on the `section-divider` pseudo-field — but the `tabbed` and `wizard` maps copy nothing, -so an author writing the key on those two arms watched it do exactly nothing, with no -signal anywhere. That silence is the defect this ships against. - -The two arms now log a warning naming the layout and the sections whose predicate is -being dropped, through one shared message builder so they cannot drift apart. - -**This changes no rendering behaviour** — the predicate is still not evaluated on those -arms. It is the interim half of a maintainer ruling (2026-08-29) that the real repair is -a **design** task: one renderer-side section/group contract with a predicate slot, -designed once for every layout arm (tabbed / TabbedForm / WizardForm / flat) rather than -patched arm by arm. The ruling requires the diagnostic to land first, so the gap stops -being invisible while that contract is designed. - -Deliberately silent on the arms that work, so the warning stays worth reading: - -- `split` / `drawer` / `modal`, and the flat layout — all honour a section `visibleWhen`. -- `ModalForm` with `contentLayout: 'tabbed'` — honours it through the real - `FormFieldTab.visibleWhen` slot that landed in objectui#6619. "Tabbed" names two - different things on this card; only `formType: 'tabbed'` (`TabbedForm`) is inert. -- A master-detail parent, which re-enters `ObjectForm` through its own parent schema — - the report is left to that inner pass, where the real layout is decided (a - master-detail `wizard` parent renders `simple`, which honours the key). Reporting at - both would double-report the tabbed parent and false-report the wizard one. - -No authorable key is added anywhere: declaring `visibleWhen` on a type whose renderer -ignores it is the defect this card family exists to close, and the shared -`FormSectionConfig` that `WizardForm` uses for its steps makes that trap concrete. diff --git a/.changeset/6237-tabbed-arm-predicate-slot.md b/.changeset/6237-tabbed-arm-predicate-slot.md deleted file mode 100644 index 05eea44304..0000000000 --- a/.changeset/6237-tabbed-arm-predicate-slot.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor -'@object-ui/plugin-form': minor ---- - -The tabbed arm of the grouping contract (objectui#6237, same maintainer ruling as -objectui#6236): `FormFieldTab` gains the predicate slot the ruling named — -`visibleWhen?: string | { dialect?: string; source: string }` — so a section rendered as -a TAB PANEL (`ModalForm` `contentLayout: 'tabbed'`) can finally carry an authored -`FormSection.visibleWhen`. The tabbed layout synthesises no `section-divider` at all, so -the #6236 membership-claim mechanism had nothing to stamp the predicate onto and no slot -to copy it into; the predicate was silently dropped one hop before evaluation (measured -in objectui#6237's card). - -The form renderer evaluates the tab's predicate with the same record assembly the -field-level rules use (`ruleRecord` / `previousRecord` / host predicate scope, #6010), -fail-open, and when FALSE draws neither the tab's trigger nor its panel. Not drawing the -panel unmounts the claimed fields through the exact mechanism a field's own false -predicate uses, so the ruled hidden-group semantics are inherited rather than -re-implemented, and are pinned in `fieldtab-visiblewhen-6237.test.tsx`: - -- **Visibility decides what is DRAWN and nothing else** — a hidden tab's values still - submit. -- **A hidden tab's fields skip client-side validation** — a user is never blocked by an - error pointing at a control they cannot see; the server-side contract remains the loud - floor for genuinely-required data (#2959's trap, answered the same way for tabs as for - sections). A tab hiding mid-session clears its members' stale errors. -- **Deterministic re-selection**: a predicate hiding the ACTIVE tab activates the user's - pick if still visible, else the declared default, else the first visible tab — never an - empty panel — and the user's pick is restored the moment its tab is re-admitted. -- **No mid-interaction collapse**: whether the tabbed arm engages stays judged on the - DECLARED tabs, so a predicate hiding one of two tabs filters the strip (and hides the - tab's fields) instead of collapsing the modal into the stacked layout under the user's - cursor. With every tab hidden the strip is omitted; unclaimed fields still render. -- **A tab without the key keeps the pre-#6237 contract** (always drawn), so existing - schemas are untouched. - -`@object-ui/plugin-form` wires the producer half: `ModalForm`'s tabbed synthesis site now -copies the section's `visibleWhen` onto the tab it emits, and the #6111 layout matrix -gains the tabbed-modal rows (direct and via `ObjectForm` delegation). `TabbedForm` / -`WizardForm` still declare no section predicate in their own section configs — those arms -remain open on objectui#6237. diff --git a/.changeset/6237-tabbed-section-predicate.md b/.changeset/6237-tabbed-section-predicate.md deleted file mode 100644 index 35b97ac321..0000000000 --- a/.changeset/6237-tabbed-section-predicate.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-form': minor ---- - -`formType: 'tabbed'` now honours an authored section `visibleWhen` (objectui#6237). - -The tabbed arm of the one grouping contract ruled 2026-08-29 (option A). Before -this, an authored `FormSection.visibleWhen` was dropped on the tabbed route -while `split` / `drawer` / `modal` and the flat layout all honoured it — the key -never reached a renderer at all, so it did nothing. - -`TabbedForm` already synthesised the renderer's `fieldTabs`, which is the same -machinery the `modal` + `contentLayout: 'tabbed'` arm runs on. The predicate was -simply dropped at three points on the way there, and all three now carry it: -`ObjectForm`'s tabbed section map, `FormSectionConfig` (which declared no such -key), and `TabbedForm`'s `fieldTabs` synthesis. - -Because the arm reaches the existing evaluator, the three ruled semantics are -inherited rather than re-implemented beside it: a hidden tab's values still -submit, its fields skip client-side validation (so a required field on a hidden -tab cannot block a submit invisibly — objectui#2959's defect through a new -door), a predicate hiding the ACTIVE tab re-selects deterministically instead of -drawing an empty panel, and arm engagement stays structural on the DECLARED -tabs so a predicate cannot collapse the strip mid-interaction. - -Two boundaries are deliberate: - -- A single-section tabbed form never engages the tab arm, so it degrades to the - untabbed layout's own predicate mechanism — a chrome-less `section-divider` - claiming its members by name. Existing single-section forms are unchanged; the - gate is emitted only where a predicate was actually authored. -- Wizard STEPS still do not take a predicate, and now say so in the type: - `WizardStepConfig` omits the key, because a step predicate is a different - contract (step-boundary reactive against the ruled live-record reactivity, and - needing navigation and final-gate semantics none of this machinery supplies). - `ObjectForm` continues to report that gap at runtime for untyped JSON. diff --git a/.changeset/6237-wizard-step-config-split.md b/.changeset/6237-wizard-step-config-split.md deleted file mode 100644 index 4e732a0331..0000000000 --- a/.changeset/6237-wizard-step-config-split.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -Split `WizardStepConfig` off `FormSectionConfig`, and correct the section-predicate -support table (objectui#6237, maintainer ruling 2026-08-30). - -`WizardForm` typed its steps as `Omit` — a -subtraction from the TabbedForm section type, which is the predicate-CARRYING -type. That defended the one key it named and left the mechanism open: every key -added to `FormSectionConfig` reached a wizard step by default, so the next -predicate in the same family (`readonlyWhen` / `requiredWhen`, already this -package's field-level vocabulary) would have handed the wizard a silent slot its -renderer does not read — the declared-but-unenforced shape the ruling split the -types to stop. - -`WizardStepConfig` is now declared independently in `WizardForm.tsx`, which is -simply what `SplitFormSectionConfig`, `ModalFormSectionConfig` and -`DrawerFormSectionConfig` already do: each layout owns its group shape, documents -`className` / `gridClassName` in its own terms, and declares `visibleWhen` only -where its renderer honours it. The derivation flips from subtractive to additive -— a key is authorable on a wizard step only if someone writes it there. - -No behaviour change and no key added or removed: `WizardStepConfig` exports the -same key set it already had, and `visibleWhen` on a wizard step literal was, and -remains, a compile error. What is new is that it stays one for the whole -predicate family, pinned by a type-level assertion that fails the build if any -`*When` key ever appears on the step type. - -Documentation repair in the same stroke: the support table in the README and in -`content/docs/plugins/plugin-form.mdx` still said `formType: 'tabbed'` sections -drop the predicate. That stopped being true when the tabbed arm landed — the row -now reads **Yes**, the surrounding prose no longer claims two inert arms or a -diagnostic that fires for `tabbed`, and the wizard row stays **No**, which is -still exactly true. diff --git a/.changeset/6238-object-payload-enabled.md b/.changeset/6238-object-payload-enabled.md deleted file mode 100644 index fc618f3cd9..0000000000 --- a/.changeset/6238-object-payload-enabled.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`MetadataService`'s two delete methods no longer PUT a hand-written tombstone. They call -the metadata API's own delete door instead, and the latent `enabled?: boolean` on -`ObjectMetadataPayload` is gone with it (objectui#6238). Object-level member of the -objectui#5761 family, surfaced by the `ObjectSchema` oracle objectui#6223 added to -`scripts/check-designer-field-key-parity.mjs`. - -**What the tombstone actually did.** `deleteObject` and `deleteMetadataItem` wrote -`{ name, enabled: false, _deleted: true }` through `client.meta.saveItem`, i.e. -`PUT /api/v1/meta/:type/:name`. Measured against the installed `@objectstack/spec` 17.2.0 -using `getMetadataTypeSchema` — the registry the framework's own `saveMetaItem` resolves a -PUT's validator from — across all 26 registered overlay schemas: - -``` -ObjectSchema.safeParse({ name, label, fields }) => success = true (control) -ObjectSchema.safeParse({ name, label, fields, isSystem: true }) => success = true (control) -ObjectSchema.safeParse({ name, enabled: false, _deleted: true }) => unrecognized_keys ["enabled","_deleted"] - -25 of 26 registered overlay schemas refuse `enabled` and/or `_deleted` BY NAME - 1 of 26 (`view`) tolerates them; 4 kinds have no registered schema at all - 0 of 26 strip them -``` - -So there were two failure modes, not one. Where the type has a strict schema — `object` -among them — the delete was a hard `422 INVALID_METADATA`, so nothing was ever recorded. -Where the schema is tolerant or absent, the framework stores the request item **verbatim** -(it deliberately persists the body rather than `parsed.data`), and `_deleted` has no reader -anywhere on the platform — so the "soft delete" was a silent no-op that left the item live -carrying two junk keys. Neither outcome deleted or disabled anything. - -**The resolution is a mechanism change, not a rename**, and there was nothing to rename to. -`ObjectSchema`'s 42-key accept set has no on/off flag; the near-spelling `enable` is -`ObjectCapabilities`, a system-features *module object*, so `enabled: false` → `enable: -false` fails on the value where it passes on the name. No wire key was invented: a metadata -soft-delete convention would be a `@objectstack/spec` contract addition, and the platform -does not have one. - -**Both sites now call `client.meta.deleteItem(type, name)`** — `DELETE -/api/v1/meta/:type/:name`, the same request `MetadataClient.reset` issues, which is the -mechanism `MetadataObjectsPage.handleObjectsChange` and `ResourceEditPage` already used for -deletes. Two mechanisms for one operation had disagreed; now there is one. The delete route -is generic over `:type` on the same route family and capability gate as the PUT, so this -holds for every category the generic `deleteMetadataItem` serves, not just `object`. The -doc comment claiming the API "exposes `saveItem` but no dedicated `deleteItem`" was stale: -`@objectstack/client` 17.2.0 declares `meta.deleteItem` on the very client this service -already holds. - -`reset` semantics are the overlay's, and that is the governed answer rather than a -shortfall: it removes the customization row — which *is* deletion for an object the -designer authored — and restores the artifact for one a package declares, an object you are -not allowed to delete. Which of the two an item is, is what the API's own `deletable` / -`resettable` verdicts report, not something a client-side flag should decide. - -**No published type changed.** `ObjectMetadataPayload` is exported from its module but that -module is not re-exported by `packages/app-shell/src/index.ts`, the package's only entry, so -the removed `enabled?: boolean` was never on the published surface and no `**/src/index.ts` -is touched. What consumers *can* observe is behaviour: `MetadataService` is reachable -through the published `useMetadataService()` hook, both method signatures are unchanged -(`Promise`), and the HTTP request they issue changes from a `PUT` with a body to a -`DELETE`. - -The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes -with the fix — that ledger ratchets in both directions, so an entry left behind for a -resolved key is as red as a missing one. It is now empty for the first time, which is the -ratchet arriving where it was pointed; the self-test's non-vacuity guard moved onto a -fixture accordingly, so an empty ledger reads as success rather than as a demand that some -key stay unresolved. diff --git a/.changeset/6240-object-payload-fields-map.md b/.changeset/6240-object-payload-fields-map.md deleted file mode 100644 index 8262d3150c..0000000000 --- a/.changeset/6240-object-payload-fields-map.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService`'s object writers PUT `fields` as the name-keyed MAP `ObjectSchema` -requires, not an array (objectui#6240). Both of the designer's write paths were affected, -and `saveFields` ran the conversion in the wrong direction outright: the server's own -document arrives with `fields` as a map, and `fields.map(toFieldPayload)` turned it into an -array on every field save. - -Measured against the installed `@objectstack/spec` 17.2.0 and against the framework's own -write door. `ObjectSchema.fields` is a required record: an array — empty or not — is -refused `invalid_type @ fields`, a map parses. `metadata-protocol`'s `saveMetaItem` -resolves metadata type `object` to that same `ObjectSchema`, `safeParse`s the whole item -and throws `422 INVALID_METADATA` **before** persisting, so the array was refused rather -than stripped or stored: every designer object save and every designer field save that went -through this service was a 422 that wrote nothing. - -This is the value-level half of the objectui#5761 parity family and is invisible to that -family's key-name gate — `fields` sits in the accept set under either shape, which is the -gate's own coverage note 4. The pins are runtime assertions on the captured request bytes. - -The conversion refuses, loudly, what it cannot key: a field with a missing or blank `name` -throws instead of writing a `{ undefined: … }` entry (measured: the spec ACCEPTS that -document, so nothing downstream would have caught it), and a duplicate name throws instead -of letting the later field silently replace the earlier — a loss an array does not have. -`saveFields` keeps preserving unknown keys of the fetched server document, which now -actually reaches storage. `saveObject` with no `existingFields` still omits the key rather -than writing `{}`: a PUT is an upsert, so `{}` would delete every field of an object on a -save that only meant to rename it. - -`saveObject(obj, existingFields)` keeps its `FieldMetadataPayload[]` parameter type — the -array is converted inside — so no caller's call site changes. diff --git a/.changeset/6241-fieldgroups-section-heading-pin.md b/.changeset/6241-fieldgroups-section-heading-pin.md deleted file mode 100644 index fce15362ee..0000000000 --- a/.changeset/6241-fieldgroups-section-heading-pin.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Tests only, no release: pin the synth → renderer seam for the default -`fieldGroups` record detail page (objectui#6241). - -`buildDefaultPageSchema` synthesizes one detail section per declared -`fieldGroups` entry and app-shell's `RecordDetailView` renders that output as -the default record page for every object declaring `fieldGroups` with no -assigned page — but nothing asserted end-to-end that those synthesized sections -render their headings. The synthesizer's own suite asserts its return value and -never renders; the `record:details` renderer suites render hand-authored -section fixtures and never consume synthesizer output. Measured on `9ea4cdee3`: -with the consumer's read of the emitted heading removed, all 109 files / 1031 -tests of `packages/plugin-detail/` still passed. - -The new file renders the real `buildDefaultPageSchema` output through the real -registry into the DOM and asserts the declared heading TEXT, its declared -order, and that a group's internal key never stands in for its label. It -asserts no key spelling, so it stays independent of the `title` / `label` -convergence open in objectui#6190 / objectstack#11661. diff --git a/.changeset/6247-option-visiblewhen-metadata-admin.md b/.changeset/6247-option-visiblewhen-metadata-admin.md deleted file mode 100644 index 7466d7e936..0000000000 --- a/.changeset/6247-option-visiblewhen-metadata-admin.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A per-option `visibleWhen` written into a metadata form now actually withdraws that option -in the metadata-admin renderer (objectui#6247). - -`SelectOptionSchema` declares `visibleWhen` (ADR-0068 / objectui#2284) and the schema is -`z.core.$strict` — an undeclared sibling key is refused with `unrecognized_keys` — so the -key is a real declaration, and a `*.form.ts` carrying a per-option predicate parses clean. -The metadata-admin renderer never read it: all three controls that consume -`fieldSpec.options` mapped the authored list straight to items. Accepted, stored, shipped, -ignored — ADR-0049's declared-but-unenforced shape. - -**This is a behaviour change, and it is the point of the fix.** Metadata that was inert -starts hiding options. An author who wrote a narrowing and got no narrowing now gets one. -Both of the old failure modes pointed the same way — the option stayed offered — so -nothing that relied on the old behaviour was relying on a narrowing being honoured; it was -relying on one being ignored. Zero `*.form.ts` in the tree uses the key today, so no -shipped form changes shape on this release. - -Three consuming controls now filter, and each has a pin asserting an option **absent** on a -false predicate: the builtin Select in `SchemaForm.tsx`, `MultiSelectWidget`, and -`colorPaletteOptions` → `ColorSwatchGroupWidget`. Asserting absence is the whole of the -test design — this evaluator fails OPEN, so "the option is shown" is equally the outcome of -predicate-true, predicate-never-arrived and predicate-faulted, and a positive-only pin -passes against the unfixed renderer. - -Per the maintainer ruling on the card (affirmed three times: 2026-08-25 batch 4; -2026-08-25 upholding A2 over the A1 counter-proposal; 2026-08-27 declining the A0 -reject-the-key alternative): - -- **Scope (A2).** `SchemaForm`'s `evaluatePredicate` ctx now binds the four ADR-0068 D1 - identity spellings — `current_user`, `user`, `ctx.user`, `os.user` — **alongside** `data`, - selected out of the host `ExpressionProvider`'s bag rather than copied, so the alias set - cannot drift from the one builder. `data` stays the **draft**: the provider's bag also - carries a `data` key meaning its own data scope, and adopting that would be - objectui#5926 gap 2's "same key, opposite meanings" one nesting level apart. `record`, - `app` and `features` are deliberately left **unbound** so they keep raising the loud - warn-once diagnostic instead of resolving to a silent `undefined`. That diagnostic now - reads the bound names off the actual scope instead of asserting "the only name is - `data`", which stopped being true the moment identity was bound — a diagnostic that lies - sends the author to un-write a spelling that was correct. No new evaluator: the option - filter routes through the same `evaluatePredicate` the section, field and repeater-row - gates already use. -- **Emptied sets (B1).** The control's **face** keeps reading the **raw** option list — - `resolveFieldFace`'s `hasOptions`, `resolveColorWidgetKey`, and each `options.length > 0` - branch condition — and only the rendered list is filtered. Withdrawing every option - renders an **empty picker**. Filtering the branch condition instead would have degraded - the builtin Select to a free-text `Input` and `MultiSelectWidget` to its comma-tag - editor — "withdraw every option" displayed as "type whatever you like" — and would have - flipped a fully-withdrawn palette from the `color-picker` registration to `color-input`, - making the labelling channel objectui#4871 point 4 fixes in the host predicate-dependent. -- **Stored values (C1).** No pruning. A selected value whose option is now hidden survives - in the stored metadata and the picker shows the placeholder. This includes the quiet path: - `MultiSelectWidget`'s toggle re-orders the selection against the **raw** list, because - ordering against the filtered one would have dropped a hidden-but-selected value on the - next unrelated click. This renderer edits **source metadata**, and objectui#4247's own - reasoning — "missing information is not a reason to destroy stored data" — applies harder - here than it did on the runtime record surface. - -`FormFieldSpec.options` is also no longer hand-written. It derives from the spec's -`SelectOption` with its narrowings named in an `Omit`, per this file's own convention: -`visibleWhen` is re-pointed to the local `VisibilityPredicate` (`dialect` optional, `source` -required — the shape an evaluator here actually takes), and `default` stays dropped **and -now says so**, because nothing on this surface reads it. Two of the spec's five option keys -had been dropped by silence, which is precisely how a legally-authored per-option -`visibleWhen` came to parse clean and render inert. - -**Not fixed here, and it bounds what this change delivers:** this interim evaluator's `in` -operator requires an array **literal** on the right (`role in ['admin','owner']`), so the -ADR's headline spelling `'admin' in current_user.positions` — membership against a *path* — -still falls through to the bare-truthy branch and evaluates TRUE regardless of the user, -silently. That is a pre-existing grammar gap of the whole subset, identical for `data.*` -(`'x' in data.tags` is equally inert) and unchanged by this card; it is filed separately. -The spellings this fix makes genuinely discriminate are the documented subset — -`path == literal`, `path != literal`, `path in [literals]`, `!path`, `path`, `&&`, `||`. diff --git a/.changeset/6250-toast-demo-shapes.md b/.changeset/6250-toast-demo-shapes.md deleted file mode 100644 index 88aa0717a3..0000000000 --- a/.changeset/6250-toast-demo-shapes.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Docs and fixtures only: the seven `components-feedback-toast/*` and seven -`components-feedback-sonner/*` `SchemaExample` fixtures hung an action object off -`onClick` (`{"type":"button", …, "onClick":{"action":"toast", …}}`) — a shape -`ButtonSchema` declares as a FUNCTION and no dispatcher reads, so all fourteen were a -RED `safeParse` on the envelope and clicking the rendered demo raised no toast -(objectui#6250). They now author the registered spellings the engine already executes, -`type: 'toast'` and `type: 'sonner'`, whose renderers draw their own trigger button and -call sonner's `toast()` from it. `feedback/toast.mdx` and `feedback/sonner.mdx` follow, -including two keys sonner's page taught that neither `SonnerSchema` declares nor its -renderer reads (`duration`, `action`). - -No package source or behaviour change; fixtures, docs and pins only. Two things the -fixture correction deliberately does NOT do, both left to the maintainer: declare an -action union on `ButtonSchema.onClick` and build a dispatcher for it, and give the toast -renderers the in-toast action button and promise form the removed demos implied. diff --git a/.changeset/6252-page-header-action-ids.md b/.changeset/6252-page-header-action-ids.md deleted file mode 100644 index 406c5c5422..0000000000 --- a/.changeset/6252-page-header-action-ids.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`page:header` resolves its `actions` as declared ACTION IDS (objectui#6252, -implementing the objectstack#11592 ruling — maintainer, 2026-08-25, on -recommendation B). - -`@objectstack/spec`'s `PageHeaderProps.actions` is -`z.array(z.string()).describe('Action IDs to show in header')` and has been for -as long as the key has existed. The canonical renderer read that array as -`ActionDef` objects and resolved nothing, so a header authored the way the -published contract declares rendered **zero buttons** — satisfying the schema -deleted the header. Two sibling surfaces already read it as ids -(`record:quick_actions`, and `layout:page-header` by delegating to it), so one -authoring key meant two different things depending on which renderer drew the -header. - -Each id is now resolved against the object's own `actions` metadata through the -same `useMetadataItem` entry `record:quick_actions` uses. Resolution happens at -the top of the actions pipeline, so the existing chain — `record_header` / -`record_more` placement, the `requiredPermissions` capability gate, `visible` / -`hidden`, `order`, and the inline/overflow split — runs unchanged over -uniformly-shaped defs: an id-authored header and an object-authored one converge -before a single filter runs. - -- Inline `ActionDef` objects keep rendering, per element, so a half-migrated - `['convert', { … }]` array resolves the id and passes the object through. This - is renderer tolerance for the migration and stays undeclared — the contract is - ids. -- An id that resolves to no action renders nothing and warns **once**, naming - the object's declared action names. A mistyped id is no longer indistinguishable - from a correctly hidden one. -- Nothing is written back onto the authored node, so an id-authored page carries - no `ActionDef` — and no `body.source` handler body — into what it serializes. diff --git a/.changeset/6269-objectview-slot-key-lists.md b/.changeset/6269-objectview-slot-key-lists.md deleted file mode 100644 index 12c8d4b312..0000000000 --- a/.changeset/6269-objectview-slot-key-lists.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`ObjectViewSchema`'s `table` and `form` slots now ship the members they promise -(objectui#6269). Both were declared by deriving from the schema they document — `table?: -Partial>`, `form?: Partial>` — and both derived types declared **zero** properties. - -`Omit` is `Pick>`, and `keyof T` on a type carrying a string index -signature is `string | number` — the literal member names are absorbed. `ObjectGridSchema` and -`ObjectFormSchema` both inherit `BaseSchema`'s `[key: string]: any` (objectui#5155), so each -`Pick` rebuilt a type holding the index signature and none of the named members. Measured -through the TypeScript checker: `ObjectGridSchema` 61 members, the `Omit` of it 0; -`ObjectFormSchema` 67, the `Omit` of it 0. This is objectui#6151's collapse in *property* -position — #6151's guard walks the `LayoutSchema` union and cannot see properties on -`ObjectViewSchema`. - -Nothing errored, because the index signature answered every key as `any`. The visible costs -were the ones only a reader of the declaration meets: `table: { colunms: 3 }` type-checked, -`table: { pageSize: 'ten' }` type-checked, and editor completion inside `table: { … }` offered -nothing at all for a slot documented as "inherits from ObjectGridSchema". - -Each `Omit` is now a `Partial>` over an explicit key list — 59 keys for `table`, 64 for -`form`, i.e. every declared member minus the identity keys the view itself fixes. `Pick` with -literal keys never computes `keyof T`, so it cannot collapse the same way. The key lists are -pinned against silent drift by `packages/types/src/__tests__/object-view-slot-key-lists.test.ts`, -which recomputes each source schema's declared members through the TypeScript checker and -requires set equality; a member added to `ObjectGridSchema` and not to the list turns it red. - -**Tightening, deliberately.** Restoring named members re-enables excess-property checks on -object literals assigned into these two slots, so a misspelled key there is now an error -instead of silently doing nothing. That is the intent of the fix. The slots' member *types* are -unchanged — every key that resolved to a real declared type before still does. - -The `Pick` lists exist only because `BaseSchema` carries a root string index signature. When an -objectui#5155 phase removes it, `Omit` stops collapsing and the lists (plus their pin) become -removable; the pin's own comment records the condition, and one of its assertions is the -tripwire that will notice. diff --git a/.changeset/6270-schemarenderer-scopeclass-memo.md b/.changeset/6270-schemarenderer-scopeclass-memo.md deleted file mode 100644 index 39f1e262c2..0000000000 --- a/.changeset/6270-schemarenderer-scopeclass-memo.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`SchemaRenderer` now hands a stable `schema` object identity to a node carrying -`responsiveStyles` (objectui#6270). - -ADR-0065 scoped styles make a styled node take a branch that rebuilds the schema -object to merge the generated scope class into `className`. That rebuild was not -memoised, so it allocated a new object on **every** `SchemaRenderer` render — even -when the `evaluatedSchema` memo directly above it held. Every downstream renderer -that memoises on `[schema]` therefore saw a fresh identity and re-ran: concretely -`ObjectMap`'s `dataConfig` and `mapConfig`, and the whole marker cascade below them -(`markers` → `filteredMarkers` → `clusteredData` / `markerBounds` → `initialViewState`). -Only nodes on that branch were affected — a plain node was already handed the -memoised `evaluatedSchema` itself. - -The trigger is narrower than "has `responsiveStyles`" reads: it needs one of the four -sized breakpoint keys (`large` / `medium` / `small` / `xsmall`). A `{ base: … }` shape -never took the branch and was never affected. - -The scoped-style computation now lives in a `useMemo` keyed on -`[evaluatedSchema, autoStyleId]`, hoisted above the renderer's early returns — its old -use site sits after them, so a memo written there would have been a conditional hook. -A genuinely changed `className`, interpolated value or breakpoint still produces a new -identity, so nothing goes stale. diff --git a/.changeset/6271-kanban-fetch-gate.md b/.changeset/6271-kanban-fetch-gate.md deleted file mode 100644 index dd74c6c0bb..0000000000 --- a/.changeset/6271-kanban-fetch-gate.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -`ObjectKanban` no longer queries twice on mount (objectui#6271). A standalone board issued -its first `find` before the object definition resolved — so `buildExpandFields` saw no -fields and that query carried no `$expand` at all — then issued a second, expanded one once -the definition landed: - -``` -['deal', { $top: 100 }] -['deal', { $top: 100, $expand: ['owner'] }] -``` - -The definition now GATES the query instead of refining it afterwards: one query per mount, -carrying the expansion the first time. - -Decided on measurement rather than on the two shapes' relative appeal. The first response -never reached the screen in the regimes that matter: with the schema resolving no slower -than the row query (profiles schema/find = 30/30, 30/60, 5/30 ms), the definition lands -first, the effect re-runs, its cleanup flips `isMounted` false, and the unexpanded rows are -discarded on arrival — a DOM probe polling every 2ms for a title only that response carried -never fired once. What the gate costs is one schema resolution ahead of the query, and that -read is cheap and shared: one small GET behind the same discovery call `find` already -awaits, served thereafter from `MetadataCache` (5-minute TTL, concurrent readers coalesced -onto one request). Measured against the real `ObjectStackAdapter` over loopback HTTP, 22 -reads of one object produced exactly one metadata request and every read after the first -returned in 0.01ms. End to end the board is not slower for it — same harness, before → -after, time to the fully populated board: 156.9 → 145.2ms (30/30), 119.8 → 110.6ms (30/60), -54.7 → 52.4ms (5/30). - -The gate is on the definition read having **settled**, not on the definition being truthy: -an adapter that exposes no `getObjectSchema`, and a read that throws, both settle with -nothing to report and the board falls through to an unexpanded query rather than waiting -forever. Boards fed rows by a parent (`data`, `bind`, inline `schema.data`) are untouched — -they never ran this effect, and they still read the definition for lane titles and labels. - -The `isOpaqueId` suppression in the card-description path is unchanged and keeps its -comment beside it: part of what it hid was this fetch ordering, but unexpanded rows still -reach it from parents that pass rows they fetched without `$expand`, from author-supplied -data, and from backends that decline an expansion. diff --git a/.changeset/6272-locationfield-spec-shape.md b/.changeset/6272-locationfield-spec-shape.md deleted file mode 100644 index 51b44d1f3c..0000000000 --- a/.changeset/6272-locationfield-spec-shape.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/fields': minor ---- - -**BREAKING (stored data): `LocationField` reads and writes the spec's `{ lat, lng }`** - -FROM — the widget read `value.latitude` / `value.longitude`, each behind `|| 0`, and emitted -`{ latitude, longitude } | null`. - -TO — it reads and writes `LocationValue` from `@objectstack/spec/data` -(`{ lat, lng, altitude?, accuracy? }`), re-exported here rather than re-declared, and reads -nothing else. A pair is read only when BOTH `lat` and `lng` are finite numbers. - -**The behaviour change, stated plainly:** a `type: 'location'` record stored in the retired -`{ latitude, longitude }` spelling — including one this widget itself wrote before this -release — now renders **EMPTY in the edit surface**, where it used to render its -coordinates. It keeps rendering correctly in detail views, list cells and on the map, which -read `lat`/`lng` first. Re-saving the record through this widget, or fixing the value at the -data layer, restores it. There is deliberately **no compatibility fallback**: the maintainer -ruled the bare flip (2026-08-28, objectui#6272 option A1) explicitly over a dated read-side -shim, choosing zero dialect over softening this cost. - -Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` outside an -`@objectstack` major sync); the break is real and is stated here. - -**Why the widget was the side that moved** - -`@objectstack/spec@17.2.0` exports `LocationValue = { lat, lng, altitude?, accuracy? }` as -the canonical stored shape and deprecates `LocationCoordinates` (`{ latitude, longitude }`). -Measured through the contract itself, `valueSchemaFor({ type: 'location' })` **rejects** -`{ latitude, longitude }` with `invalid_type` at `[lat]` and `[lng]`, and **accepts** -`{ lat, lng }`. So this widget was the one `location` surface producing a shape the -platform's own validator refuses, and `LocationCellRenderer` / `ObjectMap` reading -`lat`/`lng` first is correct by contract, not tolerance. - -The user-visible defect it fixes: a spec-canonical `{ lat, lng }` record rendered **`0, 0`** -in the edit box — not an error state but a valid coordinate in the Gulf of Guinea — while -the same record rendered correctly one panel away. The `|| 0` defaults are gone with the -rename, so a half-stored pair (`{ lat }` alone) no longer invents the coordinate it is -missing; it reads as unset. A stored `{ lat: 0, lng: 0 }` still renders `0, 0`, because that -is now the only way those digits can appear. - -`GeolocationField` is **not** part of this change: `geolocation` is not a member of the -spec's closed `FieldType` union and its value schema accepts both spellings, so it keeps its -own `{ latitude, longitude }` shape. diff --git a/.changeset/6278-context-menu-item-icon.md b/.changeset/6278-context-menu-item-icon.md deleted file mode 100644 index 080e0bf78f..0000000000 --- a/.changeset/6278-context-menu-item-icon.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`ui:context-menu` now resolves a menu item's authored `icon` to a glyph. It previously never read the key at all. - -Both arms of `renderContextMenuItems` — the leaf `ContextMenuItem` and the `ContextMenuSubTrigger` — ignored `icon` entirely, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew its label and nothing else. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `ui:dropdown-menu` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. This mirrors the repair `ui:dropdown-menu` received for the identical defect. - -The `components-overlay-context-menu/basic-context-menu` catalog fixture already declared four live names — `copy`, `scissors`, `clipboard`, `trash` — which drew nothing before this change and draw their glyphs now. Those names are also brought under `check:lucide-icon-record-names` by a new `context-menu` census entry, so a future retired spelling fails the gate instead of silently drawing nothing. diff --git a/.changeset/6280-scan-surface-claims.md b/.changeset/6280-scan-surface-claims.md deleted file mode 100644 index e4cdf7472e..0000000000 --- a/.changeset/6280-scan-surface-claims.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Doc-only fixes in `@object-ui/components` and the docs site: two stale claims about -`scripts/check-doc-links.mjs`'s scan surface, both frozen at an earlier `SCAN_ROOTS` -shape (objectui#6280). - -- `packages/components/src/__tests__/readme-shadcn-sync-categories.test.ts`'s - `## Scan surface` docblock said `README_SHADCN_SYNC.md` had "never been scanned by - anything" — false since objectui#4938, whose `packages/*` row excludes only the - basenames `README.md`/`CHANGELOG.md` and so does include this file. Rewritten to - argue from the current tree: the file IS scanned, but check-doc-links only inspects - `[text](href)` markdown-link syntax (never the backticked code spans this README - uses for every in-repo path) and has no notion of prose-vs-manifest consistency — - so the hand-rolled checks below survive regardless of scan surface, for reasons - unrelated to whether the surface reaches this file. -- `content/docs/guide/ci-cd-pipeline.md` described the scan surface twice (prose and - the two-link-checkers table), both frozen at the objectui#3622 shape ("the internal - `docs/` tree and every package `README.md`"). Re-derived from the live `SCAN_ROOTS` - table (17 rows) and rewritten to include the app READMEs and root-level markdown - (objectui#4148), the rest of each package/app directory tree (objectui#4938), and - every nested `README.md` (objectui#6026). - -No source or behaviour change; text and a test docblock only. diff --git a/.changeset/6285-capability-labels-derive-from-spec.md b/.changeset/6285-capability-labels-derive-from-spec.md deleted file mode 100644 index 5c5c1affa0..0000000000 --- a/.changeset/6285-capability-labels-derive-from-spec.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/i18n': patch ---- - -The capability picker localizes `manage_sharing` (objectui#6285). Before this, "Manage -Sharing" was the one platform capability in `sys_permission_set`'s picker that rendered in -English in every locale, beside seven siblings that translated — a user-visible missing -translation, in all ten packs at once. - -The cause was an unchecked copy. `CURATED_CAPABILITY_LABELS` in -`CapabilityMultiSelectField.tsx` listed seven capability names under a doc comment claiming -it mirrored `@objectstack/spec/security`'s `PLATFORM_CAPABILITIES`; the spec grew an eighth -member and the list did not follow, so `manage_sharing` fell through to the English label -the `sys_capability` registry serves. Nothing could catch it: the i18n gate reads that list -as this key family's vocabulary and checks the members it names — all seven had keys — and -no instrument compared the vocabulary to the array it was named after. - -`capability.label.manage_sharing` is now authored in all ten packs and in the field widgets' -provider-less defaults map, the list carries the member, and the prose claim is replaced by -a check: `CapabilityMultiSelectField.specParity-6285.test.tsx` imports `PLATFORM_CAPABILITIES` -and fails on any difference in either direction, reading the declaration through the i18n -gate's own source reader so what it pins is exactly what that gate consumes. `labelFor` also -gains a `defaultValue`, so a capability that arrives in a future spec bump before its -translation is authored degrades to the registry's English label rather than rendering a raw -i18n key at the user. diff --git a/.changeset/6286-explain-batch-cap-from-spec.md b/.changeset/6286-explain-batch-cap-from-spec.md deleted file mode 100644 index 67e18ef880..0000000000 --- a/.changeset/6286-explain-batch-cap-from-spec.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -The batch-explain cap the row-verdict hook paginates under is now imported from -`@objectstack/spec/security` instead of re-declared locally (objectui#6286). -`useRecordCrudVerdicts` carried `const EXPLAIN_BATCH_MAX_RECORD_IDS = 200`, a hand copy of -a SERVER contract constant, under a doc comment that named its own expiry condition: the -pinned `@objectstack/spec@17.0.0-rc.6` predated the batch form, and the pin bump would -supersede the declaration. It has. - -**No value changes and no behaviour changes.** The spec exports `200`, which is what the -local copy said, verified by resolving the installed package and reading the export — both -statically (`dist/security/index.d.mts`) and at runtime through the same specifier the -source now uses. What changes is reference identity: if the server relaxes or tightens the -cap and the spec follows, the client follows too, instead of paginating at the old boundary -with no signal anywhere. The cap's whole point is that an over-cap request is refused with -`400 VALIDATION_FAILED` rather than truncated, so a client that silently disagrees with it -is exactly the drift `scripts/check-spec-symbol-derivation.mjs` argues about — and could -not catch here, because both of its scanners skip non-exported declarations and this const -was module-local (objectui#5899). - -The declared floor already carries the symbol, so no range moves: `@objectstack/spec@17.0.0` -— the minimum `^17.0.0` admits — exports `EXPLAIN_BATCH_MAX_RECORD_IDS = 200` from -`./security`. Measured against the published tarballs of `17.0.0-rc.6`, `17.0.0`, `17.1.0` -and `17.2.0`: only the rc lacks it. The declaration was therefore expired one release -earlier than the card that found it assumed. - -The half of the comment that explains *why* the cap exists and what the server does with an -over-cap request is kept and now sits on the import; only the half explaining why it was -declared LOCALLY is gone, since that is the part that stopped being true. - -Covered by a new reference-identity test rather than a value assertion. Every assertion on -`200` passes on both sides of this change — a ghost — so -`useRecordCrudVerdicts.batchCap.test.tsx` stands the spec module in at a cap no hand copy -could produce and asserts the request chunking follows it, with a control case proving the -stand-in installed and differs from the shipped value. The pre-existing cap assertion in -`rowRecordCrudVerdict.test.tsx` now derives its fixture and its bound from the same export -instead of re-typing `200`, which removes the last hand copy on this surface without -pretending to be a two-world test. diff --git a/.changeset/6287-flownode-description-key.md b/.changeset/6287-flownode-description-key.md deleted file mode 100644 index e14fbd707d..0000000000 --- a/.changeset/6287-flownode-description-key.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The flow designer's node inspector no longer offers a **Description** field, and strips a -stored `description` off a node the first time an author edits it (objectui#6287). - -`FlowNodeSchema` is `.strict()` (objectstack#4001) and refuses that key by name — measured -on the installed `@objectstack/spec@17.2.0`: - -``` -FlowNodeSchema.safeParse({ id, type, label, description, config }) - -> unrecognized_keys: ["description"] -``` - -By this package's own reading of that mechanism, the cost is not untidiness but an -unsavable draft: the key "surfaces as `unrecognized_keys` in the live client validation and -as a 422 on save" (`flow-canvas-layout.withCanonicalGeometry`, on the identical retired `ui` -case). So the field was not merely describing a shape the contract refuses — it was -producing one, on every keystroke, and nothing anywhere read the value back. The spec's flow -node has eleven keys and no note key of any spelling, so there was no reader to grow into. - -Stored flows heal on the author's first edit, the same migrate-on-write boundary the retired -`ui` geometry gets, and for the same reason: with the field gone there would otherwise be no -way left to clear a `description` an author had already saved. - -The three hand-written copies of the node and edge shapes that let this drift go unseen are -now one declaration each — `FlowNodeInspector`'s node and edge types alias the canonical -`FlowNodeLike` / `FlowDesignerEdge`, and `flow-decision-edges`' fourth edge copy aliases the -same canvas edge instead of restating it with a `condition?: unknown` that had already -outlived objectui#3202's narrowing by months. diff --git a/.changeset/6293-conditionbuilder-reference-value.md b/.changeset/6293-conditionbuilder-reference-value.md deleted file mode 100644 index 1bee446a33..0000000000 --- a/.changeset/6293-conditionbuilder-reference-value.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ConditionBuilder`'s row mode now compiles a value that is plainly a **reference** as one, -instead of quoting it into a string literal (objectui#6293). - -`fmtValue` quoted anything that was not a number / `true` / `false` / `null`, and the value -box is free text — so an author building "this field differs from its prior value", the idiom -that *defines* a change-detection predicate, got `previous == 'previous.status'`. That is -syntactically valid CEL, `previous` is a declared root, and a string literal's contents are -deliberately not scanned for references by `flow-ref-check` or by the server-side validator. -The predicate parsed, registered, evaluated — and was always false, with no author-time signal -at any layer, at all five surfaces that mount this builder. - -A value matching a **declared root prefix** now emits as the reference: `record`, `previous`, -`parent`, `user`, `current_user`, `org`. That set is this builder's own vocabulary — `record` -/ `user` / `org` are exactly what its subject dropdown offers one control to the left, -`previous` and `parent` are bound by `evalFieldPredicate` and by the server-side hook / -validation evaluators, and `current_user` is the ADR-0068 spelling of the same identity object -`user` names. Roots this builder never offers (`data`, `os`, `app`, `features`, `input`, -`vars`, `page`) are deliberately excluded: `data.csv` is a plausible literal and `data` *is* -bound, so capturing it would trade one silently-false predicate for another rather than for a -loud one. Declaring which roots a mounting surface actually binds is caller-supplied -vocabulary and belongs to objectui#6296. - -The test is "a dotted path under a declared root", not "contains a dot" — a version string -(`1.2.3`), a filename, and a path under an unbound root all stay literal text. The literal and -number controls are unchanged: `done` still compiles to `'done'`, `42` still to `42`. - -**Nothing already stored is rewritten.** A persisted `previous == 'previous.status'` no longer -round-trips byte-for-byte, so the builder's existing safety rule hands it to the raw CEL editor -rather than reinterpreting it — the author sees both readings and decides. In the other -direction a hand-authored `record.status != previous.status` now round-trips *into* the row -builder, which it could not before. - -The repair is at the authoring surface, where the ambiguity is: no consumer-side tolerance is -added, and the emitted reference is now an identifier the existing reference checkers can see, -where a string literal's contents were invisible to them. diff --git a/.changeset/6296-conditionbuilder-subject-vocabulary.md b/.changeset/6296-conditionbuilder-subject-vocabulary.md deleted file mode 100644 index 77b92ba6e0..0000000000 --- a/.changeset/6296-conditionbuilder-subject-vocabulary.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`ConditionBuilder` now takes a caller-supplied **subject vocabulary** instead of hardcoding a -record-scoped one (objectui#6296). - -The builder built every row subject as `record.` + field name, plus a fixed `record.id` / -`user.*` / `org.*` context list. That is correct for all five files that mount it today — six -mount sites, since `ActionDefaultInspector` mounts it twice — because every one of them is a -record-scoped site. It is wrong for a **flattened**-scoped site such as the flow designer's -entry condition, where the trigger record's fields *are* the top-level evaluation context -(bare `status`) and the prior values arrive as `previous.FIELD`. This repo's own -`flow-scope.ts` already computes that distinction (`fieldPrefix: onStart ? '' : 'record.'`, -`includePrevious`), and objectstack's `packages/formula/src/validate.ts` defines the two -scopes. - -A new optional `subjects` prop declares what a site actually binds: - -- `fieldPrefix` — defaults to `'record.'`; `''` declares a flattened scope. -- `includePrevious` — also offer `previous.FIELD` per field, plus the whole-record `previous` - token, which is what makes the create-path idiom `previous == null` **selectable** rather - than something the author has to recall from help text. -- `context` — replace the context subjects, so a flattened site does not inherit `record.id`, - a root it does not bind. Offering it there would make this editor emit the one spelling its - own sibling ref-check flags as out of scope. - -Declared, never inferred: the component does not guess a site's scope from the value it is -handed. **A caller that declares nothing gets exactly the previous behaviour** — the option -list, the `record.` prefix on a compiled row, and the single-quoted value spelling are all -pinned positively against that default, so changing it fails rather than re-baselines. - -**Double-quoted string literals now round-trip into row mode.** `unfmtValue` stripped only -single quotes while `fmtValue` re-emitted only single quotes, so `status == "done"` could -never survive the builder's byte-for-byte adoption check and was handed to the raw CEL editor -— even though double quotes are what the entry-condition placeholder teaches and what every -shipped example flow uses. Each row now remembers the quote character it was parsed with and -re-emits that one, so the author's own spelling is preserved rather than normalised, and the -byte-for-byte safety rule is kept exactly as it was rather than loosened. Rows built in the -builder still emit single quotes, unchanged. - -Measured against the shipped corpus — every start-node entry condition in objectstack's -example apps plus the HotCRM example from objectui#6226 — row-mode adoption goes from 3/17 to -15/17. The two that remain on raw mode are `&&` mixed with a parenthesised `||` group: a -grammar limit of the row model, unrelated to subjects, and out of this card's scope. - -The component is not re-exported from the package index, so no external caller can pass the -new prop yet; the wiring that will (objectui#6226) is a separate card. Scored `minor` for the -added capability rather than `patch`, since the widening is real even while its only future -caller is in-repo. diff --git a/.changeset/6298-componentconfig-one-authority.md b/.changeset/6298-componentconfig-one-authority.md deleted file mode 100644 index 21d49637bb..0000000000 --- a/.changeset/6298-componentconfig-one-authority.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor ---- - -`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it - -`@object-ui/types` and `@object-ui/core` each published a declaration of -`ComponentConfig`, so an auto-import picked between two different types by -alphabetical order. After the `ComponentMeta` convergence the remaining -difference was genericity and the `component` slot: `@object-ui/types`' was -non-generic with `component: any`, core's was `` with -`component: ComponentRenderer`. - -`@object-ui/types`' declaration gains that type parameter, **defaulted**, so -every existing spelling keeps its meaning exactly — bare `ComponentConfig` is -`ComponentConfig`, whose `component` is `any`, as before. `@object-ui/core` -re-exports it instead of declaring its own. - -The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`, -`deprecated`) were not dropped: they moved to a named extension, -`RegistryComponentConfig`, which is what `Registry.getConfig`, -`getAllConfigs` and `getNamespaceComponents` return. Those return values are -type-identical to what they returned before, so every read path is unchanged. - -**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core` -*and* touches one of those five registry-only keys through that annotation must -switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig` -no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's -versioning policy: objectui's own breaking changes ship as `minor` with the break -spelled out here, because the whole publishable set is one changeset `fixed` group -pinned to `@objectstack`'s major. diff --git a/.changeset/6301-packaged-automation-setup-page.md b/.changeset/6301-packaged-automation-setup-page.md deleted file mode 100644 index f8b9ddb079..0000000000 --- a/.changeset/6301-packaged-automation-setup-page.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Setup gains a **Packaged automation** page — the operational surface for the flows an -installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other -framework-contributed Setup surface is: the page registers the component-registry ref -`automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it -at `/apps//component/automation/packaged`. No bespoke route is added — a second way -in would be a URL the app metadata does not know about. - -Per packaged flow the page does exactly two things: - -- **on/off for this scope** — reads the activation state the engine reports - (`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation` - ledger) and flips it through `POST /api/v1/automation//toggle`; -- **clone** — `POST /api/v1/automation//clone` with a mandatory new machine name and - label (§7.1). The carried-over definition is never offered as editable form fields; the - copy is edited in Studio like any other flow. - -Authoring stays in Studio. The list is scoped to packaged flows by the server's own -three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the -`_packageId`-only shortcut, which classifies a tenant overlay bound to a package as -packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an -install-wide switch. - -**Server refusals reach the operator verbatim** — no client-side softening or rewording. -Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose -message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard -(409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a -list nothing on the client could reconstruct), and the §7.1 clone name conflict (409). - -⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no -base-moved notice, no link from a clone back to its source. Cloned-without-disabled and -disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including -the case where a response carries a `clonedFrom` key anyway — the platform does not track -that lineage, so a page that displayed it would be displaying something it invented. - -`@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real -translations in all nine other packs, matching each pack's existing vocabulary for "flow" -and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket, -fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and -each one's quotation convention. The group deliberately carries no drift or ancestry -wording, and no server refusal text: those arrive as server-authored prose and are -rendered verbatim. diff --git a/.changeset/6302-aggregate-filter-lowering.md b/.changeset/6302-aggregate-filter-lowering.md deleted file mode 100644 index c10dcd9616..0000000000 --- a/.changeset/6302-aggregate-filter-lowering.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`ObjectStackAdapter.aggregate()` lowers rule-shaped filter arrays before the -analytics wire, reusing the lowering `find()` already runs (objectui#6302). - -`find()` has translated `[{ field, operator, value }, ...]` into the server's -filter AST for as long as `convertQueryParams` has existed. The analytics path -did not: `aggregate()` assigned `payload.where = params.filter` verbatim and -posted it to `/analytics/query`. - -The two doors are not equally forgiving, so the gap had a user-visible end. -`lowerAnalyticsWhere` in `@objectstack/service-analytics` — shared by both -aggregation strategies — accepts AST tuples and throws on an array of rule -objects. A stored `ViewFilterRule[]` that a LIST renders correctly therefore -rendered `element:number` into its error state on every analytics-capable -deployment, which is the default one because the CLI always loads analytics. - -An array filter now goes through the same `translateFilterArray` the `find()` -path uses — one lowering, so the two paths cannot disagree about one stored -filter. Rules spread into a logical node (`['and', ...rules, ...tuples]`, the -commonest composite there is) are lowered at depth, as they already were on -`find()`. Non-array filters are untouched: the MongoDB-style object this branch -was written for is what `/analytics/query` already accepts, and translating it -would be a semantic change this fix does not make. Already-AST arrays, -record-shaped filters, and the no-filter case are byte-unchanged. diff --git a/.changeset/6306-action-icon-type-resolution.md b/.changeset/6306-action-icon-type-resolution.md deleted file mode 100644 index 741c32c49f..0000000000 --- a/.changeset/6306-action-icon-type-resolution.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/components': patch ---- - -An `action:icon` hosted by an `action:bar` now reaches its handler. It forwarded the -COMPONENT id as the action type, so the click resolved nothing at all — no error, no -toast, a button that silently did nothing (objectui#6306, the objectstack#2169 "Mark Done -does nothing" shape). - -`action:bar` does not route members through `SchemaRenderer`. It pulls each member's -renderer off the registry and RENAMES the declared type as it spreads it onto the child: -`type` becomes the component id (`'action:icon'`) and the real declaration moves to -`actionType`. `action:button` has always resolved that pair when it forwards -(`schema.actionType || schema.type`); `action:icon` read `schema.type` alone and dropped -`actionType` entirely. `ActionRunner.execute` resolves its handler from -`action.type || action.actionType || action.name`, and `'action:icon'` binds no registered -handler and no builtin — for a declaration carrying `target` rather than `endpoint` it does -not reach the legacy `navigate`/`api` fallback either, so it fell through to -`executeActionSchema` and the authored action never ran. - -**The bug was a function of the layout, not the declaration.** One authored action executed -or did nothing depending on which `component` the host picked for it — the same asymmetry -objectui#5493 fixed on this renderer for `onSuccess`, one key over. - -`check:action-forward-parity` could not have caught this and its green run was never -evidence: `type` **is** in the forward whitelist, and that gate diffs key PRESENCE against -the owed set. This is a wrong-VALUE defect behind a present key, a class the gate has no -opinion on by construction. The existing icon coverage could not catch it either — it -rendered `action:icon` bar members three times and asserted only `visible`/`enabled`, never -that a click reached a handler, which is exactly how this shipped. - -Pinned by `action-bar-member-type-resolution.test.tsx`, which executes clicks rather than -inspecting props. Every row that reads the icon member's zero renders a sibling -`action:button` member of the SAME declaration in the SAME bar and reads its one first, so -a zero cannot be "the harness never executed anything". One row registers a trap handler -keyed on the component id, making the unfixed behaviour a positive artefact (the trap -fires) rather than only a missing call. A standalone row stays green in both worlds on -purpose: it refuses a "fix" written as `schema.actionType` alone, which would trade this -defect for its mirror image on the surface where `type` IS the action type. - -Scope is this one renderer. `type: schema` appears in exactly two files under -`renderers/action/` — `action-button.tsx` (already correct) and `action-icon.tsx`; -`action:group` and `action:menu` compose their members differently and are untouched. diff --git a/.changeset/6307-bold-version-literals.md b/.changeset/6307-bold-version-literals.md deleted file mode 100644 index 9b3e4f62ca..0000000000 --- a/.changeset/6307-bold-version-literals.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Docs and gate change; no published surface. - -Two consumer-facing guides stated Node/pnpm floors this project neither declares nor -tests: `content/docs/guide/quick-start.md` carried `**Node.js** 20+` / `**pnpm** 9+ or -npm/yarn`, and `content/docs/guide/building-crud-app.md` the same pair on one line. Both -pages address the reader's OWN project, so the root `engines` field never governed them, -and no published package supplies a floor either — of the 46 workspace manifests, zero -declare `engines.node` or `engines.pnpm` (the only `engines` block outside the root is -`packages/vscode-extension`'s `engines.vscode`). The pages now state what is measurable -instead: the packages are built and tested on Node 22.x with pnpm 10.x — 26 of the 27 -`node-version:` declarations in `.github/workflows` read `'22.x'` and the 27th reads -`'22'`; the root `packageManager` field is `pnpm@10.31.0`, which is what `corepack enable` -hands every CI job. Worded as what CI exercises, not as a requirement the project has not -measured. - -The reason nothing objected: `scripts/__tests__/doc-version-claims.test.ts` scans -`content/docs` for exactly this, and the `SEP` character class between a toolchain name -and its version admitted backticks, quotes, whitespace, colons, commas, pipes and brackets -— but not `*`. So `**Node.js** 20+` never matched `TOOLCHAIN + SEP + VERSION`, and the -ratchet reported green over four literals it had never examined. `SEP` now admits `*` and -`_`; measured over the 241 files the three scan roots resolve to, the corpus goes from 33 -matched literals to 37 — exactly those four, none lost. None of the four was ledgered: -they were deleted, which is what the gate's own failure message asks for when a literal -restates no manifest and no lane tests it, and the sentences replacing them are inventoried -as `anchored` entries naming the anchor each can be re-measured against. - -A new fixture test keeps the widening measurable now that the repaired corpus carries no -emphasised claim at all, and pins the one boundary it does not cross: `_Node.js_ 20+` stays -invisible, because `_` is a word character and the `\b` on each side of the toolchain name -therefore fires on neither side of it (measured: zero such spellings in the corpus today). - -objectui#6307. diff --git a/.changeset/6308-living-workflow-example.md b/.changeset/6308-living-workflow-example.md deleted file mode 100644 index 25ffccc64a..0000000000 --- a/.changeset/6308-living-workflow-example.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Docs change; no published surface. - -`content/docs/guide/ci-cd-pipeline.md`'s "Adding a New Workflow" section told a -contributor to "follow the existing pattern for pnpm + Turbo setup" and then gave a copied -YAML block in which every line had drifted: `actions/setup-node@v4` where every workflow -in this repository now uses `@v7`, a hardcoded `node-version: 20` where every workflow -declares `'22.x'` (and 20 sits below the floor the root `package.json`'s `engines` field -now declares), and `pnpm/action-setup@v4`, which no workflow here has ever used — pnpm -comes from `corepack enable` plus the root `packageManager` field instead. Re-measured on -this change's own HEAD: `actions/setup-node@v7` — 28 occurrences, no other version; -`node-version:` — 27 `'22.x'` and 1 `'22'`; `pnpm/action-setup` — 0. - -A copied block is a fossil by construction, so the section now points at -`readme-exports.yml` as a living example instead of repeating one: it is short, runs on -every pull request, and its setup is the complete pattern most new build/test/lint -workflows need (checkout, `corepack enable`, `actions/setup-node` with pnpm's cache, -`pnpm install --frozen-lockfile`, then a `turbo run build` step). Only the two steps that -hold regardless of which Node or pnpm version the repository is on — the checkout step and -`corepack enable` — stay quoted on the page; the reader copies the version-specific steps -from the workflow itself. No new version literal was introduced, so no -`doc-version-claims.test.ts` `KNOWN_CLAIMS` entry was needed. - -objectui#6308. diff --git a/.changeset/6309-delete-object-fields-bridge.md b/.changeset/6309-delete-object-fields-bridge.md deleted file mode 100644 index 18804b6669..0000000000 --- a/.changeset/6309-delete-object-fields-bridge.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/types': patch -'@object-ui/fields': patch ---- - -Delete the dead `metadata-admin/previews/object-fields-bridge.ts` module, and the three -prose references that still described it as wired. - -The module exported `bridgeFromDraft`, `commitToDraft` and `FieldsBridgeResult` and had -**zero importers** — re-measured on the merged base, not inherited from the filing. Nothing -in the repository could reach it either: `@object-ui/app-shell`'s `exports` map declares -only `.` and `./styles.css`, so the file was not addressable as a deep import even from -outside the workspace. - -Removing it is not the whole change. Three comments — in `types/src/designer.ts`, `types`' -`designer-field-types.test.ts` (twice) and `fields`' `richtext-cell-renderer-5452.test.tsx` -— cited the bridge as a live corroborating source. Left behind, they would have swapped -dead code for false documentation: three in-repo pointers telling a future reader that this -bridge mediates between the framework field record and `FieldDesigner`, and nothing telling -them it is unreachable. The two that named it as the consumer deriving an editable-subset -check from `DESIGNER_FIELD_TYPES` now name `MetadataFieldsPage`, which does exactly that -with the same idiom and the same `objectui#3017` anchor. The third cited the bridge's -`richtext` → `html` mapping as one of three corroborations that `richtext` stores HTML; the -other two (the showcase seed and the field-type decision tree) are live and carry the point -on their own, so that clause is dropped rather than repointed. - -No behaviour changes: nothing imported the module, so there is nothing to migrate. diff --git a/.changeset/6310-designer-formula-key-retired.md b/.changeset/6310-designer-formula-key-retired.md deleted file mode 100644 index 905322ab4c..0000000000 --- a/.changeset/6310-designer-formula-key-retired.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/plugin-designer': patch ---- - -`appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten -locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310). - -objectui#6043 retired the Field Designer's formula-expression textarea, which was the -key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label: -t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in -eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in -`packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner > -fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. - -Removed under objectui#4658's evidence standard, re-measured on this branch rather than -inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could -reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its -sole textual occurrence anywhere in the repo was the defaults-map row this change removes -with it — so the key goes from NEEDS-REVIEW to no footprint at all. - -The map and all ten packs move in one commit, which is what keeps -`defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack -lacks, and `all-locales-key-parity` fails a pack left behind. - -Not touched: `designer.field.formula` (`'Formula (CEL)'`) in -`packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging -to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula -expressions. - -`packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the -removal by name, following the four prior retirements (objectui#4145, objectui#4392, -objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can -see a dead key come BACK into the packs: the reverse sweep that found this one is report-only -by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and -`check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted — -reviving the row in all ten packs turns exactly that one case red, naming each pack, while the -parity gate and the defaults-map mirror stay green. diff --git a/.changeset/6313-quick-reference-node-floor.md b/.changeset/6313-quick-reference-node-floor.md deleted file mode 100644 index c67eeeddbc..0000000000 --- a/.changeset/6313-quick-reference-node-floor.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Tooling-only fix (objectui#6313): `QUICK_REFERENCE.md`'s "Current Release" pin -(`scripts/__tests__/quick-reference-current-release-4143.test.ts`) and the sync -generator (`scripts/sync-quick-reference-release.mjs`) derived the Node/pnpm -version floors with `match(/(\d+)/)?.[1]`, which keeps only the LEADING integer -group. Against root `engines.node: ">=22.11"` (objectui#5306 / PR #6311) that -produced a floor of `22`, silently discarding the `.11` — so a row reading -exactly `≥ 22` passed a pin whose whole premise is disagreeing with its own -cited anchor. Both derivations now strip the comparator and keep the WHOLE -version string; `pnpm quick-reference:sync` regenerated the Node.js row to -`≥ 22.11`. Added regression coverage pinning that a `≥ 22`-shaped row is now -rejected and that a `≥ 220`-shaped row still is (objectui#4913), and corrected -the test file's docblock, which had gone stale on the same anchor one decimal -place up. No published package source changed. diff --git a/.changeset/6316-docs-route-eager-closure-gate.md b/.changeset/6316-docs-route-eager-closure-gate.md deleted file mode 100644 index d39e34ce12..0000000000 --- a/.changeset/6316-docs-route-eager-closure-gate.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Tooling-only change; no published behaviour changes. The docs route's eager closure now has -an instrument. `check:eager-closure` reads `apps/console/dist/eager-closure.json` and weighs -the console, so the budget objectui#4616 set over `/docs/[[...slug]]` — the route every one -of the docs pages shares, and the one `apps/site/app/components/registerCatalogBlocks.ts` -adds side-effect imports to — was governed by nothing, and its only measurement was -reconstructed by hand, once. `pnpm check:docs-route-closure` weighs it structurally instead -of in bytes (objectui#6316, triage ruling shape 2, so no docs-site build in CI): every -package the registrar names must be already reachable from that route's static module graph -— a declaration and no payload, which the gate proves for `@object-ui/plugin-form` and -`@object-ui/plugin-grid` through `@object-ui/plugin-view` — or recorded in the script's -`MEASURED_PAYLOAD` with what it is for. Anything else is a genuinely new graph, and it fails -so that a human argues for it in review. diff --git a/.changeset/6318-code-editor-bar-chart-zod.md b/.changeset/6318-code-editor-bar-chart-zod.md deleted file mode 100644 index 96b8ed1c72..0000000000 --- a/.changeset/6318-code-editor-bar-chart-zod.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/types": minor ---- - -Model `code-editor` and `bar-chart` in `AnyComponentSchema`, and repair three catalog fixtures - -Both types render — `@object-ui/plugin-editor` registers `code-editor`, -`@object-ui/plugin-charts` registers `bar-chart` — and neither had a Zod member, -so `safeValidateSchema` (and therefore `objectui validate`) refused every -document that named them, whatever the document said. `CodeEditorSchema` and -`BarChartSchema` are now declared in `@object-ui/types` and mirrored in -`@object-ui/types/zod`, derived key-for-key from what the two renderers -demonstrably read rather than from a view of what either component ought to -accept. - -Alongside them, three `examples/schema-catalog` entries that were wrong about -their own renderer: `basic-select`'s third option spelled its label `type`, so -the option rendered blank; `icon-toolbar`'s buttons carried only `icon`/`value`, -which `button-group` never reads, so all three rendered blank; and `basic-tabs` -gave its items no `value` and no `defaultValue`, so no panel could be selected. diff --git a/.changeset/6320-check-nested-dist-ignore.md b/.changeset/6320-check-nested-dist-ignore.md deleted file mode 100644 index 9f99ca1fc5..0000000000 --- a/.changeset/6320-check-nested-dist-ignore.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -Fix `objectui check` scanning build output because its ignore list only excluded a -root-level `dist/` / `node_modules/` (objectui#6320). - -`packages/cli/src/commands/check.ts` passed `ignore: ['node_modules/**', 'dist/**', -'.git/**']` to `globSync`. `glob` matches `ignore` patterns against the path relative to -`cwd`, so an unanchored `dist/**` / `node_modules/**` excludes only a directory of that -name at the scan root — every nested `packages//dist/`, `examples//dist/`, -`apps//dist/` (and their `node_modules/`) was still scanned. In a built workspace -this means `objectui check` re-reads the author's own schemas a second time from build -output, roughly doubling every count it reports (measured on this repository: 617 → 1047 -files globbed after a full build) with nothing in the output explaining why. - -The ignore patterns are now anchored at every depth (`'**/dist/**'`, `'**/node_modules/**'`), -matching the fix's stated intent: exclude build output and installed dependencies wherever -they live, not only at the project root. A root-level `dist/` / `node_modules/` remains -excluded, unchanged. - -Confirmed before widening: no example, template, or docs fixture in this repository -authors a schema under a directory literally named `dist` — the widened pattern excludes -only generated content. diff --git a/.changeset/6321-changeset-guard-self-coverage.md b/.changeset/6321-changeset-guard-self-coverage.md deleted file mode 100644 index 25123a3f24..0000000000 --- a/.changeset/6321-changeset-guard-self-coverage.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Internal CI-only change: `changeset-guard.yml`'s `paths:` filter now includes its own -YAML and `scripts/check-changeset-no-major.mjs`, so a change to the gate is exercised by -the PR that makes it instead of by the next unrelated `.changeset/**` PR (objectui#6321). -`content/docs/guide/ci-cd-pipeline.md` is updated to match. No published package changed. diff --git a/.changeset/6329-actionparam-one-authority.md b/.changeset/6329-actionparam-one-authority.md deleted file mode 100644 index ac3ed137ee..0000000000 --- a/.changeset/6329-actionparam-one-authority.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ActionParam` has one authority again (objectui#6329). The name was declared three times, -not twice as the card counted: `@object-ui/types` publishes it — derived from the spec's -`ActionParamSchema` input, with its own parity suite — and `ActionDefaultInspector.tsx` and -`ActionPreview.tsx` each carried a module-local `interface` of the same name. app-shell -already read the published one elsewhere (`utils/resolveActionParams.test.ts`), so both -locals were shadows. They are deleted, not reconciled against each other, under the -2026-08-25 family ruling 甲A1. - -Neither shadow needed a member the published type lacks, so nothing was added to the -published surface. What the shadows got wrong was the DECLARATION, in the direction that -lets wrong metadata compile: - -- The inspector's copy carried `[k: string]: unknown`. An index signature admits every key - at type `unknown`, so a commit of a key `ActionParamSchema` rejects by name — `.strict()`, - and `referenceTo` is listed in its alias map — type-checked here and failed on save. It - also made the two copies look compatible when they were describing different authoring - surfaces: `options` / `helpText` / `defaultValue` were declared outright on one side and - swallowed as `unknown` on the other. -- The preview's copy declared `label?: string | { en?: string }`, admitting the `en` tag and - no other, while its own `localize` helper has always read `Object.values(o)[0]`. An inline - locale map keyed `fr-FR` rendered correctly and failed `tsc`. The published `I18nLabel` - admits both authorized forms, so the type now matches what the code already did — this - widens the declaration, not the runtime's acceptance. - -Two behaviour-visible consequences, both of them the local `type?: string` being withdrawn -in favour of the published `ResolvableParamFieldType` (the spec's 49-member `FieldType` plus -objectui's three declared param aliases): - -- `ActionPreview.renderFieldMock` no longer branches on `long_text` or `integer`. Neither is - in that vocabulary — `long_text` belongs to the console form-builder dialect and `integer` - to JSON Schema — so a param spelled either way is a parse rejection on the server and - could never have reached the preview. The two comparisons compiled only because the local - copy typed `type` as `string`. -- The inspector's param-type dropdown narrows its commit through the runtime witnesses - `@object-ui/types` exports (`ACTION_PARAM_FIELD_TYPES` + `OBJECTUI_LOCAL_PARAM_FIELD_TYPES`) - rather than writing the raw DOM string. An unrecognised spelling clears the key instead of - being written into metadata the server would refuse; the eight offered spellings are - unaffected, and are now checked against the vocabulary at compile time. diff --git a/.changeset/6331-flow-node-label-producer-census.md b/.changeset/6331-flow-node-label-producer-census.md deleted file mode 100644 index b1d4df1cac..0000000000 --- a/.changeset/6331-flow-node-label-producer-census.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Pins the flow-node `label` at the PRODUCER, where a cast cannot bypass it -(objectui#6331), and records why the reader-side type stays optional so the -question is not re-opened a third time. - -`FlowNodeSchema` requires `label`; the designer's `FlowDesignerNode` makes it -optional, and that is a deliberate layer difference — a canvas holds nodes the -user has dropped but not finished. Both hold only if something guarantees a node -ACQUIRES a label before save. A census of every construction path (canvas -palette/append, insert-on-edge, the one-click revision loop, the empty-flow -seed, the Studio "New flow" skeleton, plus the nested-region and drag writers, -which re-spread an existing node) found six node literals across five producer -call sites and every one already writing a label — but three of the five were -enforced only by accident. Measured on `origin/main` by deleting the `label` -line from `insertOnEdge` and from `FlowPreview.handleAddNode`: vitest green -(58/58), `tsc` exit 0, `eslint` exit 0 with zero errors. - -`flow-node-producers.label.test.tsx` closes that in two halves — each producer -driven through its real affordance with the EMITTED node `safeParse`d whole -against the spec (never a hand-composed node, which is what let the existing -seed test stay green through the ablation), plus a source census that refuses a -node literal writing no label, so a producer added later lands red there instead -of at the author's save. - -No runtime behaviour changes; the published types are untouched. diff --git a/.changeset/6332-explain-request-adoption.md b/.changeset/6332-explain-request-adoption.md deleted file mode 100644 index b6952b279c..0000000000 --- a/.changeset/6332-explain-request-adoption.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Type-only: `useRecordCrudVerdicts` now builds its `POST /api/v1/security/explain` -body against `@objectstack/spec/security`'s `ExplainRequest` instead of an -untyped object literal, and `RecordCrudOperation` is now a *declared* subset of -the spec's eight-verb `ExplainOperation` rather than a coincidental one. - -No release: `satisfies` and the subset wrapper are erased at compile time, the -emitted JavaScript is unchanged, and `RecordCrudOperation` is not part of -`@object-ui/plugin-grid`'s public `.d.ts` (verified — it does not appear in -`dist/index.d.ts`). - -What it buys is a class of compile errors the untyped literal accepted: -a renamed or mis-cased request key (`recordIDs`, `objectName`) and a verb the -explain API does not accept are now `tsc` failures at the call site instead of -a `400 VALIDATION_FAILED` — or, for a mis-cased `recordIds`, a request the -server reads as "no ids at all". Pinned at compile time in -`useRecordCrudVerdicts.explainRequest.test.ts`. - -The narrowing at the heart of the hook is deliberately preserved: the two kebab -verbs stay written out locally, so an upstream release that adds a ninth verb -cannot widen what this list asks about. Only the request side is adopted — the -response stays `unknown` on purpose, because asserting the spec's entry type -would make the hook's fail-open runtime guards look like dead code. diff --git a/.changeset/6335-starred-apps-nav-exclusion.md b/.changeset/6335-starred-apps-nav-exclusion.md deleted file mode 100644 index d56cbe2400..0000000000 --- a/.changeset/6335-starred-apps-nav-exclusion.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`StarredApps` now filters out `nav`-type favorites before rendering (objectui#6335). - -`FavoritesProvider` has always documented `nav` favorites (sidebar entries pinned via -the in-tree pin toggle) as "Excluded from Home/Starred and from the generic sidebar -Favorites list so it doesn't render twice" — but `StarredApps` filtered nothing by type, -so a `nav` favorite handed to it rendered anyway. `FavoriteItem['type']` also has no -`nav` key under `home.recentApps.itemType.*` (only five of its six members do), so a -`nav` card that reached `StarredApps` fell through to the raw `"nav"` label instead of a -translated one. - -`StarredApps` now filters `items` to `type !== 'nav'` before rendering — the same -exclusion already applied to the sidebar Favorites list in `AppSidebar.tsx` and -`UnifiedSidebar.tsx` — restoring declared-equals-enforced behaviour and making the -missing locale key correct by construction. No locale packs were touched, and the -`FavoriteItem` / `RecentItem` union types are unchanged. diff --git a/.changeset/6336-changeset-overwrite-guard.md b/.changeset/6336-changeset-overwrite-guard.md deleted file mode 100644 index 4dd585854d..0000000000 --- a/.changeset/6336-changeset-overwrite-guard.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Internal CI-only change: a new report-only `Changeset Overwrite Report` job in -`changeset-guard.yml` names any `.changeset/*.md` a change modified or deleted -without having added it, and prints the release declaration that was there. - -No published package changes, so this releases nothing. - -objectui#6336 — a hand-picked `changesets`-style filename can land on one that -already exists, and the overwrite is silent in both directions that should catch -it: `git status` reports ` M` rather than `??`, and a deleted release declaration -is flagged by nothing downstream. The cost lands on a third party — whichever -earlier pull request's declaration disappears — and surfaces only when a package -fails to bump. - -Report-only is measured, not cautious: across all 5281 first-parent commits on -`main`, every one of the 19 modifications of a pre-existing changeset was -legitimate, so a blocking gate would have failed all of them. -`OS_CHANGESET_OVERWRITE_ENFORCE=1` flips it for whoever revisits that with a new -measurement. diff --git a/.changeset/6342-dead-migration-guide-pointers.md b/.changeset/6342-dead-migration-guide-pointers.md deleted file mode 100644 index a4a89c9c2a..0000000000 --- a/.changeset/6342-dead-migration-guide-pointers.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Two deprecation warnings pointed at `MIGRATION_GUIDE.md`, a file deleted from the -repository in `8c5d20455` (objectui#6342). - -`Registry.register()`'s missing-namespace warning now points at the live docs page -that documents namespaced registration -(`/docs/guide/plugin-development#namespaced-registration`) instead of the deleted -guide. `ValidationEngine`'s function-based-condition warning drops its `See:` line -entirely: the deleted guide covered component namespaces and lazy field -registration and never documented conditions at all, so that pointer was -misdirected as well as dead, and the warning already carries the complete -before/after migration inline. - -Both are console messages shipped to application developers, so neither can use -the immutable `git show ^:` provenance form objectui#6275 used for a -docblock — a reader of the npm package has no repository to run it against. diff --git a/.changeset/6347-button-group-schema-block.md b/.changeset/6347-button-group-schema-block.md deleted file mode 100644 index 9404a64884..0000000000 --- a/.changeset/6347-button-group-schema-block.md +++ /dev/null @@ -1,82 +0,0 @@ ---- ---- - -Docs only, publishes nothing: `content/docs/components/basic/button-group.mdx` -published a `ButtonGroupSchema` / `ButtonGroupButton` surface that disagreed -with the shipped types in **both** directions. objectui#6347 named the two -over-statements; re-measuring the whole fence against -`packages/types/src/navigation.ts`, `packages/types/src/base.ts` and their Zod -mirrors found eleven rows. - -Over-stated — documented, never declared: - -| page before | shipped declaration | page after | -| --- | --- | --- | -| `ButtonGroupSchema.value?: string \| string[]` | not declared (`navigation.ts:335-351`, `zod/navigation.zod.ts:154-159`) | removed | -| `ButtonGroupSchema.selectionMode?: 'single' \| 'multiple' \| 'none'` | not declared (same two blocks) | removed | -| `ButtonGroupButton.value: string` (required) | not declared (`navigation.ts:305-330`, `zod/navigation.zod.ts:142-149`) | removed | -| `ButtonGroupButton.icon?: string` | not declared (same two blocks) | removed | -| `buttons: ButtonGroupButton[]` (required) | `buttons?: ButtonGroupButton[]` (`navigation.ts:340`, `.zod.ts:156`) | `buttons?:` | -| `label?: string` | `label: string` — required (`navigation.ts:309`, `.zod.ts:143`) | `label:` | - -Under-stated — declared, never documented: - -| page before | shipped declaration | page after | -| --- | --- | --- | -| `ButtonGroupButton` had no `variant` row | `variant?: 'default' \| 'secondary' \| 'destructive' \| 'outline' \| 'ghost' \| 'link'` (`navigation.ts:313`, `.zod.ts:144`) | added | -| `ButtonGroupButton` had no `size` row | `size?: 'default' \| 'sm' \| 'lg' \| 'icon'` (`navigation.ts:317`, `.zod.ts:145`) | added | -| `ButtonGroupButton` had no `onClick` row | `onClick?: () => void` (`navigation.ts:325`, `.zod.ts:147`) | added, annotated as a runtime slot | -| `ButtonGroupButton` had no `className` row | `className?: string` (`navigation.ts:329`, `.zod.ts:148`) | added | -| `ButtonGroupSchema.variant?: 'default' \| 'outline' \| 'ghost'` | six members (`navigation.ts:345`, `.zod.ts:157`) | `secondary`, `destructive`, `link` restored | -| `ButtonGroupSchema.size?: 'sm' \| 'default' \| 'lg'` | four members (`navigation.ts:350`, `.zod.ts:158`) | `icon` restored | -| `disabled?: boolean` | `BaseSchema.disabled?: boolean \| string` (`base.ts`, `zod/base.zod.ts:190`) — `ButtonGroupSchema` does **not** redeclare it | `boolean \| string` | - -Both directions are one defect class with the sign flipped. Fixing only the -direction a card happens to notice is what left these omissions sitting beside -two earlier corrections to the same page (objectui#6132, objectui#6143). - -The `disabled` row deserves its own note, because it looks like a convention and -is not. Fourteen `content/docs/components/**` pages spell a component schema's -own `disabled` as `boolean` — but **thirteen of those fourteen schemas redeclare -`disabled?: boolean` themselves** (`ButtonSchema`, `SelectSchema`, -`SwitchSchema`, `ToggleGroupSchema` and nine more), so those pages are right. -`ButtonGroupSchema` is the one that does not, so it inherits `BaseSchema`'s -`boolean | string` and this page was the outlier rather than the convention. -Whether `ButtonGroupSchema` *should* narrow it like its thirteen siblings is a -types question, not a docs one, and is deliberately not answered here — the -renderer reads neither spelling today. - -`onClick` is documented as declared and annotated as a runtime slot: it is -`z.function()`, and objectui#4453 narrowed the runtime to -`typeof === 'function'`, so a JSON author cannot supply one. - -**`## Selection Mode` is deleted, not softened.** -`packages/components/src/renderers/basic/button-group.tsx` implements no -selection behaviour at all — no `selectionMode` read, no group-level `value` -read, no state — and it does not wire the declared per-button `onClick` either. -It maps `schema.buttons` to `Button` elements reading `variant`, `size`, -`className` and `label` only. A heading over two inert demos taught a -capability nothing draws. The capability question itself is filed separately -rather than answered here. - -The two catalog fixtures the section rendered -(`components-basic-button-group/single-selection`, `/multiple-selection`) are -**kept**. Catalog entries are a separate verification population, fenced off by -PR #6345; orphaning them from this page is the accepted cost, and no gate reads -a catalog entry's page references (the index is generated from the schema -directory by `scripts/regenerate-catalog-index.py`, path-keyed). - -`packages/types/src/__tests__/button-group-doc-surface-6347.test.ts` pins the -result. Nothing in CI parses a `plaintext` fence — `check:doc-types` reads only -the `type` string literals and `check:doc-snippets` compiles `ts`/`tsx` fences — -so without it a green CI run would have said "nothing else broke", not "the -correction is right". The pin reads membership off the mirror's `.shape` rather -than off parse acceptance, because `BaseSchema` is `.passthrough()` and carries -`[key: string]: any`: an undeclared `selectionMode` parses green and -type-checks, so acceptance cannot tell "declared" from "admitted unexamined". - -No type was minted, no fence moved (the page holds one `plaintext` fence before -and after, so objectui#5867's shrink-only declared population is unchanged), and -no catalog fixture was edited. - -Part of objectui#6347. diff --git a/.changeset/6349-name-authority-batch-3.md b/.changeset/6349-name-authority-batch-3.md deleted file mode 100644 index 3f4bb1e2f5..0000000000 --- a/.changeset/6349-name-authority-batch-3.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/plugin-grid': patch -'@object-ui/app-shell': patch ---- - -One authority per exported type name, batch 3 of objectui#6349: `ComboboxOption`, -`NamedActionDef`, `OrgTranslate`. - -**`@object-ui/components` — `ComboboxOption` now IS `@object-ui/types`' declaration.** -The component declared its own `{ value, label }`, a strict subset of the -`ComboboxOption` that `@object-ui/types` declares for `ComboboxSchema.options` and -mirrors in `form.zod.ts` (`{ value, label, disabled? }`). The component now re-exports -the types declaration (through the `@object-ui/types/form` subpath — the root barrel -does not publish the name), so the name `ComboboxOption` exported from -`@object-ui/components` gains the optional `disabled?: boolean` member. Every value -that type-checked before still does — nothing narrows and no key changes type; the -one thing that moves is `keyof ComboboxOption`, so a consumer that EXHAUSTS the type -(a `Record` over its keys) will need the new key. Note that the `Combobox` component -itself does not read `option.disabled` — that member was already declared on the -`@object-ui/types` face and is now visible on this one too; it is recorded as a -separate finding, not changed here. - -**`@object-ui/plugin-grid` / `@object-ui/app-shell` — internal, surface unchanged.** -`NamedActionDef` was declared identically in `resolveBulkActions.ts` and -`resolveLegacyRowActions.ts`; the latter is now the one authority and the former -re-exports it. `OrgTranslate` was declared identically in `orgErrorMessage.ts` and -`orgRoleLabel.ts`; the former is now the one authority and the latter re-exports it. -Neither name is on its package's public entry, and every deep-`dist` module still -exports the same name with the same shape. - -`FilterBuilderCondition` / `FilterGroup` (the other two names this batch was sized -with) are deliberately NOT converged: their shapes disagree on `id`, `value` and on -`operator`, and the only dependency-legal re-point would retype `operator` — the -vocabulary objectui#7561 is asking a maintainer to rule on. diff --git a/.changeset/6349-types-internal-name-collisions-batch-1.md b/.changeset/6349-types-internal-name-collisions-batch-1.md deleted file mode 100644 index 65c27667e9..0000000000 --- a/.changeset/6349-types-internal-name-collisions-batch-1.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Three exported type names inside `@object-ui/types` had two authorities each; each now has -one (objectui#6349, first batch — the three intra-package collisions from the 46-name -census on objectui#6273). - -**`ActionSchema` — renamed, because the two shapes are unrelated.** `crud.ts` and -`ui-action.ts` both declared it. Measured member-by-member they share 9 keys out of 28 each: -`crud.ts` `extends BaseSchema` and pins `type: 'action'` (a UI node — a button in a -component tree), `ui-action.ts` extends nothing and types `type` as `ActionType` (a spec-v2 -action definition, with `name`, `locations`, `params`, `target`). Re-pointing either at the -other would silently hand a consumer a different type, so this took the rename branch -(objectui#5044 is the precedent for choosing the surviving name). `ui-action.ts`'s -declaration is now spelled **`UIActionSchema`** — the name `src/index.ts` has always -PUBLISHED it under, via `export type { ActionSchema as UIActionSchema }`, which is now a -plain re-export. **The package's public surface is unchanged**: `ActionSchema` still means -`crud.ts`'s legacy shape and `UIActionSchema` still means `ui-action.ts`'s, exactly as -before. Nothing outside `ui-action.ts` imported the old spelling — there is no `./ui-action` -subpath in `exports`, so the old name was never reachable from outside the package. - -**`BreadcrumbItem` / `BreadcrumbSchema` — re-pointed, because one copy was stale.** Both -were declared in `data-display.ts` and in `navigation.ts`. The data-display pair was not a -second dialect but a strict SUBSET: no key declared differently on either side, and missing -`BreadcrumbItem.icon` / `onClick` / `siblings` and `BreadcrumbSchema.maxItems`. Everything -that reads a breadcrumb was already on the navigation declaration — `registry.ts` maps the -`'breadcrumb'` component type to it, `src/index.ts` re-exports it under the bare names, -`zod/navigation.zod.ts` mirrors it (`icon`, `onClick`, `siblings`, `maxItems` included), the -`ui:breadcrumb` renderer consumes it, and the component's own documentation page documents -`icon` and `maxItems`. `data-display.ts` now re-exports the one authority. - -**What changes for a consumer.** The `@object-ui/types/data-display` subpath is published, so -its `BreadcrumbItem` / `BreadcrumbSchema` and the `DataDisplaySchema` union's breadcrumb -member widen to the navigation declaration — they gain the four keys above. Nothing narrows -and no key changes type, so every value that type-checked before still does; what the subpath -now declares is what the renderer already honoured and the docs already described. Graded -`minor` because a published `.d.ts` member changes shape, per this repo's version-alignment -rule (never `major`). - -The three `KNOWN_COLLISIONS` lines came down in the same change; that baseline -(`scripts/__tests__/one-authority-per-exported-name-6273.test.ts`) is shrink-only and fails in -both directions, so converging without deleting them would have been red too. 43 entries → 40. diff --git a/.changeset/6349-types-internal-name-collisions-batch-2.md b/.changeset/6349-types-internal-name-collisions-batch-2.md deleted file mode 100644 index eb4a02d169..0000000000 --- a/.changeset/6349-types-internal-name-collisions-batch-2.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The last two exported type names with two authorities *inside* `@object-ui/types` now have -one each (objectui#6349, second batch — the remaining intra-package collisions from the -census on objectui#6273). Both took the **rename** branch, because in both cases the two -declarations are genuinely different types; the surviving spelling is in each case the name -`src/index.ts` has always published that declaration under, so **no importable name -changes** and the barrel alias becomes a plain re-export. - -**`MenuItem` — renamed to `AppMenuItem` in `app.ts`.** `app.ts` declared a flat, -all-optional `interface` (`type?: 'item' | 'group' | 'separator'`, `label`, `icon`, `path`, -`href`, `children`, `badge`, `hidden`) — the `@deprecated` legacy navigation item that -`AppComponentSchema.menu`, `AppAction.items` and `menuItemToNavigationItem` read. -`overlay.ts` declared a discriminated **union**, `MenuCommandItem | MenuDividerItem`, whose -command arm requires `label` and whose both arms **tombstone** `type` as `type?: never` -(objectui#6523) — precisely the key `app.ts` declares as a three-value enum. Re-pointing -either at the other would have made an authored `type: 'separator'` legal on one side and a -type error on the other, so the two names had to part. `@object-ui/types` continues to -publish overlay's union as `MenuItem` and app's interface as `AppMenuItem`, exactly as -before. - -**`ValidationFunction` — renamed to `FieldValidationFunction` in `field-types.ts`.** -`data-protocol.ts` declares `(value, context?: ValidationContext) => boolean | string`; -`field-types.ts` declared `(value) => boolean | string | Promise`. They -disagree at both ends of the arrow — different parameter lists, and a `Promise` return that -the data-protocol signature does not admit — so field-types' is not assignable to -data-protocol's in the direction that matters. `data-protocol.ts` had said so in prose ("may -differ from similarly named validation function types in other packages (e.g., in -`field-types`)") for as long as both existed. The published names `ValidationFunction` -(data-protocol's) and `FieldValidationFunction` (field-types') are unchanged. - -Both `KNOWN_COLLISIONS` lines come down in the same change; that baseline fails in **both** -directions, so converging without deleting them is red too. 38 entries -> 36. diff --git a/.changeset/6353-timeline-bare-key-ownership.md b/.changeset/6353-timeline-bare-key-ownership.md deleted file mode 100644 index 84d83034d6..0000000000 --- a/.changeset/6353-timeline-bare-key-ownership.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-timeline': patch ---- - -The bare `timeline` component key is now owned by DECLARATION rather than by -module-evaluation order (objectui#6353). - -`packages/plugin-timeline` registers the same short name twice — -`plugin-timeline:timeline` (`src/renderer.tsx`, the presentational renderer) and -`view:timeline` (`src/index.tsx`, the object-bound `ObjectTimelineRenderer`). Neither -passed `skipFallback`, so under `Registry.register` both also claimed the **bare** -`timeline` key and the last module to evaluate won it. `src/index.tsx` re-exports -`./renderer` (line 300) before its own `import` (line 307), so the presentational one -registered first and the object-bound one overwrote it. - -The resolved outcome was the intended one and **does not change here**: `type: -'timeline'` still renders `ObjectTimelineRenderer`, which delegates inward to the -presentational renderer. What changes is that it is now decided rather than inherited. -Reordering those two lines would previously have handed `type: 'timeline'` to the -presentational renderer, which reads none of the object-bound keys (`object`, `filter`, -`sort`, `limit`) — an authored timeline would have stopped fetching, with no error and -no failing test. The registry's own collision guard names this remedy in its warning -text; this applies it. - -`src/renderer.tsx` now registers with `skipFallback: true`, so only `view:timeline` -claims the bare key, in any evaluation order. The presentational renderer stays -reachable under its explicit `plugin-timeline:timeline` key, which is the lookup a -presentational host already uses — no consumer-visible resolution changes. - -`src/__tests__/timeline-bare-key-ownership.test.ts` is the half that outlives the fix: -it fails if the declaration is dropped, if a third registration starts claiming the bare -key, or if resolution becomes order-dependent again. It reads both registrations' real -declared metadata back out of the registry and replays them into a fresh `Registry` in -**both** orders, so order-independence is a property under test rather than a property -of the file the test happens to import. diff --git a/.changeset/6355-retire-timeline-timescale-alias.md b/.changeset/6355-retire-timeline-timescale-alias.md deleted file mode 100644 index 67cf90ac23..0000000000 --- a/.changeset/6355-retire-timeline-timescale-alias.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -'@object-ui/plugin-timeline': minor -'@object-ui/components': minor -'@object-ui/types': minor ---- - -Retire the `timeScale` alias on the timeline node — `scale` is the only axis spelling -(objectui#6355, maintainer ruling 2026-08-27). - -**BREAKING for authored metadata.** `timeScale` was this renderer's pre-spec spelling of the -Gantt axis bucket. `scale` is canonical — it is `@objectstack/spec` `ui/TimelineConfig.json`'s -axis key and the key the renderer preferred (objectui#6170 ruling, 2026-08-25: `timeScale` -goes the alias-retirement route, not a silent second spelling). objectui#6355's ruling -retires it immediately, with no phased window, while the project is at startup stage. - -**What breaks, and how you will find out.** A timeline document that spells `timeScale` is -now **refused**, loudly, at the authoring boundary: - -- `TimelineSchema.timeScale` is declared `?: never` — writing it is a type error; -- the Zod twin declares `z.never().optional()` — parsing a document that carries the key - fails with `invalid_type` / `expected: never` on the `timeScale` path. - -The fix is a rename: `timeScale` → `scale`. The accepted values are unchanged (`hour`, -`day`, `week`, `month`, `quarter`, `year`), so no value needs rewriting. - -**Why a tombstone rather than deleting the key.** `BaseSchema` is `.passthrough()` on the -Zod side and carries `[key: string]: any` on the TS side, so an *undeclared* key is accepted -unvalidated by both halves. Deleting `timeScale` outright would have let the retired spelling -parse green and type-check green while the renderer no longer read it — the Gantt axis would -silently fall back to the `month` default, the chart would change bucket, and nothing would -error. That is the silent axis breakage objectui#2942 closed, running in the other direction, -and it is the specific outcome this retirement is shaped to prevent. Keeping the key declared -as `never` on both halves is what makes the removal audible. Absent stays valid on both, so a -document that never wrote the alias is untouched. - -Also in this change: - -- `resolveTimelineScale` drops the `?? schema.timeScale` fallback read; its parameter narrows - to `{ scale?: unknown }`. -- The designer drops its deprecated `timeScale` input. The `scale` input already offers all - six buckets. -- `ObjectTimeline` now emits the resolved axis under `scale` when it composes the schema it - hands to the renderer. It previously wrote the alias, which would have made **every** - object-bound Gantt fall through to the `month` default the moment the fallback read went — - silently, since that is a composed schema no author ever sees. Writing `scale` after the - spread also restores the precedence the surrounding code intends: a `timelineConfig.scale` - now actually beats a flat `schema.scale`, where under the alias the resolver's - `scale ?? timeScale` ordering let the flat key win. -- The two in-repo authors are migrated in the same change: the schema-catalog - `gantt-style-timeline.json` fixture and the registration's own `examples.gantt` block. -- Docs drop the `timeScale` row and gain a retirement callout; - `packages/components/.../TIMELINE.md`'s Gantt table now documents `scale` with the full - six-value vocabulary it has accepted since objectui#2942 (its row still claimed three). - -Version note: `minor`, not `major`, per AGENTS.md §版本号策略 — objectui's major tracks the -`@objectstack` major and all publishable packages share one `fixed` group, so a breaking -narrowing is declared `minor` with the break spelled out here. diff --git a/.changeset/6357-basechema-bind-declaration.md b/.changeset/6357-basechema-bind-declaration.md deleted file mode 100644 index c2d8302b83..0000000000 --- a/.changeset/6357-basechema-bind-declaration.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-dashboard': patch ---- - -`BaseSchema` declares `bind`, the data-scope binding path, on both halves — the TypeScript -interface and its Zod mirror (objectui#6357). - -`bind` was read by ten production sites and declared by no schema shape. It resolved as `any` -through `BaseSchema`'s index signature and rode `.passthrough()` on the validator, while three -separate documents taught it as an authorable key of *every* node: this repo's own `AGENTS.md` -§4 ("Every node in the UI tree follows this shape (`@object-ui/types`)"), the published -agent-facing `skills/objectui/rules/protocol.md` ("Every UI component node MUST follow this -shape"), and `content/docs/fields/grid.mdx`. So the agent-facing protocol told authors to write -a key the published types did not know existed. - -The census chose the home rather than guessing it. Nine reads go through -`useDataScope(schema.bind)` — `list` and `tree-view` in `@object-ui/components`, and the -`object-*` widgets in `plugin-charts`, `plugin-dashboard` (×2), `plugin-grid`, `plugin-kanban`, -`plugin-list`, `plugin-timeline`. A tenth is `plugin-grid`'s `gridNeedsDataSource` predicate, -where a present `bind` is one of the escape hatches that makes a missing data-source adapter -legitimate. Two more sites destructure the key out so `SchemaRenderer`'s prop spread cannot -write `bind="data.revenue"` onto the DOM. Per-component declaration was measured and rejected: -it costs nine copies of one key and buys nothing extra, because neither half can refuse the key -on a non-reader either way. `placeholder` is the standing precedent for a cross-cutting key -declared on `BaseSchema` and honoured only by a subset. - -**Accept-set narrowing, on the value and not the key.** `bind: 42` type-checked and parsed green -before this change and is refused by both halves now. It only refuses what already crashed: -`useDataScope` is `(path?: string)` and resolves via `path.split('.')`, so a non-string `bind` -threw a `TypeError` at render time. Every `bind` authored in this repo is a string, and the -declaration is optional, so nothing that renders today stops. - -**What this does NOT change**, stated because the pin would otherwise be read as more than it is: -an *undeclared* key is still accepted by both halves, so this did not buy rejection of a -misspelling such as `bindTo` (objectui#5155 / objectui#6269 own that ceiling). And `data-table` -still does not call `useDataScope`, so a `bind` on it is still ignored and still renders a header -over an empty body with no error — a documented silent failure that this declaration neither -causes nor cures, since the key was accepted on every node before it existed. - -`ObjectPivotTable` drops its local `bind?: string`: its `PivotTableSchema & {…}` intersection -extends `BaseSchema`, so the member was a true duplicate. Two other local declarations are left -in place and ratcheted rather than removed — their containing types never reference `BaseSchema`, -so deleting the member would delete the declaration rather than inherit it. diff --git a/.changeset/6360-master-detail-declined-detail-config-hint.md b/.changeset/6360-master-detail-declined-detail-config-hint.md deleted file mode 100644 index e074fb8689..0000000000 --- a/.changeset/6360-master-detail-declined-detail-config-hint.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`object-master-detail-form` now renders a config hint naming `childObject` for a detail -collection whose child object never resolved, instead of `Loading columns…` forever -(objectui#6360). - -`MasterDetailForm` already declines to fetch the schema of such a detail (objectui#5940) -and returns the entry unresolved, which is correct — asking the data layer for an object -literally named `undefined` is what that guard removed. But the decline is precisely the -guarantee that the entry's columns can never arrive, and the render branch it fell into -read `!d.columns?.length ?

Loading columns…

`. The author was shown a -spinner-shaped message that was permanently, unfixably wrong, and that never named the -key they had to set. - -The `!d.childObject` case now takes its own branch, checked **before** the columns arm -because nothing is pending — there is no first paint where "loading" is honest. The copy -and structure are `LineItemsPanel`'s, which took the same branch for the same key in -objectui#6194 / PR #6359; the two components had been disagreeing about what an author -sees for the identical authoring mistake, and the weaker of the two was the one that read -as the precedent. The hint carries its own `data-testid` (`md-detail-no-child-object`). - -Two source comments — at the decline itself and at the resolver's `catch` — asserted that -"the grid card shows a config hint". They were false, and following them cost a reader a -run of the component. The first is now true and says so. The second is **corrected rather -than made true**: a detail whose schema fetch *threw* does name a child object, so it -skips the new branch and still lands on `Loading columns…`. Distinguishing that from -"still in flight" needs per-entry error state the resolver does not keep, so it is filed -as objectui#6372 and the comment now points at it instead of promising a hint that is not -rendered there. - -No spec or schema change: `childObject` is already REQUIRED on `MasterDetailDetailConfig`. -This is renderer-side reporting of an authoring error that the type system cannot catch, -because a detail entry reaches this renderer straight off an authored JSON schema. diff --git a/.changeset/6361-spec-floor-17-2-0.md b/.changeset/6361-spec-floor-17-2-0.md deleted file mode 100644 index c41aca32a7..0000000000 --- a/.changeset/6361-spec-floor-17-2-0.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/data-objectstack': patch ---- - -`@object-ui/core` and `@object-ui/data-objectstack` now declare -`"@objectstack/spec": "^17.2.0"` rather than `^17.0.0`, which is the lowest published -spec that carries every symbol each package's own build output references -(objectui#6361). - -`packages/core/dist/utils/column-sortability.d.ts` references -`FIELD_SORTABLE_UNPROVISIONED_ANCHOR`, `FIELD_UNSORTABLE_VIRTUAL_TYPE`, -`FieldSortability` and `ObjectSortability` from `@objectstack/spec/api`, and -`packages/data-objectstack/dist/index.js` references the first two — none of which -`@objectstack/spec@17.0.0` exports. Measured against the published tarballs rather than -the installed tree, by `scripts/check-spec-range-floors.mjs`: six `floor-too-low` -findings across the two packages, and `^17.2.0` is that gate's own computed answer for -both. So the old range was a claim neither package could honour: any consumer -resolution that lands 17.0.0 — a sibling pinning it exactly, an `overrides` entry, a -mirror two minors behind — satisfied `^17.0.0` and got a dangling reference. - -Nothing a consumer installs today changes: normal resolution already picks the newest -17.x, and `pnpm-lock.yaml` still resolves `17.2.0` on both edges after the bump — only -the recorded `specifier:` moves. No source and no behaviour changes, which is why this -is scored `patch`, on the reasoning objectui#5793 used for the same remediation on -`@object-ui/plugin-detail`. - -The bump is release-blocking rather than cosmetic. `check:spec-floors` is deliberately -not a `pull_request` job, so every PR stayed green while its blocking copy on the -publish path — `pnpm changeset:publish` runs it before a single tarball reaches npm — -would have cancelled the next release. diff --git a/.changeset/6369-dead-lockfile-merge-driver.md b/.changeset/6369-dead-lockfile-merge-driver.md deleted file mode 100644 index 958aaf5114..0000000000 --- a/.changeset/6369-dead-lockfile-merge-driver.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -CI/doc-only: `.github/workflows/dependabot-auto-merge.yml` no longer configures the -`pnpm-lock.yaml` merge driver, and the pinned "Lockfile Merge Driver" table in -`content/docs/guide/ci-cd-pipeline.md` loses the row that named it (objectui#6369). - -A merge driver runs only when git has to merge the attributed path *on the runner*. The -only merge that job performs is `gh pr merge --auto --squash`, which GitHub executes -server-side in the merge queue — the runner's local git config takes no part in it, so -the driver had no occasion to fire. Swept the whole file before removing: no `git -merge`, `rebase`, `pull`, `cherry-pick`, `am`, `apply` or `revert` anywhere in it, the -`git config` pair being the only `git` present; `actions/checkout` checks out the merge -commit GitHub already computed rather than computing one; and the gate script imports -`node:fs` only. - -Same no-occasion property objectui#6358 measured on `changelog.yml`, reached by a -different route — that job never merged at all, this one merges only where local config -cannot reach. With both dead copies gone the mechanism has one row left, -`changeset-release.yml`. - -The guide sentence that produced both copies is narrowed in the same change: it now asks -for a **local** merge, and records that neither a push nor a server-side merge is one. - -No source and no behaviour change; nothing a consumer installs is affected. diff --git a/.changeset/6372-6371-master-detail-entry-identity.md b/.changeset/6372-6371-master-detail-entry-identity.md deleted file mode 100644 index f9c66819da..0000000000 --- a/.changeset/6372-6371-master-detail-entry-identity.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`MasterDetailForm` gives every detail collection a per-entry record carrying its own -identity and its own resolution status, closing two defects that both came from the same -absence (objectui#6372, objectui#6371). - -`resolvedDetails` was a plain `MasterDetailDetailConfig[]` with no per-entry metadata, so -both *what happened to this entry* and *which entry is this* were inferred from the -entry's position in the array. One record answers both, which is why they land together — -either one alone would have reshaped this structure and the second would then have -rewritten the first. - -**objectui#6372 — a detail whose schema fetch threw sat on "Loading columns…" forever.** -The resolver's `catch` returned the entry unchanged, and an entry with no `columns` is how -*still in flight* is represented too, so the two states were indistinguishable and the -render branch showed the same spinner-shaped message for both. For the failed one it never -ended: the fetch is not retried, so nothing could ever replace it. Entries now carry a -resolution status, and a failed one renders a refusal placeholder naming the child object -whose schema could not be loaded (shaped on `AdvancedChartImpl`'s refusal placeholders — -`role="status"`, because a refusal is a state, not an alert). Measured before the fix -rather than read from source: a detail whose `getObjectSchema` rejects rendered -`

Loading columns…

`. - -The thrown error is no longer discarded. The bare `catch` threw away the whole diagnosis, -so whoever debugged this had neither a message nor a stack; the decline arm next to it has -warned since objectui#5940, and this arm now matches it and passes the error object -through. - -⭐ The fetch and the derive are caught **separately**, because they are different failures -with different truths to tell. A schema that loads fine and then yields no relationship -field is a configuration error, and calling it a load failure would be false. That arm's -render is deliberately unchanged; only its error stops being swallowed. - -**objectui#6371 — a declined entry had no identity across a reorder.** There was no -duplicate-key collision: the map index is unique among siblings by construction, so two -declined details keyed as `undefined-0` and `undefined-1`, distinct. The real defect is -that for a declined entry the data half of that key is `undefined`, leaving position as -the entry's whole identity — and the row-state store was addressed the same way, seeded -once at mount and never re-synced when the authored config changed. Reordering or removing -an entry therefore handed a collection a different collection's rows. - -Entries now carry an id synthesized once from the incoming config: the child object for a -named collection, and the authored position for a declined one, which has no other -identity to offer. Row state is keyed by that id, so a collection can only ever read its -own slot. Three reads were affected, not the one the report named: - -- the grid value, which showed the wrong collection's rows; -- the document **subtotal** reducer, so a reorder did not merely mis-associate a grid, it - mis-computed the total; -- the batch payload on save, which read - `details.filter(d => d.relationshipField).map((d, i) => state[i])` — after the filter `i` - indexed the filtered array while the row state was indexed against the full one, so a - declined entry above a real collection shifted every read below it by one and that - collection's rows were **silently dropped from the transaction**. Data loss on save, not - a display defect. diff --git a/.changeset/6373-datatable-emit-boundary.md b/.changeset/6373-datatable-emit-boundary.md deleted file mode 100644 index 5da8a51593..0000000000 --- a/.changeset/6373-datatable-emit-boundary.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`ObjectDataTable` no longer writes six undeclared keys into the `data-table` columns slot -(objectui#6373). `enrich()` returned `NormalizedColumn`, whose `[key: string]: any` accepts -anything, so nothing checked the producer's output against -`DataTableSchema.columns: TableColumn[]`: `{ ...col, ...fieldMeta }` spread `label`, -`options`, `referenceTo`, `format`, `currency` and `decimals` onto every emitted column, and -`TableColumn` declares none of them. - -The measured read set of the consumer (`data-table.tsx`, comments stripped) contains none of -the six, so all six retire from the emit rather than being declared — declaring a key nothing -reads is the same `declared != enforced` defect facing the other way. Rendering is unchanged -because none of those keys was the live path for its own value: the `FieldMeta` the `cell` -closure captures is what this widget's type-aware rendering has always read, and it is -untouched. Authored spellings still pass through, so a column the author wrote as -`{ format: '$0,0' }` keeps its `format` exactly as before. - -`type` is unchanged — objectui#5853's fold at this seam still applies. `name` is unchanged -and still written: `data-table` reads `col.accessorKey || col.name` and objectui#5120 holds -that alias while two published skill guides still teach a `{ name, label }` column. The hold -is now declared at the seam instead of arriving anonymously inside a spread. - -The seam's emit type carries ADR-0049 `?: never` tombstones for the retired keys rather than -being a bare `TableColumn` annotation. Measured before the shape was chosen: a bare annotation -raises no error at all here, because TypeScript's excess-property check exempts properties -that arrive through a spread — it would have type-checked the boundary without enforcing it. diff --git a/.changeset/6375-concurrent-update-subsumed-guard.md b/.changeset/6375-concurrent-update-subsumed-guard.md deleted file mode 100644 index d780ec58a6..0000000000 --- a/.changeset/6375-concurrent-update-subsumed-guard.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Internal only, no behaviour change: `@object-ui/data-objectstack`'s -`normaliseClientError` carried two stacked `CONCURRENT_UPDATE` guards whose -first could never decide an outcome — its condition -(`code !== 'CONCURRENT_UPDATE' && httpStatus !== 409`) is strictly stronger -than the line below it, so every input it would have returned was returned -one line later anyway. Its `httpStatus !== 409` half advertised a second -acceptance path (a bare 409 still being re-wrapped) that never existed, on the -one function whose whole job is deciding which errors get re-wrapped. Deleted, -with the effective rule — the wire `code` is the sole discriminator — written -where the dead line used to be. - -Also aligned the doc comment above the exported `isConcurrentUpdateError` with -the predicate underneath it: the doc named only the wire shape while the code -accepts `name === 'ConcurrentUpdateError'` as well. The `name` limb is kept — -it is the deliberate cross-realm discriminator that -`isViewConfigPermissionDeniedError`'s doc already cites this function as its -precedent for — and the doc now says so. - -Both accepted sets (the re-wrap's and the predicate's) are now pinned as an -explicit truth table in `packages/data-objectstack/src/occ.test.ts`. diff --git a/.changeset/6378-boot-redirect-splash.md b/.changeset/6378-boot-redirect-splash.md deleted file mode 100644 index 739b8c31a9..0000000000 --- a/.changeset/6378-boot-redirect-splash.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The console boot no longer flashes a fully-white frame after the splash has painted -(objectui#6378). - -Cause, established by measurement before any fix was written — the card named -`LoadingScreen`'s unmount timing and `RouteFader` as suspects and both are exonerated. -A CDP `Page.startScreencast` frame ledger (every frame classified with the card's own -rule: white when no colour channel falls below 242) was correlated against a DOM-state -ledger on the same clock (`performance.timeOrigin`), against the production -`apps/console` bundle with the boot endpoints mocked. `RouteFader` never mounts on the -boot path at all, and `LoadingScreen` unmounts exactly when its own gate says to. What -is wrong is what REPLACES it: every readiness gate renders the splash while it waits and -a bare `` the moment it decides. `` renders `null` and react-router -runs the navigation as a transition, so the destination tree renders while the commit -that already dropped the splash is what the compositor is showing — 41–147 ms during -which `#root` holds no view and the viewport is the bare page background. The flash is -intermittent only because it depends on a frame being swapped inside that window; the -window itself was present on every measured boot. - -`RedirectWithSplash` (new, `@object-ui/app-shell`) pairs the same `` with the -same `LoadingScreen` the gate one line above was already rendering, so the handoff -changes no pixels and the transition runs underneath an unchanged screen. The console's -three boot redirects use it: the auth gate's `/login` bounce, the `/` landing resolver, -and the catch-all route. The nested organization `index` redirect deliberately does not — -it fires under an already-painted layout, where covering the screen would be the -regression. - -Acceptance campaign — same instrument on both sides, the two arms INTERLEAVED inside one -process and one browser so drift in this shared container's load lands on both equally. -102 paired boots per arm across five cells (signed-out `/`, signed-in `/`, an unmatched -entry, and the card's two throttled network profiles). The empty-viewport window: 102/102 -pre-fix, 0/102 post-fix. The white frame itself, pooled over the three cells where the -pre-fix build actually flashed: 67/87 (77%) pre-fix, 0/87 post-fix — 95% upper bound on -the residual rate 3.4%, against a card-reported defect rate of ~1/3. The two throttled -cells are reported but NOT pooled: the pre-fix build flashed 0 times there, so before and -after agree and those cells prove nothing about the pixels (they still separate 15/15 vs -0/15 on the DOM window). - -`e2e/console-boot-indicator.spec.ts` gains the deterministic half as a gate — after -React's first commit the viewport centre must never stop being covered. That reading is -what makes an intermittent defect gateable: the flash needs a frame to be swapped inside -the window, but the window itself was present on every measured boot. Verified red-first, -6/6 red on the pre-fix bundle and 6/6 green on this one. diff --git a/.changeset/6391-lockfile-merge-driver-measurement.md b/.changeset/6391-lockfile-merge-driver-measurement.md deleted file mode 100644 index d92c283548..0000000000 --- a/.changeset/6391-lockfile-merge-driver-measurement.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -CI/doc-only: records the measurement of the last remaining "Lockfile Merge Driver" row -(objectui#6391). No mechanism is added or removed. - -`changeset-release.yml` performs no local merge, so the driver it configures cannot fire in -that job. Swept the whole file with a control term on every zero-hit — no `git merge`, -`rebase`, `pull`, `cherry-pick`, `am`, `apply` or `revert`; every `git` in it is the two -`git config` lines, `git status` twice, and the `git checkout -- .` / `git clean` pair that -undoes the version step, `checkout -- .` being an index-restore rather than a merge. The -deciding fact is in the marketplace action: read at `changesets/action` v1.9.0 (`a45c4d5`), -`src/git.ts` and the shipped `dist/` agreeing, its entire git surface is `checkout`, -`reset --hard`, `add .`, `commit -m`, `push --force` and `config user.*` — the version branch -is updated by `reset --hard` plus a force-push, and a force-push resolves no merge. - -⭐ The `.gitattributes` line is NOT dead, which is why the row was not removed with the other -two. `CONTRIBUTING.md` has contributors configure the same driver and then `git merge -upstream/main` — a real local merge on the attributed path. Measured in a scratch repository -with one variable changed: with the attribute the driver fires and the lockfile is -regenerated; without it the identical merge ends in `CONFLICT (content)`. The CI half is dead -and the repository-wide half is live, while `ci-cd-pipeline-doc.test.ts` binds them together, -so the removal is a decision rather than a cleanup and is escalated instead of guessed. - -The row's stated reason ("version bumps rewrite the lockfile") was wrong and is corrected — a -rewrite is not a merge — and the guide gains the third ⛔ note in the series that already -records "pushing is not merging" and "a server-side merge is not a local one". - -No source and no behaviour change; nothing a consumer installs is affected. diff --git a/.changeset/6392-vestigial-pnpm-steps.md b/.changeset/6392-vestigial-pnpm-steps.md deleted file mode 100644 index 0c9cfc4f68..0000000000 --- a/.changeset/6392-vestigial-pnpm-steps.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -CI-only change in `.github/workflows/dependabot-auto-merge.yml` (objectui#6392): removed the -`corepack enable` + `pnpm --version` steps left behind once objectui#6389 removed the lockfile -merge driver that had been their only consumer. Nothing in this job runs `pnpm install` or -otherwise shells out to `pnpm`, so the two lines were a package manager enabled and -version-printed for zero consumers. A comment in the workflow records the reasoning for the -removal (and for not keeping `pnpm --version` alone as a fail-fast) so a future reader does not -re-add it unexplained; the `cache: 'pnpm'` comment on the `Setup Node.js` step below was reworded -to note that no pnpm setup happens in this job at all. - -No source or behaviour change outside the workflow file; no published package touched. diff --git a/.changeset/6394-master-detail-underivable-detail-config-hint.md b/.changeset/6394-master-detail-underivable-detail-config-hint.md deleted file mode 100644 index 52579e4076..0000000000 --- a/.changeset/6394-master-detail-underivable-detail-config-hint.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`MasterDetailForm` shows a config hint naming `relationshipField` for a detail collection -whose child schema **loaded fine but could not be derived from**, instead of a permanent -`Loading columns…` (objectui#6394). - -This is the third and last arm of the same resolver to be closed. `deriveDetail` throws -when no lookup/`master_detail` field on the child object references the parent — a -configuration error whose remedy is a key the author writes. The `catch` returned the -entry unresolved, so it fell through to `!d.columns?.length ?

Loading columns…

`, -and that message never ended: the derive is not retried, so those columns could never -arrive. Same unbounded-wait-shown-as-a-spinner family as objectui#5940 / objectui#6188 / -objectui#6194 / objectui#6360 / objectui#6372. - -The entry now carries `status: 'underivable'`, and the renderer gives it a branch of its -own that names both ends of the relationship it could not find and the key to set: - -> Could not work out how `po_line` links to `purchase_order`: no lookup or master_detail -> field on it references the parent. Set `relationshipField` on this collection to the -> field that holds the parent record. - -⛔ Deliberately **not** objectui#6372's refusal placeholder, which states the schema could -not be loaded — false for a schema that loaded fine. The two failures keep separate copy -because they have different remedies: one is "check the object exists and reload", this one -is "set this key". The thrown error is still logged with its stack (objectui#6372), since -the placeholder shows the author the key rather than the raw message. - -Behaviour is unchanged for the other two arms and for a detail that is genuinely still -fetching — that one keeps `Loading columns…`, where the message is true. diff --git a/.changeset/6395-approvals-characterization-pins.md b/.changeset/6395-approvals-characterization-pins.md deleted file mode 100644 index 35a9404272..0000000000 --- a/.changeset/6395-approvals-characterization-pins.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only: pins three delivered-but-unasserted Approvals Inbox behaviours -(#2762 P1/P2, shipped in #2803/#2811) — approver chips keyed by (name, group), -the "Flow-initiated" origin cell, and the record-card `prettifyKey`/ -`decisionAmountEntry` copy — as a characterization suite (objectui#6395). No -production behaviour changes. diff --git a/.changeset/6396-previous-values-dom-leak.md b/.changeset/6396-previous-values-dom-leak.md deleted file mode 100644 index 7033c319c4..0000000000 --- a/.changeset/6396-previous-values-dom-leak.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The form renderer no longer leaks `FormSchema.previousValues` onto the `
` DOM node -(objectui#6396). - -`previousValues` is a declared schema key with a real consumer: the renderer's -`previousRecord` memo, which binds `previous` for field-rule CEL predicates and is the -INSERT/UPDATE signal the read-only submit strip gates on (objectui#3484). That consumer -reads it off `schema` and is unchanged. The defect was on the other channel — -`SchemaRenderer` spreads every non-metadata top-level schema key as a React prop *in -addition* to handing the node over as `schema`, so an edit-mode host (`ObjectForm`, which -is what the `object-master-detail-form` header composes) delivered a second, top-level copy -in `...props`. The renderer already consume-and-drops that whole family before its DOM -spread — `objectName`, `onDirtyChange`, `defaultValues`, `fields`, `layout`, … — -and `previousValues` was the one member missing from the list. - -Two things followed, on every edit-mode header render. React declined the prop and printed -`React does not recognize the previousValues prop on a DOM element`, and — measured on -React 19, and not recorded on the card — the persisted record was still stamped onto the -element as `previousvalues="[object Object]"`. Consume-and-dropping the duplicate removes -both. - -Scope is the runtime leak only. The declared key stays exactly as declared -(`packages/types/src/form.ts`, `packages/types/src/zod/form.zod.ts` are untouched): it has -a live consumer, so there is nothing here for the enforce-or-remove channel. diff --git a/.changeset/6397-partial-schema-collapse-pin.md b/.changeset/6397-partial-schema-collapse-pin.md deleted file mode 100644 index 244bf72b19..0000000000 --- a/.changeset/6397-partial-schema-collapse-pin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`PartialSchema` is pinned as collapsed, and its doc comment now says so -(objectui#6397). Nothing about the type changes — the declaration is deliberately -left exactly as written. - -The alias promises "all properties optional except the type" and does not deliver -it. Every instantiation declares exactly ONE property, `type`, and carries a live -`[key: string]: any`, so it accepts any key at `any`. Measured through the -TypeScript checker against the emitted `index.d.ts` — the same instrument that -produced objectui#6269's 61 -> 0 reading: - - PartialSchema -> 1 declared property: type (source: 61) - PartialSchema -> 1 declared property: type (source: 67) - PartialSchema -> 1 declared property: type (source: 42) - PartialSchema -> 1 declared property: type (source: 27) - -`Omit` is `Pick>`, and `keyof T` on a type carrying a -string index signature is `string | number` — the literal member names are -absorbed. Every `T extends BaseSchema` inherits `BaseSchema`'s `[key: string]: any` -(objectui#5155), so `Partial>` rebuilds a type holding the index -signature and none of the named members. This is objectui#6151's collapse -(heritage clause) and objectui#6269's (property position) in a third position: a -generic mapped-type alias, which is why neither of their guards sees it — #6151's -walks the `LayoutSchema` union, #6269's reads `ObjectViewSchema`'s two slots. - -**Why a pin and not a repair or a retirement.** Triage ruled on 2026-08-25 that -retiring the alias is a removal of a published export of `@object-ui/types` — a -breaking removal of published capability, which sits on the human floor — and that -escalating it today would spend the maintainer's attention on a question -objectui#5155 is expected to moot. Repair in place is unavailable: `T` is generic, -so there is no literal key list to `Pick` the way #6269 could for its two concrete -schemas, and every generic re-spelling collapses for the same `keyof T` reason. -Once #5155 removes the root index signature, the alias starts working as written -with no edit at all. What this ships is the removal of the one impermissible -state — *declared, published, collapsed, and unpinned*. - -No runtime code, no type declaration and no accepted value changes; a consumer's -`PartialSchema` means exactly what it meant before. Declared `patch` rather -than as a no-release so the doc-comment warning actually reaches the published -`.d.ts` a consumer reads — that warning is the deliverable half of the card. diff --git a/.changeset/6400-workflow-version-anchor.md b/.changeset/6400-workflow-version-anchor.md deleted file mode 100644 index 05085edcaa..0000000000 --- a/.changeset/6400-workflow-version-anchor.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Test-only (objectui#6400): the version-claim ledger's `Node 22.x` entries no -longer restate a hand-written count of `.github/workflows`, they are asserted -against it. - -`scripts/__tests__/doc-version-claims.test.ts` gains a `workflowVersionKey` -field on `KNOWN_CLAIMS` — the objectui#6400 counterpart to `skeletonDep`. It -names the GitHub-Actions key (`node-version`) whose declarations across -`.github/workflows` are a claim's anchor, and a new -`describe('doc version claims - the workflow-version assertion')` reads those -declarations on every run and demands the page state their major. - -The entry that motivated it was `anchored` and both halves of its reason had -gone false: it named "the 14 `node-version: 22.x` declarations" against a tree -holding 28 across 23 files, one of which is spelled `'22'` rather than `'22.x'`, -and it credited `ci-cd-pipeline-doc.test.ts` with pinning the line when that -file contains no `node-version` and no `22` at all. The claim itself stayed true -throughout — what rotted is the sentence a reviewer re-measures it by, which is -the entire load an `anchored` entry without a machine-checked field carries. - -Three entries carry the new field (`ci-cd-pipeline.md`, `building-crud-app.md`, -`quick-start.md` — all three counts were stale), and none of their reasons -states a count any more. The comparison is on the **major**, not the spelling, -so the one lane declaring `'22'` is inside the anchor rather than a blind spot; -a value the parser cannot read is reported rather than skipped, and the line -parser is cross-checked against a counter that knows only the key. - -No package source changed and nothing is published by this. diff --git a/.changeset/6405-fields-shared-stylesheet-builder.md b/.changeset/6405-fields-shared-stylesheet-builder.md deleted file mode 100644 index 8ae7c7db09..0000000000 --- a/.changeset/6405-fields-shared-stylesheet-builder.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Build-tooling consolidation with no published bytes changed: -`packages/fields/scripts/build-css.mjs` no longer carries its own copy of the -components-sheet subtraction and now runs the shared -`scripts/build-plugin-stylesheet.mjs` builder that `@object-ui/plugin-grid` and -`@object-ui/plugin-kanban` already use (objectui#4929). One implementation of -the subtraction now exists, so a fix to it reaches every package that ships a -supplement stylesheet instead of only the one it was applied to. - -Declared as **no release** deliberately. `@object-ui/fields` ships -`dist/index.css` to consumers, so the acceptance gate for this change was byte -identity of that artifact, not equivalent-looking code — and it holds: the -emitted sheet is `git hash-object f9cd6504a9e3a9603836230aea27c5a3a7e4e431`, -22761 bytes, `167 rules kept (163 classes)`, both before and after the rewrite, -and now from any working directory. Nothing a consumer installs changes, so -there is no behaviour to release; bumping the fixed group of packages for a -build script that emits identical bytes would put a version in the changelog -that describes nothing. - -Fields keeps what is specific to it — its `MUST_SURVIVE` sentinels, its -`CLASS_CEILING` of 600 and its stylesheet banner, the last through a documented -`build({ header })` hook added to the shared module rather than a retained -second copy of the builder. diff --git a/.changeset/6409-keyed-version-recogniser.md b/.changeset/6409-keyed-version-recogniser.md deleted file mode 100644 index 9961167d95..0000000000 --- a/.changeset/6409-keyed-version-recogniser.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -The doc version-literal ledger can now see a version written behind a -`-version:` key — `node-version: 20`, the shape every workflow example in -these docs uses (objectui#6409). Test-only: `scripts/__tests__/doc-version-claims.test.ts` -gains a second recogniser for that shape, plus the permanent fixture that rebuilds the -pre-fix recogniser and asserts it fails on the same line the new one matches. No -published package source changes. diff --git a/.changeset/6412-packaged-actions-activation.md b/.changeset/6412-packaged-actions-activation.md deleted file mode 100644 index cf50e3c65b..0000000000 --- a/.changeset/6412-packaged-actions-activation.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Setup › **Packaged automation** gains a packaged **actions** section beside its flows one — -the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch -doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated: -「动作 可能是需要开关的,因为有的 action 我不想启用。」 - -Per packaged action the section does exactly **one** thing: **on/off for this scope**. That -is all the `sys_metadata_activation` ledger knows about an action, and the section claims -nothing more. - -- **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic - data API list the object itself sanctions for exactly this - (`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer - 'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows - everything armed. -- **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded - `actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with - the object-embedded declaration winning a `:` key clash. Listing only the - first source would leave an administrator with no off-switch for a packaged standalone - action. -- **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly - the one key its body declares (`{ enabled }`); `global` is the object segment for an - object-less action, the same spelling both dispatch doors take. - -**⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3 -charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered -until real pull appears. A clone control here would advertise machinery that does not exist — -which is also why the server's §5 refusal for actions recommends the platform operator and an -ordinary sibling action where the flow refusal recommends a clone. - -**⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no -base-moved notice. The platform tracks no such lineage, so a surface showing it would be -showing something it had to invent. The absence is pinned against a response that smuggles -`clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire. - -Server refusals reach the operator **verbatim** — no client-side softening, no retry loop. -Three shapes are reachable in tests, each transcribed character-for-character from the -runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the -posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`, -naming the objects a machine name collides across — a list nothing on the client could -reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`). - -One further refusal is the section's own and it points the same way: a `hasMore` on the -ledger read is treated as a **load failure** rather than rendered. A dropped row reads as -"active", so a partial ledger would show a switched-off action as armed — the one direction -this section must not fail in. - -The flows section is unchanged in behaviour. It gains a heading beside the new one, and the -page subtitle now says "Flows and actions" — that string moved in all ten packs together. -Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real -translations; the two toggle-failure keys are artifact-neutral by wording and are reused -rather than duplicated. diff --git a/.changeset/6416-plugin-report-namespace.md b/.changeset/6416-plugin-report-namespace.md deleted file mode 100644 index 41d0b87d0c..0000000000 --- a/.changeset/6416-plugin-report-namespace.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-report': patch -'@object-ui/cli': patch ---- - -`@object-ui/plugin-report` now registers its three components under namespace -**`plugin-report`**, the spelling its consumers already declare (objectui#6416). - -It used to register `report`, `spec-report` and `report-viewer` under namespace -`report`, while `apps/console` declared the lazy stubs for the same three short -names under `plugin-report` and the CLI's known-type whitelist shipped the -`plugin-report:*` spellings as renderable. Two things followed from the -disagreement: - -- **`plugin-report:report`, `plugin-report:report-viewer` and - `plugin-report:spec-report` could never be satisfied.** `Registry.register` - clears the lazy stub for the type IT registers, and that type was - `report:report`, so those three stubs were never cleared and no component was - ever stored under them: `get('report', 'plugin-report')` returned `undefined` - and `hasLazy('report', 'plugin-report')` stayed `true` forever. A schema - authored with any of the three whitelisted keys resolved to nothing — the - gate handed authors a green light for a key the runtime could not satisfy. -- **The bare `report` key was claimed twice under two different namespaces.** - `Registry.register` and `Registry.registerLazy` share the - `meta?.namespace && !meta?.skipFallback` branch, so what bare `report` - *declared* depended on whether the plugin chunk had loaded yet — the - objectui#6353 shape. - -**No authored metadata changes.** The direction was chosen by measurement: -nothing in this repository, and nothing in the sibling `objectstack` checkout, -authors a `report:*` spelling (0 hits), while the bare spellings are authored in -48 places. `type: 'report'`, `type: 'spec-report'` and `type: 'report-viewer'` -resolve exactly as before; the three unreachable `report:*` keys are retired and -the three `plugin-report:*` keys now name real components for the first time. - -`packages/cli/src/utils/known-schema-types.ts` is regenerated from the -registrations, dropping `report:report`, `report:report-viewer` and -`report:spec-report`. - -Two pins are the half that outlives the fix: -`packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts` replays -this package's real declared metadata and a console-shaped lazy stub into a -fresh `Registry` in **both** registration orders, checking the bare key's -declared namespace after every step, so order- and phase-independence are -properties under test rather than properties of the file the test imports. -`scripts/__tests__/report-namespace-agreement-6416.test.ts` re-derives both -sites from source and fails if the plugin, the console stubs and the generated -whitelist ever disagree again. diff --git a/.changeset/6419-objectview-expand-gate.md b/.changeset/6419-objectview-expand-gate.md deleted file mode 100644 index 8b53c9e1a0..0000000000 --- a/.changeset/6419-objectview-expand-gate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView`'s non-grid fetch now carries `$expand` (objectui#6419). The effect built its -expand set from `objectSchemaRef.current` — a ref assigned in the render body, deliberately -kept out of the effect's dependency list so the effect would run exactly once per mount. On -that one run the ref was still `null`, so `buildExpandFields` saw no fields and the query -went out as `{ $top: 100 }` with no `$expand` at all; because the effect never re-ran on the -schema's arrival, it never went out with one either. - -`ObjectView` hands the rows it fetches to the child view as `data={data}`, which suppresses -that child's own fetch. So every lookup / master_detail / user / tree field in the six -non-grid views it hosts — kanban, calendar, gallery, timeline, gantt, map — rendered from -raw foreign-key ids: blank on the kanban (its `resolveDisplay` suppresses opaque ids) and -potentially the raw id on the other five. - -The object schema and the fact that its read has SETTLED are now one piece of state, keyed -by object name, and the record query waits on it — the shape `ObjectKanban` adopted in -objectui#6271. The gate is on the read having settled, **not** on a truthy schema: a view -whose adapter exposes no `getObjectSchema`, or whose read threw, still queries (unexpanded) -rather than waiting forever, and switching objects closes the gate in the same commit rather -than sending the previous object's expand set. - -The trade was measured on this effect rather than inherited, because it has five more -dependencies than the board's. With an instrumented adapter (schema and `find` both 30ms) -across four host regimes: before, one query with no `$expand` and one raw delivery to the -child; with `objectSchema` merely added to the dependency list, two queries and two -deliveries — `raw` then `expanded`, a visible two-step paint, because here the raw rows -settle into state *before* the re-run's cleanup rather than being discarded as they were on -the board; gated, one query carrying `$expand` the first time and a single expanded -delivery, with correct rows landing at the same wall clock as the dependency version. diff --git a/.changeset/6421-concurrent-update-code-optional.md b/.changeset/6421-concurrent-update-code-optional.md deleted file mode 100644 index bc5118a490..0000000000 --- a/.changeset/6421-concurrent-update-code-optional.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`isConcurrentUpdateError` no longer promises a `code` its runtime check -knowingly accepts values without. - -The exported predicate matches on two limbs — `code === 'CONCURRENT_UPDATE'` -(the wire shape) **or** `name === 'ConcurrentUpdateError'` (the deliberate -cross-realm discriminator, for a host that bundles the adapter twice so -`instanceof` fails). Its narrowed type declared `code: 'CONCURRENT_UPDATE'` as -a **required** literal, so for exactly the case the second limb exists to serve -the predicate handed the caller a property the value does not have. `code` is -now declared optional (`code?: 'CONCURRENT_UPDATE'`), stating only what both -limbs guarantee. - -**Type-level only; zero runtime change.** The two-limb check is byte-identical -— it is deliberate and documented, and the diff touches nothing but the return -type and the comment above it. There was no runtime symptom to fix: the only -consumer (`InlineEditSaveBar`'s conflict builder) reads just the optional -`currentVersion` / `currentRecord` fields. - -**Breaking for TypeScript consumers that read the narrowed `code`.** After -narrowing, `err.code` is now `'CONCURRENT_UPDATE' | undefined` instead of -`'CONCURRENT_UPDATE'`, so code that assigned it to a non-optional `string` will -newly fail to compile. That failure is the point: on a name-only error the -value was already `undefined` at runtime, and the old declaration was the -reason the compiler could not say so. Guard the read (`err.code === -'CONCURRENT_UPDATE'` still narrows fine) or branch on the predicate itself -rather than on the field. - -Pinned by `ConcurrentUpdateDialog.narrowedCode-6421.test.tsx`, which asserts -the invariant by assignability — the value the runtime accepts through the -`name` limb must be assignable to the type the predicate returns — alongside -runtime coverage of both limbs, which `plugin-detail` had none of. diff --git a/.changeset/6424-dismantle-fitcontent-cast.md b/.changeset/6424-dismantle-fitcontent-cast.md deleted file mode 100644 index d4ab6e2d57..0000000000 --- a/.changeset/6424-dismantle-fitcontent-cast.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -No published behaviour changes: the emit-side `(col as any).fitContent` cast in -`ObjectGrid`'s grouped-width pass is dismantled, and the guard that observed it is -re-pointed rather than deleted (objectui#6424, maintainer ruling 2026-08-29, option -甲). - -`TableColumn` declares `fitContent` since PR #6673, so the ruling's declare branch -completes with the cast removed. ⚠️ The narrowness of what that buys is measured, not -assumed: the cast was already worth **zero** type safety, because the receiver is `any` -either way — `applyColumnChrome` is `(col: any)`, so `orderedColumns` is `any[]` and the -loop widens it a second time. The TypeScript checker reports `isAny=true` for the -receiver both before and after the cast, against a control in the same query shape that -reports a non-`any` type. Removing it changed no emit type and no rendered output. - -The load-bearing half is the guard. `columnReadBoundary-6458.test.ts` used that cast as -its anti-vacuity control 3 — the one control proving its scanner can find a real cast in -the real file at real scale — and that control's own comment requires it to be re-pointed -at another real cast and never deleted. Its scanner had the receiver hard-coded to `col`, -and dismantling left the file with zero `col` casts outside the guarded region, so the -receiver becomes a parameter (defaulting to `col`, so every bound assertion is unchanged) -and the anchor moves to `(schema as any).hideRowHeightToggle`. - -That anchor is chosen on a principle rather than merely moved: it is a deliberately held -non-authoring key (objectui#5091) that is independently pinned by -`gridNonAuthorKeys.test.tsx`, so it cannot expire silently the way `fitContent` just did — -retiring it turns that guard red in the same run. Control 2 gains a pair of assertions -pinning that the new receiver parameter is honoured rather than ignored, so the bound -assertions' scoping to `col` cannot quietly become a fiction. diff --git a/.changeset/6424-retire-headericon-hold.md b/.changeset/6424-retire-headericon-hold.md deleted file mode 100644 index 01740df288..0000000000 --- a/.changeset/6424-retire-headericon-hold.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Internal type cleanup in `plugin-grid`: `ObjectGridColumnHolds` no longer declares -`headerIcon`, which `TableColumn` has declared since objectui#6615, plus the docblock -corrections and pins that go with it. Nothing published moves — `ObjectGridColumnHolds` -is not part of the package entry's exported surface (measured: 0 occurrences in -`dist/index.d.ts`, control `ObjectGridColumnState` 1), and the two emit types are -byte-identical without the member (27 resolved members before and after, against a -positive control that removing `pinned` instead takes them to 26). No published -behaviour changes. diff --git a/.changeset/6424-tablecolumn-declares-fitcontent.md b/.changeset/6424-tablecolumn-declares-fitcontent.md deleted file mode 100644 index 622f93d18e..0000000000 --- a/.changeset/6424-tablecolumn-declares-fitcontent.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`TableColumn` declares `fitContent`, the content-hugging flag `data-table` has -honoured all along (objectui#6424, maintainer ruling 2026-08-28, Option A — the -card's second key, in the shape #6615 landed `headerIcon` in). - -The key was undeclared-but-honoured: `data-table` skips `fitContent` columns in -the auto-width pass and renders them as a `width:1%` + `whitespace-nowrap` cell -with no `overflow-hidden` clamp — but the published declaration refused the key, -so a typed author writing `{ accessorKey: '_actions', fitContent: true }` got a -compile error for a key the renderer implements, and `TableColumnSchema.parse` -silently STRIPPED it, while the same key placed by an untyped producer worked. -The runtime admitted a vocabulary the declaration refused — the second de-facto -contract AGENTS.md #0.1 forbids, here with the CONSUMER out of step. - -Retiring the reads instead was excluded BY MEASUREMENT, not preference: shipped -source authors the key (`ObjectGrid` writes `fitContent: true` on the injected -row-actions `_actions` column) and `data-table-fit-content.test.tsx` pins the -result. Retiring would re-clip inline row-action buttons. - -- `TableColumn.fitContent?: boolean` — serializable metadata, unlike the - `React.ReactNode` slot `headerIcon` is. -- `TableColumnSchema` mirrors it as `z.boolean().optional()`: the flag now - SURVIVES parse instead of vanishing, and a non-boolean is a loud refusal - naming the key rather than acceptance-without-validation. Pinned by output - survival, not parse acceptance — acceptance was green before, while the flag - was stripped. -- `StaticTableColumn` / `StaticTableColumnSchema` tombstone it (`?: never` + - `z.never().optional()`), per #5474's lockstep rule: every rich key needs a - deliberate static-side decision, and the static renderer has no auto-width - pass to opt out of (its measured read set is the five live keys). Authoring - it on a static `table` column is a loud parse refusal naming the key, not a - silent strip. - -No runtime behaviour changes in `data-table` itself — the reads were already -live; the declaration and the parse road now agree with them. The two -`(col as any).fitContent` sites drop with the declaration, but that removal is -bookkeeping rather than the fix: `col` is already `any` at both sites, widened -by the file's own `col: any` normalization, so the casts were redundant at -compile time today. They become load-bearing the moment those render callbacks -are typed — which is the standing instrument gap, not closed here. diff --git a/.changeset/6424-tablecolumn-declares-headericon.md b/.changeset/6424-tablecolumn-declares-headericon.md deleted file mode 100644 index 258b5755dd..0000000000 --- a/.changeset/6424-tablecolumn-declares-headericon.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`TableColumn` declares `headerIcon`, the icon node `data-table` has rendered into the -header cell all along (objectui#6424, maintainer ruling 2026-08-27, Option C per-key). - -The key was undeclared-but-honoured: `data-table` renders `col.headerIcon` before the -header text, and `ObjectGrid` writes it for `showColumnTypeIcons` — but the published -declaration refused it, so a typed author writing `{ accessorKey: 'x', headerIcon: icon }` -got a compile error for a key the renderer implements, and `TableColumnSchema.parse` -silently STRIPPED it, while the same key placed by an untyped producer worked. The runtime -admitted a vocabulary the declaration refused — the second de-facto contract AGENTS.md -#0.1 forbids, here with the CONSUMER out of step. - -- `TableColumn.headerIcon?: React.ReactNode` — a runtime slot like `cell`, not - serializable metadata. -- `TableColumnSchema` mirrors it (`z.any()`, passthrough): the node now SURVIVES parse - instead of vanishing. Pinned by output survival, not parse acceptance — acceptance was - green before while the icon was stripped. -- `StaticTableColumn` / `StaticTableColumnSchema` tombstone it (`?: never` + - `z.never().optional()`), per #5474's lockstep rule: every rich key needs a deliberate - static-side decision, and the static renderer never read this one. Authoring it on a - static `table` column is a loud parse refusal naming the key, not a silent strip. - -No runtime behaviour changes in `data-table` itself — the reads were already live; the -declaration and the parse road now agree with them. - -The card's second key, `fitContent`, is deliberately NOT declared and NOT retired here: -the ruled measurement found real authors (`ObjectGrid`'s row-actions column authors -`fitContent: true` on `main`), so per the ruling that arm goes back to the decision box -with the reading rather than into this PR. - -Scope of the enforcement claim, measured: the declaration, the parse road, and the -renderer's behaviour now agree; the renderer's internal column reads remain any-mediated -(the `col: any` normalization in `data-table.tsx`) — a standing instrument gap, not -closed here. diff --git a/.changeset/6425-objectdatatable-column-holds.md b/.changeset/6425-objectdatatable-column-holds.md deleted file mode 100644 index d222901e91..0000000000 --- a/.changeset/6425-objectdatatable-column-holds.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -No published behaviour changes, and nothing declared or retired. - -`ObjectDataTable.enrich()` read six field-meta overrides off the AUTHORED column — three -through `(col as any)`, two more through the column bag's `[key: string]: any`, which -answers `any` just as loudly without the tell (objectui#6425). Those reads now go through -a local `AuthoredColumnOverrides` keyhole: every honoured key carries a written verdict -and the card that owns it, and every other `FieldMeta` member is refused by a DERIVED -`?: never` band, so a seventh one has to be adjudicated instead of admitted by silence. - -Type-only at the seam plus a new test file. `@object-ui/types`' `TableColumn` and its zod -mirror are untouched: whether `format` / `options` / `referenceTo` / `currency` / -`decimals` get declared on the published type or retired stays objectui#6425's open -ruling, and this change is the per-key evidence that ruling needs, not the ruling. diff --git a/.changeset/6425-per-key-declare-retire.md b/.changeset/6425-per-key-declare-retire.md deleted file mode 100644 index 6ecf44a494..0000000000 --- a/.changeset/6425-per-key-declare-retire.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-grid': patch ---- - -Land objectui#6425's per-key ruling for `ObjectDataTable`'s authored column -override keys (maintainer, 2026-08-27): - -- **Declare `format`, `options`, `currency`** on `TableColumn` and its - `TableColumnSchema` zod mirror, in the same stroke. All three are honoured - by `object-data-table`'s cell pipeline — `format` / `options` were - documented author overrides the published types refused (a typed author got - a compile error and the zod parse silently stripped the key); `currency` - shipped in production but was never promised. The zod mirror now passes the - keys through instead of stripping them; `StaticTableColumn` and its mirror - tombstone all three under the #5474 lockstep rule (the static renderer - reads no field-meta overrides). -- **Retire `decimals`**, immediately: zero readers measured anywhere - (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads - `precision`), so no authored `decimals` could reach a render. The authored - read is removed and the key falls into `AuthoredColumnOverrides`' derived - refusal band — render output is pinned unchanged. -- **Re-arm plugin-grid's #6004 `options` retirement with an explicit - tombstone**: that refusal rested on the key's NON-membership (excess-property - freshness), which declaring `options` on `TableColumn` silently ended. - `ObjectGridRetiredOptionsTombstone` (`?: never`, intersected into both - `ObjectGridColumnDraft` and `ObjectGridColumn`) restores the refusal by - assignability; #6004's verdict itself is unchanged. -- **`referenceTo` is deliberately NOT declared as spelled** — it stays held, - owned by objectui#6597 (fix the spelling chain or withdraw the README - line). The remaining hold is that card's scope, not unfinished work here. diff --git a/.changeset/6426-dashboard-test-header-compiled.md b/.changeset/6426-dashboard-test-header-compiled.md deleted file mode 100644 index 2e6e50ba75..0000000000 --- a/.changeset/6426-dashboard-test-header-compiled.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -Comment-only fix: two headers in `@object-ui/plugin-dashboard` still said this -package's tests are "compiled by nothing", a debt that has since been paid. The -paragraphs did not merely describe the old state — they prescribed against -writing a compile-time pin in this package, and nearly misrouted the legitimate -one that objectui#6373 landed a few files away. - -- `src/__tests__/MetricWidget.domPassthrough.test.tsx` — the header claimed - `tsconfig.json` excludes the tests, that the package is "the sole remaining - `TEST_DEBT` entry", and that a `@ts-expect-error` written there "would be - especially dishonest". -- `src/domPassthroughPins.ts` — the counterpart half of the same contract - carried the same premise (plus a stale "6 errors, objectui#4118" count), so - fixing only the test file would have left the two halves disagreeing. - -Both now state today's arrangement and name the config that does it: -`tsconfig.test.json` type-checks this package's tests, chained from the -package's `type-check` script (`tsc --noEmit && tsc -p tsconfig.test.json`). -The `src/` placement of the pins is kept and re-framed as a preference rather -than a constraint — they still belong next to the contract and still emit zero -runtime bytes, but a pin in a test file is checked now too. - -Measured on this tree: `tsconfig.test.json --listFiles` includes 81 of this -package's test files (both edited-adjacent files among them); -`node scripts/check-type-check-coverage.mjs` reports `test type-check coverage: -41/41 packages compile their tests, 0 declared debt`; and an `@ts-expect-error` -injected into the test file guarding nothing is reported as -`error TS2578: Unused '@ts-expect-error' directive`. - -No behaviour change, no public surface change. diff --git a/.changeset/6431-param-dialog-close-parity.md b/.changeset/6431-param-dialog-close-parity.md deleted file mode 100644 index 071f1fc0ef..0000000000 --- a/.changeset/6431-param-dialog-close-parity.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`app-shell`'s two action runtimes now reset `ActionParamDialog` the same way when it closes -(objectui#6431). `useConsoleActionRuntime` closed with `setParamState({ open: false, params: [] })` — -replacing the whole state object, emptying `params` and dropping `title`, `description` and -`resolve` — while `RecordDetailView`, which mounts a second runtime into the same dialog, -closed with `setParamState(s => ({ ...s, open: false }))`. The console runtime moves onto the -field-preserving shape. - -The user-visible effect is in the fade-out. `DialogContent` carries -`duration-200 data-[state=closed]:animate-out`, so Radix keeps the content mounted through -its exit animation and the dialog goes on rendering off `state` for the whole ~200ms. Under -the blanking shape a params form the user had just filled in re-titled itself from the -action's own label to the generic "Action parameters", swapped the action's description for -the generic one, and dropped every param row — an empty, generically-labelled box fading out -where a form had been. The confirm pair converged on the same shape for the same reason in -objectui#6034; this is that ruling re-measured on this dialog rather than inherited, because -`ParamDialogState` carries a form rather than display text and "blank it on close" could have -been deliberate here. - -It was not, and nothing else changes for the user. The values a user types are not in -`paramState` at all — they live in `ActionParamDialog`'s own `values` state, which its -`useEffect` reseeds from the param defaults on every `state.open` false→true edge, so a -reopened dialog starts blank under either reset shape. The retained `resolve` is inert: the -dialog settles the promise before it asks for the close, and the open path replaces the whole -state object. diff --git a/.changeset/6436-lockfile-merge-driver-ci-half.md b/.changeset/6436-lockfile-merge-driver-ci-half.md deleted file mode 100644 index 0d3ab89dfc..0000000000 --- a/.changeset/6436-lockfile-merge-driver-ci-half.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Removes the dead CI half of the `pnpm-lock.yaml` merge driver and re-points the mechanism at -what it now is: contributor-facing, with zero CI consumers (objectui#6436, maintainer ruling of -2026-08-27 adopting option B). - -`changeset-release.yml` configured `merge.pnpm-merge` but performs no local merge — the version -branch is updated by `reset --hard` plus a force-push inside `changesets/action`, and a -force-push resolves nothing — so that configuration could never fire. It was the last CI -consumer. `.gitattributes` and CONTRIBUTING.md's contributor path are kept: measured in a -scratch repository with one variable changed, the attribute is what turns a conflicting lockfile -merge into a regeneration instead of conflict markers left inside `pnpm-lock.yaml`. - -The pin test's anti-vacuity weight moves rather than vanishing. `configuring.length > 0` was -doing two jobs — proving the grep matched something, and proving the mechanism had a consumer. -Zero is the correct count now, so job one moves to a control term that must hit the same -population (`git config`, which `changelog.yml` runs) and job two moves to the two files that -are the mechanism today: `.gitattributes` and CONTRIBUTING.md. The phantom direction becomes -load-bearing, since it now asserts the page names no workflow at all. - -Also re-points, for the second time, the worked-example citation in -`scripts/check-pre-install-import-graph.mjs` that cited this config as "a quoted argument that -must not be read as an install". No live in-repo example remains, so it is cited as formerly -`changeset-release.yml`, anchored on the test case's name rather than a line number. diff --git a/.changeset/6442-catalog-blocks-claimant-list.md b/.changeset/6442-catalog-blocks-claimant-list.md deleted file mode 100644 index 724ec3a477..0000000000 --- a/.changeset/6442-catalog-blocks-claimant-list.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in the docs-site gallery host, -`apps/site/app/components/registerCatalogBlocks.ts`. Its `IMPORT ORDER IS CONTRACT` -note justified the import order with a bare-keyword claimant list naming two packages -that do not claim the key: it said bare `chart` is claimed by `plugin-charts`, -`plugin-dashboard` and `plugin-report`. Re-derived with the repo's own -`deriveRegistryKeys` (`scripts/check-doc-component-types.mjs`), bare `chart` has one -package claimant, `plugin-charts` — `plugin-dashboard` registers `dashboard`, -`dashboard-grid`, `metric`, `metric-card`, `object-data-table`, `object-metric`, -`object-pivot` and `pivot`, `plugin-report` registers `report`, `spec-report` and -`report-viewer`, and neither registers `chart` at all. The two `apps/console` sites -that also hold the key are `registerLazy` stubs whose loader is `@object-ui/plugin-charts` -itself, which `Registry.register` drops unconditionally once the real component arrives, -so they are placeholders for that package rather than competing claimants of the keyword. - -The same re-derivation extends the correction to the `calendar` half the issue had not -checked in depth: bare `calendar` likewise has one package claimant, `plugin-calendar`, -because `@object-ui/components` registers the date-picker primitive as `ui:calendar` -with `skipFallback: true` precisely so it does not take the bare keyword. The note -therefore no longer opens by asserting that several packages register the same bare -keyword — measured across the workspace, no bare key is claimed by two of the thirteen -packages this file imports. The mechanism (last registration of a bare keyword wins) and -the reason the order stays fixed are kept. No code, no imports and no assertions changed, -and no published behaviour changes. diff --git a/.changeset/6443-nav-visible-fault-diagnostic.md b/.changeset/6443-nav-visible-fault-diagnostic.md deleted file mode 100644 index 516875ab35..0000000000 --- a/.changeset/6443-nav-visible-fault-diagnostic.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A nav / area / field `visible` predicate that FAULTS now says so, in both builds, once per -distinct predicate source (objectui#6443). Observability only — no verdict moves. - -`ExpressionProvider.evaluateVisibility` is the gate behind a navigation item's `visible`, -an area's derived visibility, and the field list `RecordFormPage` renders. It is fail-open: -a predicate that cannot be evaluated returns `true`, so a menu entry whose role gate has -stopped working renders **for everyone — including the role it was written to exclude** and -looks exactly like an entry the author meant to show. - -That fault was swallowed one layer down. `evaluateCondition` is fail-soft: it answers an -unevaluable predicate with `true` from its own `catch` and does not throw, so this site's -`try/catch` never saw a predicate fault at all. Measured per dialect at this site before the -fix — the bare-string dialect, the one a live gate was measured breaking on, printed -**nothing at all**: - -| dialect | console at this site, before | after | -|---|---|---| -| bare string | nothing | one named line | -| `{ dialect: 'cel' }` envelope | one generic line | one named line (the generic one is *replaced*, not added to) | -| `${…}` template | one generic line **per evaluation** | one named line, deduped | - -The fix wires `EvaluationOptions.onFault` (the seam objectui#6038 landed) to -`reportUnresolvableVisibilityPredicate`, exported from `@object-ui/react` — the same -reporter, message, severity, dedupe `Set` and rate limit the node gate and `page:tabs` -already use, so one authored predicate is entitled to one line rather than one line per -package. It costs no extra engine call: the evaluator hands back the reason at the point it -already knows the predicate faulted, with no `throwOnError` double evaluation. - -A nav item is not a schema node, so the reporter's `type` slot — which, with the gate key -and the predicate source, is the dedupe key — is the constant `app-shell:visible`. The rate -limit is therefore **one line per distinct authored predicate source**, not one per menu -entry: a broken role gate copy-pasted across eight entries is one authoring mistake, in one -string, fixed in one edit. - -**Fail-open is unchanged.** The item still renders for everyone on a fault. Flipping that to -fail-closed is a permission-boundary change, not a diagnostic, and is not this change's to -make; the change makes the silence stop and nothing else. diff --git a/.changeset/6444-evaluator-fault-warn-dedupe.md b/.changeset/6444-evaluator-fault-warn-dedupe.md deleted file mode 100644 index afdf051590..0000000000 --- a/.changeset/6444-evaluator-fault-warn-dedupe.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`ExpressionEvaluator.evaluate` now reports a faulting `${…}` at most **once per authored -source** instead of once per evaluation (objectui#6444). It is the hottest of the three -predicate paths in this area — `SchemaRenderer` calls it for every `properties.*` value, -every `props.*` value and `content`, for every node, on every render — so a single broken -`${…}` prop in a 200-row list wrote 200 console lines per render, and 200 more on the next -one. Measured on the built evaluator before the fix: three identical faulting -`evaluateCondition` calls produced 3 lines where the `{ dialect: 'cel' }` envelope produced -1; the 200-row list produced 200. After: 1 in every case. - -This is the one-per-source rate limit both sibling reporters already carry -(`warnPredicateFailure` in `fieldRules.ts`, `visibilityDiagnostic.ts` in `@object-ui/react`), -not a third mechanism. The dedupe key is the predicate's **authoring** identity — the fault -site plus the source text, never the scope it ran against — which is both the siblings' -precedent and the defect itself: the 200-row flood is one authored source evaluated against -200 distinct scopes, so a scope-sensitive key would emit all 200 lines again. - -Nothing else moves. The two message texts are unchanged, a distinct broken source still gets -its own line, `EvaluationOptions.onFault` still fires on every fault (objectui#6038's passback -contract, so a caller doing its own warn-once bookkeeping keeps control of it), `throwOnError` -still throws on every evaluation, and no symbol is added to the published surface. diff --git a/.changeset/6445-disabled-gate-fault-diagnostic.md b/.changeset/6445-disabled-gate-fault-diagnostic.md deleted file mode 100644 index ac2eff72dd..0000000000 --- a/.changeset/6445-disabled-gate-fault-diagnostic.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/react': minor ---- - -A `disabled` / `disabledOn` predicate that cannot be evaluated is now reported on the console, in development **and** in production — and the message says what this gate's fail-soft default actually did. - -`SchemaRenderer` routes six visibility legs (`visibleWhen`, `visible`, `visibleOn`, `visibility`, `hidden`, `hiddenOn`) through one reporter, and called `evaluateCondition` **bare** on the two enablement legs — the only uninstrumented predicate pair in the file. A faulting `disabled` predicate had therefore never been reported in any build, in any dialect that does not report on its own. - -It is also the pair whose fail-soft answer **bites**. `evaluateCondition` answers an unevaluable predicate with `true`; on the negated visibility legs that means SHOWN, here it means GREYED OUT. So the user got a control they could see and could not use, and the author got nothing to grep for. - -- **Wiring only, one engine call.** Both legs pass `EvaluationOptions.onFault` (objectui#6038's seam), which hands back the fault the evaluator has already caught. No `throwOnError`, no second evaluation, no `__DEV__` split — dev and production print identical bytes. -- **No verdict moves.** The fail-soft `true` is preserved deliberately: a faulting `disabled` predicate still disables, exactly as before. Flipping that is a shipped-behaviour change and is not part of this. -- **Its own copy, not the visibility reporter's.** The shipped line says the safe default meant the gate "did NOT bite", which is written about a gate that shows the node. This gate's line says the opposite, because the opposite is true: `[ObjectUI] An enablement predicate could not be evaluated`, then the node, the key, the source, the engine's reason, and that the node renders disabled — on screen, greyed out — with nothing else on screen to say a predicate failed. One reporter, one dedupe, one severity; a second message. -- **Rate-limited exactly as the visibility gate is**, per `(node type, key, predicate source)`: two hundred rows of one broken predicate print one line, a second distinct source still prints, and the same source authored on `disabled` and on `visibleWhen` prints two — the gates cannot silence each other. diff --git a/.changeset/6447-pnpm-ledger-manifest-anchor.md b/.changeset/6447-pnpm-ledger-manifest-anchor.md deleted file mode 100644 index 95acc6e9cb..0000000000 --- a/.changeset/6447-pnpm-ledger-manifest-anchor.md +++ /dev/null @@ -1,37 +0,0 @@ ---- ---- - -The version-claim ledger's two `pnpm 10.x` entries now rest on a run instead of a sentence -(objectui#6447). Test-only: no package changes, so this changeset declares no bump. - -`scripts/__tests__/doc-version-claims.test.ts` classified both entries `anchored` on the -strength of a `why` string that said "17 `corepack enable` steps across 12 workflow files". -The tree holds 20 across 14 and has since four days after that sentence was typed. Nothing -re-took the measurement, which is the exact defect the file exists to notice, sitting in the -strongest class it has — the second site of it, after objectui#6400 moved the `Node 22.x` -entries' count into a derivation. - -- New `manifestVersionField` on `KnownClaim`, the third way an entry names its anchor and - has the comparison RUN: it names a field of the root `package.json` whose value is the - anchor. A second source field rather than one more spelling on objectui#6400's - `workflowVersionKey`, because the two read different kinds of thing — that one sweeps a - directory for every declaration of a GitHub-Actions key and reduces them to one major, - this one reads a single field holding a tool and a version welded with an at-sign. -- The comparison judges BOTH halves. A version-only check would stay green if this - workspace switched package managers and kept a major — a page teaching `pnpm 10.x` - against a field naming something else, with the gate reporting agreement. So the tool the - field names is compared against the toolchain word the claim leads with, read with the - same `TOOLCHAIN` alternation the scan matched that word with; no new vocabulary enters - the file for it. -- Both `pnpm 10.x` reasons lose their `corepack enable` count. The count was never the - anchor — it was the argument for why the field is the anchor, and that argument does not - need a live number to stand. -- The docblock lost its own frozen counts under one stated policy: a count describing the - CURRENT tree is removed and replaced by the run that re-takes it, while a count recording - what a change MEASURED when it landed is kept and attributed to that change, so it reads - as history. A card about frozen counts cannot leave frozen counts in the prose arguing - for the reason strings. - -Both directions are pinned permanently by a manifest fixture rather than demonstrated once: -a major bump goes red on the version, and a tool swap that KEEPS the major goes red on the -tool — the case a version-only comparison would have called agreement. diff --git a/.changeset/6448-ci-cd-pipeline-node-version-spelling.md b/.changeset/6448-ci-cd-pipeline-node-version-spelling.md deleted file mode 100644 index 0d37d272f5..0000000000 --- a/.changeset/6448-ci-cd-pipeline-node-version-spelling.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Drops one prose sentence in `content/docs/guide/ci-cd-pipeline.md` off a spelling-level -absolute (objectui#6448). The "adding a new workflow" steps used to say a fossilised -example block pinned `node-version: 20` "where every workflow declares `'22.x'`" — but -one lane, `.github/workflows/half-state-patrol.yml`, spells it `'22'` rather than -`'22.x'`, so the quoted claim was false by exactly that one lane's quoting, even though -every workflow agrees on the major. - -Re-measured on the branch point (`d3bf4fa6f`): 29 declarations spell `'22.x'`, 1 spells -`'22'`, all 30 read major 22. The sentence now reads "where every workflow declares 22" — -true today, and it stays true if `half-state-patrol.yml` is later normalised to `'22.x'` -(a separate change, objectui#5986), because the sentence no longer asserts a spelling. - -This is disposition A from the finding, the one both the card and triage recommended: -the paragraph is teaching "don't copy a fossilised pin", and the quote-mark spelling of -the current value was never part of that lesson — asserting it was is exactly the -count/spelling-shaped fact objectui#6400 and #6447 are open about elsewhere on this same -page. No other part of the paragraph changed (the `@v7` and `pnpm/action-setup@v4` claims -still measure true), and `scripts/__tests__/doc-version-claims.test.ts`'s ledger entry for -this sentence (`node-version: 20`, landed by objectui#6409) needed no edit — its `why` was -already phrased at the version level, not the spelling, so page and ledger now agree. - -Docs-only prose fix; no published behaviour changes, hence empty frontmatter. diff --git a/.changeset/6453-calendar-expand-gate.md b/.changeset/6453-calendar-expand-gate.md deleted file mode 100644 index 4400c0ec3a..0000000000 --- a/.changeset/6453-calendar-expand-gate.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -A standalone `object-calendar` bound to an object now queries WITH its `$expand`, so -lookup / master_detail / user / tree fields render the related record instead of a raw -foreign-key id (objectui#6453). - -`ObjectCalendar`'s fetch effect built its expand set from a ref assigned in the render body -(`objectSchemaRef.current = objectSchema`) and left `objectSchema` out of its dependency -list. That bought the effect exactly one run per mount and paid for it with the expansion, -permanently: on that one run the ref was still `null`, `buildExpandFields` saw no fields, -the query went out with no `$expand` at all, and nothing re-ran the effect when the schema -landed. Only the standalone calendar reached this path — one hosted by `ObjectView` or -`ListView` receives its rows as `data`, which objectui#6419 already covers. - -The ref is replaced by a settled-and-keyed resolution (`{ key, def } | null`) that GATES the -record query, the third member of the family after objectui#6271 (`ObjectKanban`) and -objectui#6419 (`ObjectView`). Measured on this component rather than inherited: gated, the -calendar issues one query carrying `$expand` in every latency profile; the alternative of -adding `objectSchema` to the dependency list issued two, and when the schema read was the -slower of the two it painted raw ids, reverted to the "Loading calendar..." placeholder, -then swapped — a three-step paint the correct rows do not arrive any later than. - -The gate is on the schema read having SETTLED, never on a truthy schema: an adapter that -exposes no `getObjectSchema`, and a read that throws, both settle with nothing and the -calendar still queries (unexpanded) rather than waiting forever. An inline `value` data set -is deliberately not gated — it issues no metadata read, so there would be no resolution to -wait for. diff --git a/.changeset/6455-listview-sort-persist-leak.md b/.changeset/6455-listview-sort-persist-leak.md deleted file mode 100644 index c082fcf6a1..0000000000 --- a/.changeset/6455-listview-sort-persist-leak.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`ListView`'s toolbar sort picker no longer persists a sort the platform refuses to order by. - -The picker keeps a platform-refused field listed while the CURRENT sort names it -(#6108). That exception is deliberate and stays: it is the only way a user can -REMOVE a sort the server answers `400 INVALID_SORT` for — withholding the option -unconditionally renders a blank row nobody can delete, and drops the sort silently -on the next edit. - -What was wrong is that the picker rendered and emitted from the same array. Editing -anything ELSE in that popover — adding a second sort key, resetting to the view's -default — re-emitted the whole array with the refused entry still in it, and the -host's `onSortChange` turned that into `persistViewPatch({ sort })`: a -personalization PUT storing a refused column, written by a user who never touched -that row. A view stored before the sortability signal existed therefore kept -re-persisting its refused `$orderby` indefinitely. - -Every `onSortChange` this component emits — the builder, the column-header sort and -"reset to default" — now crosses one boundary that drops what the served projection -refuses, while `currentSort` keeps the array whole. So what the picker LISTS and -what it PERSISTS are separate: the refused entry stays visible and removable, -removing it persists the removal, and no write carries it. This is the separation -#5729 already made at the grid seam (`ObjectGrid`'s `manualSort` / -`manualOnSortChange` pair); the picker was the second door onto the same stored -view state. - -Only under a served sortability projection (objectstack#10235 ruling A). `undefined` -means NO SIGNAL SERVED — an older deployment, an inline/mock data source — not -"nothing is sortable", and that branch is byte-identical in behaviour to before. diff --git a/.changeset/6457-persisted-column-width-key.md b/.changeset/6457-persisted-column-width-key.md deleted file mode 100644 index ba3c1dcde7..0000000000 --- a/.changeset/6457-persisted-column-width-key.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -ObjectGrid now restores a persisted column width on the ungrouped path — the stamp key was -`size`, which nothing downstream reads (objectui#6457). - -Resize a grid column and reload: the width came back. It was written to `localStorage` -(and reported to the host, which persists it through `dataSource.updateViewConfig`), read -back into `columnState.widths`, and stamped onto the column — as `size`. `TableColumn` -declares `width`, and `data-table` resolves a column's width at all four of its sites as -`columnWidths[accessorKey] || col.width || autoSizedWidths[accessorKey]`; it reads no -column-level `size` anywhere, and ObjectGrid never passes a `columnWidths` prop down. So -the round trip completed and was discarded at the last hop, and the column fell back to -the char-estimate auto width. The `persistedColumns` map now stamps `width`. - -The correct key was not a judgement call: the **grouped** path in the same component reads -the same `columnState.widths` and has always stamped `width`, and it worked. One path was -out of step with its sibling — so this restores a convention rather than teaching -`data-table` a second spelling. `TableColumn` is not edited: the consumer's declaration -was the correct one. Precedence is unchanged and needs no change — a persisted width still -loses to an in-session resize and still beats auto-sizing. - -Two things stop it recurring. The map's callback is no longer `(col: any)`: typed as -`ObjectGridColumn` (`TableColumn & …`, declared since objectui#6004), a stray `size` here -is now a compile error instead of a silent, user-visible drop — the `any` was what let the -wrong key cross a boundary that had already declared the right one. And the new pin is the -**inbound** half — a persisted width seeded through both channels, asserted at the rendered -header cell. The pre-existing suite asserted only the outbound half, which passes on the -broken code, because the write is exactly what was wrong; that is the measured reason this -shipped. diff --git a/.changeset/6458-retire-undeclared-column-reads.md b/.changeset/6458-retire-undeclared-column-reads.md deleted file mode 100644 index 9fd2cefec4..0000000000 --- a/.changeset/6458-retire-undeclared-column-reads.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -Retire four undeclared authored column reads in `ObjectGrid.generateColumns()` -(objectui#6458). - -`ListColumnSchema` is a strict object, so an author who wrote `format`, -`options`, `appearance` or `essential` on a list column was **refused at -publish** with `unrecognized_keys` while the grid happily honoured the key at -runtime. That is the `declared != enforced` split AGENTS.md #0.1 exists to -stop, and it was reachable only through `(col as any)`. All four reads are -removed. Each was re-measured on this branch before deletion: **zero authored -occurrences on a column across `examples/` and `apps/`**, per key, with `field` -as the positive control in the same query shape — so no real author's metadata -changes behaviour today. - -What each retirement leaves as the only road: - -- `format` and `appearance` — the object-field fallback (`objectDefField?.format` - / `?.appearance`), which is already the road every measured author uses. -- `options` — the object schema's select options. The column-level override was - exactly the shape that let AI-authored metadata drift from the schema it is - supposed to obey; one source for options beats two. -- `essential` — mobile visibility stays positional (`colIndex === 0`). - -**Retired for want of authors, not forbidden forever.** If a real request for -semantic mobile-column control arrives, the declare route reopens: declare the -key on `@objectstack/spec` and read it without a cast. What stays ruled out is -the third road — a renderer-side tolerance for a key the schema refuses. - -`columnReadBoundary-6458.test.ts` moves with the change: its bound on undeclared -cast reads in that branch is now the **empty set**, so a new one goes red on -arrival rather than accreting quietly. diff --git a/.changeset/6459-grid-data-table-schema-slot.md b/.changeset/6459-grid-data-table-schema-slot.md deleted file mode 100644 index fc46f0ffec..0000000000 --- a/.changeset/6459-grid-data-table-schema-slot.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -feat(plugin-grid): type the data-table schema slot ObjectGrid fills - -`const dataTableSchema: any` becomes `ObjectGridDataTableSchema`, and -`buildGroupTableSchema`'s return carries the same annotation, so the ~46 keys -this grid writes into the `data-table` slot are finally checked against the -slot's declaration — the receiver half of the seam whose producer half #6004 -typed. The `(dataTableSchema.columns as any[])` cast in the grouped writer -drops with it. - -The annotation is not a bare `DataTableSchema`, and that is the substance: -measured on this program, a bare annotation with an undeclared bogus key -written longhand in the fresh literal compiles with **zero** diagnostics, -because `BaseSchema`'s `[key: string]: any` index signature makes every key a -member — excess-property checking never has a non-member to refuse. -`ObjectGridDataTableSchema` derives from `DataTableSchema` by stripping the -index signature (never by hand-listing members), which makes the same probe go -red (TS2353) at both writer literals — shown able to fail before being claimed -as coverage, per the #6004 rule. - -Two schema-level keys the grid passes are undeclared on `DataTableSchema` and -HELD at the seam with measured live readers in `data-table`: -`renderCellEditor` and `cellClassName`. Whether `DataTableSchema` should -declare them is filed for a ruling — nothing is declared on or retired from -`@object-ui/types` here. diff --git a/.changeset/6460-objectview-fetch-view-identity.md b/.changeset/6460-objectview-fetch-view-identity.md deleted file mode 100644 index 0b919f24c1..0000000000 --- a/.changeset/6460-objectview-fetch-view-identity.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView`'s non-grid fetch no longer re-queries once per parent render when the host -passes an inline `views` array (objectui#6460). - -The effect that fetches rows for the six non-grid view types (kanban, calendar, gallery, -timeline, gantt, map) listed `activeView` — an **element of the `views` prop array** — among -its dependencies. A host writing `views={[{ id: 'cal', type: 'calendar', label: … }]}`, which -is how this component's own docs write it, produces a fresh element object on every one of -its own renders, so the dependency changed identity every render and a new `find()` went out -each time. Measured with an instrumented adapter and three parent re-renders: **4 queries -where a hoisted array gives 1**. Because `ObjectView` hands its rows to the child view as -`data={data}`, each extra query also re-delivered a fresh row array downstream — the -"duplicate events in child views like the calendar" hazard, from the re-run direction. - -The effect now depends on the **values it reads** — the active view's `filter` and `sort`, -plus its `id` — held at a steady reference while they are structurally unchanged, instead of -on the view object's identity. Asking hosts to hoist the array was considered and rejected: -that is a contract change on every caller of a published component, and it leaves the defect -live for every host that does not comply. - -Nothing about precedence moves: a named `listViews` config's `filter`/`sort` still outrank -the view's, which still outrank `table.filter`/`table.sort` and their deprecated aliases. -Changing a view's filter, changing its sort, and switching the active view all still -re-fetch. The comparison never serializes, so it stays correct for filter and sort values -that have no faithful stringification — a `Date`, a function, a `Map`, `NaN`, or plain -key-order instability — and every case it cannot model resolves to "changed", which costs a -redundant query rather than withholding a needed one. diff --git a/.changeset/6464-recall-submitter-gate.md b/.changeset/6464-recall-submitter-gate.md deleted file mode 100644 index 541e31c934..0000000000 --- a/.changeset/6464-recall-submitter-gate.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -The record page's approval band offers its **Recall** button to the approval's submitter -only (objectui#6464). - -Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval; -user B — not the submitter, read access, not an admin — opens the record and the band still -lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes -on submitter identity and refuses everyone else, so the only outcome available to that -button was a failure toast. Record state was never at risk; this was purely a -writability-feedback mismatch, the same family as objectui#3794. - -The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at -all" — which is a question about the DataSource, not about the viewer. Identity now joins -it, threaded the way every other signal on that band already travels: the HOST resolves it -and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and -never re-derives who submitted what. - -- `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on - `InlineEditContextValue`. Additive and optional; no existing prop changes. -- `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a - resolved `false`. -- `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing - approvals read and threads it. - -**The signal is tri-state, and the third state is the load-bearing one.** `true` offers -recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity -— renders exactly as it did before this release.** Omission preserving prior behaviour -mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide" -would have traded a cosmetic defect for a functional loss: every host whose band runs off -the record's `approval_status` mirror alone would silently lose its submitter's only way to -unlock their own record. - -**Withdrawn rather than disabled-with-reason.** The card offered either. For a -non-submitter this control is never actionable on any pending record, so a permanently -disabled button is standing clutter rather than a lesson; and the two sibling submitter -levers already hide — the approvals panel's Remind button, and the declared -`approval_recall` action's `visible` predicate. The band, its quorum tally and the -approvals timeline still tell a non-submitter exactly what state the record is in. Only the -lever they can never pull is gone. - -**This changes no permission.** Nothing about what the server allows moves, `canEdit` and -the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an -authorization verdict — the recall endpoint remains the sole authority, and it refused -these callers before this change and refuses them after. There is deliberately **no admin -carve-out** (the reporter ruled that case out, cf. objectstack#9464). - -The derivation itself is now one function, `isSubmitterOf` — server-resolved -`viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends -that predate it, joined with `??` so a server that resolved `false` is believed rather than -re-litigated client-side. The approvals panel's Remind gate, which already carried that -expression inline and whose behaviour is unchanged, now reads the same answer: two copies -would have been two definitions of who submitted. - -The **untranslated refusal text** the reporter also saw ("No pending approval request found -for this record", concatenated after a localized prefix) is a separate defect and is not -addressed here; it is tracked on objectstack#11993. diff --git a/.changeset/6465-stale-typecheck-headers.md b/.changeset/6465-stale-typecheck-headers.md deleted file mode 100644 index 755b985bf4..0000000000 --- a/.changeset/6465-stale-typecheck-headers.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Comment-only fix: eleven test headers across `@object-ui/app-shell` and -`@object-ui/core` still described a type-checking arrangement that no longer -exists. Two distinct stale claims, both now corrected: - -1. **"this package's tests are compiled by nothing."** Both packages graduated - in objectui#4040; each `type-check` script is now - `tsc --noEmit && tsc -p tsconfig.test.json`, and `TEST_DEBT` in - `scripts/check-type-check-coverage.mjs` is literally `{}`. -2. **Present-tense citations of `tsconfig.typetests.json`,** of which zero exist - repo-wide (objectui#4291 retired the last of them). Several headers told the - reader that a listing in such a project is what makes their assertions - load-bearing — a file the reader cannot find. - -Both matter beyond tidiness because several of these headers *prescribe*: one -said a `@ts-expect-error` here "would be read by no compiler", another that "a -new type-assertion test file is unchecked until it is added to that include -list". Both instruct the next author away from assertions that are in fact -checked, and away from the config that checks them. That is the same shape -objectui#6426 was filed and fixed for in `@object-ui/plugin-dashboard`. - -Where a header is now positive ("this file IS compiled"), the claim was -confirmed for **that file**, not inferred from the package-level coverage -reading: `tsc -p tsconfig.test.json --listFiles` lists all 8 edited app-shell -files (of 4455) and all 4 edited core files (of 634), and both projects exit 0. -Surviving mentions of `TEST_DEBT` and `tsconfig.typetests.json` in these files -are deliberate, and are now past-tense — they record why the header used to say -otherwise. - -No behaviour change, no public surface change, no assertion or directive -touched. diff --git a/.changeset/6467-unused-import-gate.md b/.changeset/6467-unused-import-gate.md deleted file mode 100644 index 091ea17dd2..0000000000 --- a/.changeset/6467-unused-import-gate.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -No published behaviour changes. Promotes the unused-import subclass of -`@typescript-eslint/no-unused-vars` to `error` (objectui#4806 R2, #6467) and -removes the 108 unused imports across 21 packages that the promotion finds. -Every edit is the deletion of an import binding that nothing referenced — -no runtime code, no exported API and no type surface moves, and the removals -are the upstream rule's own fixer output rather than hand edits. diff --git a/.changeset/6469-gantt-block-precedence.md b/.changeset/6469-gantt-block-precedence.md deleted file mode 100644 index ffb8feea59..0000000000 --- a/.changeset/6469-gantt-block-precedence.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-gantt': minor -'@object-ui/types': minor ---- - -**plugin-gantt: the `gantt` block now outranks the flat top-level spelling, and -the losing face's keys are named instead of dropped.** - -`getGanttConfig` checked the flattened top-level spelling first and returned -early, so a node carrying both spellings rendered the flat one and every key -inside an authored `gantt` block was discarded with **no diagnostic** — not even -the `GanttConfigSchema.safeParse` warning, which sat behind that early return. - -`plugin-map` had the identical two-faces shape ruled the other way (maintainer -ruling on objectui#5018, 2026-08-17, landed in PR #5156): the block wins, with a -dev-mode warning naming the ignored top-level keys. objectui#6469 inherits that -ruling, so the two sibling view plugins now answer the same question the same -way. - -What changes: - -- A node carrying **both** spellings now renders the **`gantt` block's** values. - The block is taken **whole** — the flat keys are not merged into it. -- In dev, `[ObjectGantt] … these top-level keys are IGNORED: …` names every - shadowed flat key, once per distinct shadowing. -- Nothing else moves. A node with only the flat spelling, or only a block, is - read exactly as before. - -**Producer-safe:** `ObjectView` (`case 'gantt'`) and `ListView` (`case 'gantt'`) -both flatten `options.gantt` onto top-level keys and emit **no** `gantt` key, so -every gantt reached through either view layer still takes the flat branch, and -the new warning cannot fire on that path. This is the same producer check the -`plugin-map` flip pinned, re-run on today's `main`. - -This **supersedes** the precedence sentence in the objectui#6051 changeset -(`.changeset/6051-gantt-flat-config-declared-keys.md`), which recorded the flat -branch winning — accurate for that change, which deliberately did not touch -precedence, and reversed by this one. diff --git a/.changeset/6470-gantt-dependency-field-deprecated.md b/.changeset/6470-gantt-dependency-field-deprecated.md deleted file mode 100644 index 74346edef4..0000000000 --- a/.changeset/6470-gantt-dependency-field-deprecated.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/plugin-gantt': patch ---- - -`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published -declaration faces, naming `dependenciesField` as the canonical spelling -(objectui#6470). Nothing is removed and nothing is rejected that was accepted -before. - -**What the two spellings were.** `ObjectGanttSchema` declares both, and -`getGanttConfig`'s flat branch reads them with a `||`: -`dependenciesField: schema.dependenciesField || schema.dependencyField`. -`dependenciesField` is the spec's key (`@objectstack/spec` -`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO -spec counterpart — zero occurrences across `packages/spec/src`, measured against -a live positive control on the plural. Until objectui#6051 declared the plural, -the singular was the ONLY dependencies spelling this interface carried, so for -the whole time the alias existed the published type taught the non-spec key and -hid the canonical one. - -**What was missing was the ranking, not the behaviour.** The two were declared as -equals: nothing on either face said which one to author, so a reader — including -an AI writing metadata, which is the reader this project optimises for — had a -coin flip between a spec key and pre-spec vocabulary. The marker turns that coin -flip into a fact the type itself carries, and the zod mirror's description makes -it readable at runtime as well as in an editor. - -This adopts the idiom already ruled for this exact shape rather than inventing a -second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry -`/** @deprecated legacy alias for the spec's X */` plus -`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same -way. - -**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the -renderer's `||` — would break every author who wrote the singular and narrow the -accept set of a published surface. That is a maintainer decision on a future -enforce-or-remove card once the deprecation has sat a release, and it is -explicitly excluded here. Two pins hold the line in both directions: -`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts` -fails if the marker goes missing AND if the alias stops being declared or -accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx` -fails if the `||` limb is dropped — the two spellings must keep resolving to the -same config, with the canonical one winning when both carry a value. - -`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the -singular; it was the only in-repo site that did, and it now authors the plural -with the alias named as legacy. No runtime code, fixture, example app or catalog -schema authored it. diff --git a/.changeset/6471-ganttconfigex-restatement-pin.md b/.changeset/6471-ganttconfigex-restatement-pin.md deleted file mode 100644 index f919b860f2..0000000000 --- a/.changeset/6471-ganttconfigex-restatement-pin.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Pin every member `plugin-gantt`'s `GanttConfigEx` restates on top of `GanttConfig` -against its twin, so two declarations of one key can no longer drift apart in silence -(objectui#6471). - -**No release.** Type-only and package-private: no runtime code changes, and the published -surface is untouched on both sides. `packages/types` has zero changed files, so -`GanttConfig`'s member list in the built `dist/index.d.ts` is byte-for-byte what it was; -the new `GanttConfigRestated` alias is exported from `ObjectGantt.tsx` for the pin to -import but is not re-exported by the package entry, and `plugin-gantt/dist/index.d.ts` -carries zero occurrences of it after a real build (positive control: `QuickFilterDef`, -which *is* re-exported, appears there). - -`GanttConfigEx` is `GanttConfig & { … }` and the intersection's second half re-declares -keys the first half already declares — nine of them arriving from the spec's -`GanttConfigSchema`, which declares 19 keys as of rc.6. The restatements are deliberate: -their JSDoc is the only prose in this repo describing what the renderer does with each -key, and the spec emits no per-member documentation, so deleting the members deletes the -documentation. What was missing was any assertion that the two declarations still agree. - -The local half is now a named type, and that is the whole mechanism rather than a tidying -step. An assertion phrased over `GanttConfigEx` cannot measure anything: `GanttConfigEx[K]` -is already `GanttConfig[K] & local[K]`, so it is assignable to `GanttConfig[K]` by -construction and stays green no matter how far the two declarations drift. Naming the -local half gives the pin two independent operands. - -Measured, not assumed: **all twelve** restated members are mutually assignable with their -`GanttConfig` twin today — including `quickFilters` and `timeSegments`, which the card and -its triage both describe as load-bearing NARROWINGS. On current `main` they narrow -nothing. objectui#6051/#6472 lifted `timeSegments` onto `GanttConfig` in the shape that is -structurally `ShiftSegmentsConfig`, and rc.6's `GanttConfigSchema.quickFilters` already -models `field` / `label` / `options` exactly as `QuickFilterDef` does. Both are kept and -pinned as a measured state rather than deleted, so a future spec bump surfaces as a -decision instead of a silent widening. diff --git a/.changeset/6473-metadatatypeactions-param-dialog-close.md b/.changeset/6473-metadatatypeactions-param-dialog-close.md deleted file mode 100644 index a1be13d209..0000000000 --- a/.changeset/6473-metadatatypeactions-param-dialog-close.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The metadata-admin type-action param dialog no longer blanks itself while it closes -(objectui#6473). - -`MetadataTypeActions` is the **third** consumer of `ActionParamDialog` in this package, -after `useConsoleActionRuntime` and `RecordDetailView` — and it was the last one still -writing `setParamState({ open: false, params: [] })` on close, replacing the whole state -object. `DialogContent` carries `duration-200 data-[state=closed]:animate-out`, so Radix -holds the content mounted through its exit animation and the dialog goes on rendering off -`state` for the whole fade-out: a user who cancelled "Test connection" or a datasource -sync watched the heading revert from the action's own label to the generic -`actionDialog.title` and every param row disappear, for 200ms, on the way out. Close now -flips `open` and keeps every other field, which is the shape objectui#6431 converged the -other two consumers on. - -Not a user-data change: the values typed into the dialog never lived in `paramState` — -they live in `ActionParamDialog`'s own `values`, reseeded from the param defaults on every -open — so a reopen still starts from the defaults, pinned as a control. - -The pre-reset `paramState.resolve?.(null)` is dropped as well, on an enumeration rather -than on "resolving twice is a no-op": `onOpenChange` is reachable from exactly three places, -all inside `ActionParamDialog`, and every one settles the promise before asking for the -close — `handleSubmit`, `handleCancel`, and the Radix root handler that delegates to -`handleCancel` (the single route Escape, an overlay click and the header close button all -take). All four routes are driven in the new test, with a census over -`ActionParamDialog.tsx` so a later call site that skipped the settle is red there instead -of leaving a promise pending forever. diff --git a/.changeset/6475-gantt-block-face-declared.md b/.changeset/6475-gantt-block-face-declared.md deleted file mode 100644 index 20c9f2ec69..0000000000 --- a/.changeset/6475-gantt-block-face-declared.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`ObjectGanttSchema` now declares the `gantt` BLOCK face — and the spec's -required trio enforces at validate/check time.** - -The `gantt` nested-block spelling of a gantt config (`{ type: 'object-gantt', -gantt: { … } }`) had **no mirror entry at all**: it rode through -`BaseSchema`'s `.passthrough()` entirely unvalidated, and the published -TypeScript never taught the shape either — an author writing `gantt: { … }` -got no completion, no type checking, no error on a misspelt member. It was -the 28th and last of the keys `getGanttConfig` (`plugin-gantt/src/ObjectGantt.tsx`) -reads off the schema that objectui#6051 (PR #6472) did not declare — severed -into its own card because, unlike the other 27, declaring it changes what a -published tool refuses. - -Both faces now declare it: - -```ts -// packages/types/src/objectql.ts -gantt?: GanttConfig; - -// packages/types/src/zod/objectql.zod.ts -gantt: SpecGanttConfigSchema.extend(GanttConfigExtensionFields).optional(), -``` - -`GanttConfig` derives from the spec's `GanttConfigSchema`, which has required -`startDateField`, `endDateField` and `titleField` since rc.6. - -**What the CLI now refuses that it accepted before:** `ObjectGanttSchema` is a -member of `AnyComponentSchema`, so it reaches `safeValidateSchema` and -therefore the CLI's `validate` and `check` commands. A `gantt` block missing -any of the three required fields — previously accepted silently — is now -**refused**, naming the missing field. A block carrying all three, or a -schema with no `gantt` block at all, is accepted exactly as before. - -**This is a `declared = enforced` restoration, not new requiredness.** -`getGanttConfig`'s block branch already fed the block to -`GanttConfigSchema.safeParse` and logged `[ObjectGantt] Invalid gantt -configuration` to the console on failure — a block missing the trio was -already non-functional at runtime, silently. What changes is *when* the -author is told: runtime console warning becomes an authoring-time refusal. - -Maintainer ruling, objectui#6475 (2026-08-27), **Option A** — enforce as-is, -immediately, no warning window (the startup-stage no-gradualism rule, -objectstack#12668: transitions do not get phased windows without named -external-user evidence, and none exists here). A census of every `gantt` -block reachable through `ObjectGanttSchema` in this repository — the -`examples/schema-catalog` fixtures, `content/docs/plugins/plugin-gantt.mdx`, -and the published `skills/objectui/guides/page-builder.md` guide — found -**zero** blocks missing the trio. diff --git a/.changeset/6477-buildconflict-narrowed-type.md b/.changeset/6477-buildconflict-narrowed-type.md deleted file mode 100644 index 770cdc3fa7..0000000000 --- a/.changeset/6477-buildconflict-narrowed-type.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Type-only change in `@object-ui/plugin-detail`: `InlineEditSaveBar`'s internal -`buildConflict` callback now takes the type `isConcurrentUpdateError` narrows to, -instead of `any`, and the `as Record | null` cast that the `any` -made necessary is gone. - -Nothing releases. Measured, not assumed: building the package before and after -the change and comparing all 52 emitted `.js` / `.cjs` / `.d.ts` artefacts leaves -51 byte-identical, including `dist/index.js`, `dist/index.umd.cjs` and -`dist/index.d.ts`. The single delta is `dist/InlineEditSaveBar.d.ts`, which gains -the `BuildConflict` type its pin test reads; the package's `exports` map declares -only `"."`, so that file is not resolvable by any consumer, and the published -`dist/index.d.ts` does not mention the name. Published behaviour and published -types are both unchanged. diff --git a/.changeset/6480-strip-read-decorations.md b/.changeset/6480-strip-read-decorations.md deleted file mode 100644 index 37be1ae602..0000000000 --- a/.changeset/6480-strip-read-decorations.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService.saveFields` no longer PUTs the framework's own read decorations back (objectui#6480). - -`saveFields` fetches the current object and spreads it verbatim (`...existingObject`) so that every key the service does not model survives a field save. That spread does not distinguish keys the **author** owns from keys the **framework** adds on the way out: `@objectstack/spec` declares `_diagnostics` and `_draft` as `METADATA_READ_DECORATIONS` — stamped onto served metadata documents by the read path — and `ObjectSchema` refuses both **by name**. A served document carrying either one was therefore spread straight back into the body of `PUT /api/v1/meta/object/:name`. - -The body now passes through the spec's own exported `stripReadDecorations` before it is sent, so the list of decorations stays the spec's rather than a local copy that goes stale the next time the framework adds one. This is the strip-on-write shape `MetadataObjectsPage.handleObjectsChange` already uses for `group`, applied where the spread is — simply not writing the key is not enough when the spread is verbatim. - -The strip is deliberately bounded to those two keys and is not a general "remove whatever the schema refuses" pass: an off-spec key the author owns still goes out and is still refused loudly, where someone can see it. Nothing is lost by dropping the decorations even though a PUT is an upsert — `_diagnostics` is the read-path validation verdict, recomputed on every read, and `_draft` reflects the row's `state` column and the `mode` parameter, never the body. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) *is* write-path state the server merges back, and the spec deliberately keeps it out of the decoration list, so it is untouched. diff --git a/.changeset/6481-objecttree-keyed-schema-latch.md b/.changeset/6481-objecttree-keyed-schema-latch.md deleted file mode 100644 index 9b0ede668f..0000000000 --- a/.changeset/6481-objecttree-keyed-schema-latch.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-tree': patch ---- - -`ObjectTree` no longer queries a switched-to object with the previous object's -`$expand` set (objectui#6481). - -The schema-settled gate was two separate pieces of state — the definition -(`objectSchema`) and a bare `schemaSettled` boolean that was set `true` in a -`finally` and never reset. Two independent values cannot express "settled, but -for a DIFFERENT object", so when the host swapped the bound object both effects -re-ran while the latch still read `true` from the previous object's settle and -the definition still held the previous object's fields. The tree issued - - find(newObject, { $filter: …, $expand: [ …previous object's relation fields… ] }) - -— rejected or silently ignored depending on the adapter, plus the transient it -painted — before a correct second query followed. - -`ObjectTree` now adopts `useSettledSchema` from `@object-ui/react` (the shared -resolution hook ruled in objectui#6482), replacing BOTH pieces of state with the -hook's single `{ key, def } | null`. Readiness is derived during render by -comparing the settled key against the currently bound object, so the gate closes -in the same commit that changes the object rather than one commit later — the -stale-key window is not merely fixed but unrepresentable. - -Behaviour that deliberately does NOT change: the gate stays inside the -object-provider branch of the record effect (the inline/static branches issue no -metadata read and must not wait on one), and every exit still settles — no -`getObjectSchema`, no object name, or a rejected read each settle with no -definition, so a tree whose adapter serves no schema still queries instead of -waiting forever. diff --git a/.changeset/6487-visibility-advice-per-tier.md b/.changeset/6487-visibility-advice-per-tier.md deleted file mode 100644 index 7750f26b92..0000000000 --- a/.changeset/6487-visibility-advice-per-tier.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/app-shell': minor -'@object-ui/components': minor ---- - -**The unresolvable-visibility-predicate report now names the roots of the tier the -predicate was actually evaluated against** (objectui#6487). An app-shell author -whose nav, area or field `visible` faulted was told to check `record` and -`page.` — two roots that tier does not bind at all. - -`formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate` -(both exported from `@object-ui/react`) take a new **optional sixth argument**, a -`PredicateScopeTier` — also exported — selecting the closing advice paragraph. -Everything above that paragraph is unchanged on every surface, and so is every -verdict: this is diagnostics copy only. - -**The published signature grew; nothing existing breaks.** The argument defaults -to `'page-component'`, so a five-argument call keeps printing the bytes it -printed before. All three in-repo call sites pass their tier explicitly rather -than lean on that default. - -Each tier's root set was derived from the code that builds the bag, not from the -prose that described it: - -- **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item - predicates. Both bind `record`, `current_user` and `page.` (the roots - `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is - byte-for-byte what it was. -- **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` - runs, wired onto this reporter by objectui#6443. Its evaluator is built from - `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so - the line now names `current_user` with its three ADR-0068 alias spellings, - `app`, and `features` — the deployment-flag root that provider documents for - exactly this kind of predicate — and states outright that `record` and - `page.` do not exist there. - -**Why not generalise the copy instead.** Dropping the concrete root names would -have made one paragraph true everywhere at the cost of making it useful nowhere: -an author who mistyped a root needs to know which roots exist *at their tier*, -which is the whole reason the paragraph is read. - -`data` is bound at the app-shell tier but is deliberately not advertised there — -every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it, -so naming it would point an author at a root that answers nothing. diff --git a/.changeset/6488-carry-over-field-keys.md b/.changeset/6488-carry-over-field-keys.md deleted file mode 100644 index 619f637a0f..0000000000 --- a/.changeset/6488-carry-over-field-keys.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService.saveFields` carries the server's per-FIELD keys through a field save -instead of rebuilding every entry from the designer model (objectui#6488). - -The method preserved unknown keys of the OBJECT document by spreading it, but that spread -is object-level and said nothing about keys INSIDE a field. Every entry was rebuilt by -`toFieldPayload`, so every key the server sent inside a field that the designer does not -model was dropped on every field save: `expression` (a formula authored in metadata-admin), -`precision`, `scale`, `system`, `sortable`, and anything a plugin registered. Measured -against the installed `@objectstack/spec` 17.2.0, `FieldSchema` accepts all five — the -designer's model is a subset of what a field may hold, and the difference was being -deleted. - -The loss is not new but was UNREACHABLE. While `fields` went out as an array the whole -body was refused `422 INVALID_METADATA` before persistence, so nothing `saveFields` -dropped ever reached storage; objectui#6240 made the body parse, and a PUT is an upsert, -so from that fix onward the drop lands. - -`toFieldPayload` now merges onto the previous SERVER entry, read from the document -`saveFields` already fetches for the object-level spread — the form -`MetadataFieldsPage.fromDesignerField` has used one writer over all along, and no extra -request. - -Two properties keep the fix from becoming its own mirror image, both pinned in -`MetadataService.fieldKeyCarryOver.test.ts`: - -- **A property the author CLEARED stays cleared.** Every modelled key is still written - unconditionally, so a cleared property arrives as an explicit `undefined` that overrides - the carried value and is dropped by `JSON.stringify` — absent from the body, which on an - upsert is the deletion. A conditional merge would leave the server's old value standing - and fail the author's deletion silently. -- **Retired designer keys do not ride back out.** `indexed`, `referenceTo`, `formula`, - `isSystem` and `sortOrder` are refused BY NAME by `FieldSchema`; a stored document can - still carry them, and echoing one back is a hard 422 that blocks every later save of the - object with no UI path to clear it. Everything else the server sent still survives — the - strip is keyed to those tombstones, not a blanket unknown-key purge. diff --git a/.changeset/6489-designer-fields-map-keying.md b/.changeset/6489-designer-fields-map-keying.md deleted file mode 100644 index 2af37a0ed1..0000000000 --- a/.changeset/6489-designer-fields-map-keying.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-designer': patch ---- - -The Field Designer builds an object's `fields` map by defining own properties and refuses -the three field lists a name-keyed map cannot carry (objectui#6489). `MetadataFieldsPage` -keyed the map by blind assignment — `nextFields[f.name] = fromDesignerField(…)` inside a -bare `for` loop — which failed silently in three directions, all measured on the installed -`@objectstack/spec` 17.2.0: - -- **A field named `__proto__` never reached the wire.** `map['__proto__'] = def` invokes the - prototype setter instead of creating a key, so the field vanished from the serialised PUT - body. `__proto__` matches `ObjectSchema.fields`' key rule `/^[a-z_][a-z0-9_]*$/`, so the - spec stood ready to accept the field the client had thrown away. The map is now built - through `Object.fromEntries`, which defines an own property. -- **A nameless field was stored under the literal key `"undefined"`.** Measured: - `ObjectSchema.safeParse` with `fields: { undefined: … }` returns `success = true`, so the - document parsed, persisted, and had no reader anywhere. It is now refused before the - request. -- **Two fields sharing a name collapsed into one entry.** A designer list carrying two - `amount` fields PUT a single entry, the later silently replacing the earlier. Also refused - before the request. - -Both refusals raise before `client.save`, so a refused list issues no PUT at all, and the -message lands in the page's existing error surface naming the offending index — the caller -is fire-and-forget (`void handleFieldsChange(next)`), so throwing past it would show the -author nothing. - -This is the plugin-designer port of the refusals objectui#6240 landed in the sibling object -writer (app-shell's `MetadataService.toFieldsMap`), down to the wording, so the two writers -of the objectui#5761 parity family cannot drift. `fromDesignerField`'s carry-over semantics -are untouched. diff --git a/.changeset/6490-saveobject-fields-required.md b/.changeset/6490-saveobject-fields-required.md deleted file mode 100644 index 7b67e3ed26..0000000000 --- a/.changeset/6490-saveobject-fields-required.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -**BREAKING (in name only):** `MetadataService.saveObject(obj, existingFields)` now requires -its second argument (objectui#6490). Calls that omitted it no longer compile. - -**Runtime behaviour is unchanged, and that is the whole justification.** A call that omitted -the field list was already a guaranteed `422` — every time it ran, against every backend. -`ObjectSchema.fields` is not merely typed, it is REQUIRED: measured against the installed -`@objectstack/spec` 17.2.0, `ObjectSchema.safeParse({ name: 'account', label: 'Account' })` -fails with `invalid_type @ fields`, and `metadata-protocol`'s `saveMetaItem` parses the whole -item against that same schema and throws `422 INVALID_METADATA` **before** it persists. The -method cannot build a valid document without the argument, so the only calls this break -breaks are calls that already failed. The signature is now honest about it, and the diagnosis -moves from a round trip at runtime to the compiler. - -Nothing new is exported and nothing new is accepted — this narrows the published surface -rather than widening it. In-repo production call sites were measured at **zero** (only tests -called it), so the migration for an external consumer is to pass the field list it was -already required to send: `saveObject(obj, fields)`. - -⛔ Two readings were considered and declined, recorded so neither is taken later as a -shortcut. **Not a `{}` default** — `{}` parses GREEN and `PUT /api/v1/meta/object/:name` is -an upsert, so defaulting would delete every field of the object on a save that only meant to -rename it, trading a loud, harmless 422 for silent data loss; the anti-wipe control from -objectui#6240 (`omits fields entirely when the caller supplied none — it does NOT write {}`) -moves with the signature and still guards the path a JavaScript consumer can reach. **Not -fetch-and-merge** — an object save could GET the current document and preserve its stored -`fields` the way `saveFields` does, but that builds capability for a path with zero measured -pull and makes the parameter redundant. - -An EMPTY list stays a different statement from a missing one: `[]` means "this object has no -fields", writes `{}`, and under the upsert performs the wipe the caller asked for — the same -authoritative reading `saveFields` gives its own empty list. Unchanged, and now pinned, -because the required parameter is what routes a caller with nothing to hand toward it. - -Scored `minor` and not `major` per AGENTS.md §版本号策略 — objectui's major is pinned to the -`@objectstack` major so that "same major ⇒ compatible" holds across the two repos, and every -publishable package sits in one `fixed` group, so objectui's own breaking changes ship as -`minor` with the break spelled out in the body. That is the convention, which is why the -break is stated in words above. diff --git a/.changeset/6493-bind-field-visibility-evaluators.md b/.changeset/6493-bind-field-visibility-evaluators.md deleted file mode 100644 index aa20d7da0e..0000000000 --- a/.changeset/6493-bind-field-visibility-evaluators.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -⚠️ **Behaviour change: `current_user` and `features` gates on an object field's -`visible` that have been doing nothing on the record form page and in the -console's record modal now TAKE EFFECT.** Read this before upgrading if any of -your object metadata gates a field on the session user or on a deployment flag. - -objectui#6010 and objectui#6110 bound the host predicate scope on the form -renderer and on the console form routes. `evaluateVisibility` was still being -reached with a THIRD and FOURTH evaluator that neither of those touched: -`RecordFormPage` and `AppContent` each built a private -`new ExpressionEvaluator({ user, app, data })` for the field-visibility filter, -beside — not from — the `ExpressionProvider` each of them mounts. Those bags -bound `user`, but not the canonical `current_user` nor the ADR-0068 `ctx.user` / -`os.user` spellings of that same object, and not `features` at all. So one -authored predicate meant two different things depending on which evaluator -reached it: `current_user` resolved on a nav item and was unbound on a field. -Both sites now build their scope with the same `buildExpressionScope` the -provider uses, which is the only declaration of what an app-shell predicate may -name. - -**Why nobody noticed, and why the fix is felt as a change.** A field `visible` -predicate fails OPEN: a field on screen is what you get when the predicate says -TRUE, when the root was never bound so the predicate faulted, *and* when the -predicate has a typo. Those worlds are indistinguishable, so an app that -authored a `current_user` gate saw the field render and had no way to tell the -rule was inert. After this change the predicate is evaluated for real, and -**fields that have always been visible will disappear for the users the rule -excludes** — and a `features` gate whose flag is off will hide its field once -`/api/v1/auth/config` resolves. - -`AppContent`'s bag also hand-rolled its user as `{ name, email, role }`, without -`positions`. It now uses the same `buildExpressionUser` normaliser every other -console surface publishes, so `'sales' in current_user.positions` — the gate the -server enforces on write — reaches the same verdict client-side instead of -faulting open. - -**Before upgrading**, audit any `visible` predicate in your object metadata that -names `current_user` (or `user` / `ctx.user` / `os.user`) or `features`, and -confirm each says what you actually want evaluated. Measured on the metadata -shipped in this repo and in the framework at the time of the change: **nothing -in it authors such a gate**, so no shipped surface changes behaviour today — -the audit is for your own object metadata, which this cannot see. - -**The error path is deliberately unchanged.** A predicate that throws still -fails open, exactly as objectui#6443 / objectui#6487 left it. This change is -about which roots are BOUND, not about what happens when evaluation fails. diff --git a/.changeset/6496-toast-button-keys.md b/.changeset/6496-toast-button-keys.md deleted file mode 100644 index a5281eb24d..0000000000 --- a/.changeset/6496-toast-button-keys.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ToastSchema` now declares the two trigger-button keys the `toast` renderer actually reads -(objectui#6496, triage scope cut 2026-08-26 — the same declare-what-runs family as -objectui#6170). - -`renderers/feedback/toast.tsx` renders a ` -to be null`), with the stored-column controls present in both blocks, reproducing the race. -The new spelling passes under the same delay. - -Each block KEEPS its existing gate and ADDS the second one (the objectui#6959 precedent and -its trap): the first gate proves the view committed with rows at all, which is what keeps -`queryBy…).toBeNull()` from passing vacuously; the second proves the schema the verdict is -derived from has landed. Trading one for the other would have left the block blind on -whichever side it dropped. - -The gate is not `expect(getObjectSchema).toHaveBeenCalled()` — a mock CALL is issued one -resolution before its value reaches state, which is the trap #6959 recorded. It awaits the -promise the component itself awaited and flushes the commit through `act`. diff --git a/.changeset/7008-field-edit-widget-host-plumbing.md b/.changeset/7008-field-edit-widget-host-plumbing.md deleted file mode 100644 index fe56d13482..0000000000 --- a/.changeset/7008-field-edit-widget-host-plumbing.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/fields': minor -'@object-ui/plugin-kanban': patch ---- - -`FieldEditWidget` now delivers the NON-DOM half of the contract it declares (objectui#7008). - -objectui#7009 made the factory forward its declared DOM pass-through block. The rest of -`FieldWidgetComponentProps` was still dropped: `error`, `onUploadingChange`, and the whole -"Host plumbing" block (`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`, -`emptyHint`, `onSelectRecord`, `onCreateNew`). A host could pass any of them with no type -error and the widget never received it — the "declared but not delivered" class this -package treats as first-class. - -`error` was the live one. `InlineFieldInput` has passed `error` into this factory since -PR #7109 and the factory dropped it, so an inline-edit control that had failed validation -never reported `aria-invalid`: a sighted user saw the red hint, a screen-reader user was -told nothing. The kanban `RequiredFieldsDialog` had the same hole from the other side — it -computes the validation state and could not hand it over — and now passes `error`, so its -controls are marked. Delivering `error` buys the a11y MARKING only; the message text stays -with the host, per the objectui#3222 contract. - -The keys travel through a new sibling executor, `toHostProps` (exported alongside -`toDomProps`), never through the DOM whitelist — none of them is DOM-legal, and routing a -`dataSource` adapter there is the `[object Object]` leak that whitelist exists to stop. -Three compile-time assertions make the two executors partition the contract, so a future -declared key cannot go undelivered silently. - -`dataSource` precedence is stated rather than left to emerge: a host's explicit -`dataSource` prop WINS over `SchemaRendererContext`. That is the order `LookupField` -already implements; the factory is a conduit and resolves nothing. A host that passes no -`dataSource` keeps reading the context exactly as before, so no in-repo host changes -behaviour. diff --git a/.changeset/7014-q1-tier1-select-option-convergence.md b/.changeset/7014-q1-tier1-select-option-convergence.md deleted file mode 100644 index 27c27512e2..0000000000 --- a/.changeset/7014-q1-tier1-select-option-convergence.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Converge the two named select-option types onto one spec-derived base (objectui#7014, Q1). - -`SelectOptionMetadata` (`field-types`, the object-metadata read model) and `SelectOption` -(`form`, the SDUI form vocabulary) each restated the select-option vocabulary by hand. -Both now extend the new `SelectOptionBase`, which derives the spec's keys from -`@objectstack/spec/data` **by reference** and writes out only the divergences. A key the -spec adds now reaches both faces with no edit here; a key it removes becomes a compile -error at the sites that read it, instead of a hand copy that goes on compiling while the -contract moves underneath it. - -**What widened.** Exactly one key, on one face: `SelectOptionMetadata` gains -`default?: boolean`. It is a spec key that face could not describe before — ruled -`enforce` on the object-field face (objectstack#7246), where the engine seeds a new -record from the option marked `default: true` — and it arrives OPTIONAL, so every -document that face accepted before is still accepted. - -**What narrowed.** Nothing. Both faces resolve to member-for-member what they resolved to -before (`SelectOption` identically; `SelectOptionMetadata` identically plus `default`), -pinned invariantly against the pre-convergence member lists in -`select-option-tier1-convergence-7014.test.ts` so a future "unification" cannot quietly -drop a key. `SelectOption.value` keeps its deliberate widening past the spec's machine -identifier (numeric/boolean values for standalone forms, objectui#3090), now named in an -`Omit` instead of restated. - -**The convergence is an EXTENSION, not a replacement.** objectui legitimately carries -keys the spec does not: `description` (`LookupField` searches it, objectui#6153) on the -metadata face, and `disabled` / `icon` on both. The spec's `SelectOptionSchema` is strict -over exactly `{label, value, color, default, visibleWhen}` and refuses each of those -three **by name**, so they are declared as objectui dialect with that refusal written -into the published JSDoc rather than described as spec-aligned. These are read-model keys -and must never reach an authored object document — a field's `options` are routed through -the strict schema, so one of them fails the whole field. - -`SelectOptionBase` is exported from `@object-ui/types` because it appears in the -`extends` clause of both published interfaces. diff --git a/.changeset/7014-q2-option-label-empty-string.md b/.changeset/7014-q2-option-label-empty-string.md deleted file mode 100644 index 7141fea4ec..0000000000 --- a/.changeset/7014-q2-option-label-empty-string.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Field designer: clearing a picklist option's **Label** no longer produces metadata the -API refuses (objectui#7014 Q2). - -`ObjectFieldInspector`'s option writer guarded the key on truthiness — -`if (o.label) out.label = o.label;` — so an empty Label box was serialised as an option -with **no `label` key at all**. Measured on `@objectstack/spec` 17.2.0, -`SelectOptionSchema` accepts `{ value: 'alpha', label: '' }` and rejects -`{ value: 'alpha' }` with `invalid_type` at `[label]`: the guard was taking a document -the platform accepts and rewriting it into one it refuses, and the save came back 422 -with nothing on screen to explain it. - -The writer now emits `label: o.label ?? ''` — the value the author actually holds, -empty string included. Nothing is invented: an emptied label stays empty rather than -falling back to the option's `value`, and the author-facing surface is now exactly as -wide as the contract instead of narrower. The `??` arm also covers an option that -arrived without a usable label (a missing or non-string stored `label`, which -`readOptions` maps to `undefined`) — there is no legal document that omits the key, and -`''` is what the Label input has been displaying for that option all along. - -Pinned in `ObjectFieldInspector.optionLabel.test.tsx`, which ends each case at -`SelectOptionSchema` / `FieldSchema` rather than merely asserting the key is present — -the point of the fix is that the contract accepts what the designer emits. diff --git a/.changeset/7014-select-option-spec-claim-correction.md b/.changeset/7014-select-option-spec-claim-correction.md deleted file mode 100644 index c086db0875..0000000000 --- a/.changeset/7014-select-option-spec-claim-correction.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the -real boundary (objectui#7014). - -**No contract change.** No type, schema, export or runtime path moves. What changes is -published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was -asserting the opposite of what the spec does. - -Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in -fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a -control that accepts the same payload minus the key: - -- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec` - `SelectOptionSchema.description`". That schema is `.strict()` over exactly - `{label, value, color, default, visibleWhen}`; `description` fails with - `unrecognized_keys`. -- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec` - `FieldSchema.rows` declares the key "authorable on exactly the multiline editor - types". `FieldSchema` refuses `rows` by name on all four of - textarea/markdown/html/richtext. - -The keys themselves stay declared and stay consumed — `LookupField` searches an option's -`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the -attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select -field's `options` through the strict option schema, so authoring `description` on an -option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422 -`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are -objectui-side read-model extensions that must never reach authored object metadata. - -A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set -and each by-name refusal, so if the spec ever adopts one of these names the claim is -re-opened loudly instead of silently becoming true. diff --git a/.changeset/7015-text-expression-content-channel.md b/.changeset/7015-text-expression-content-channel.md deleted file mode 100644 index 4048b82e2c..0000000000 --- a/.changeset/7015-text-expression-content-channel.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Docs only: the expression guides taught `"value": "${...}"` on `type: "text"` -nodes, which the renderer reads back but never evaluates, so the reader saw the -literal `${...}` on screen. All 29 authored occurrences now spell the carriage -`content`, the ruled sole evaluation channel for `text` (objectui#7015, -maintainer ruling 2026-08-31 on objectstack#13670, option 2). - -No package source changed, so this ships nothing — `check-changeset-presence` -independently reports "no changeset is owed" for this diff. The declaration is -here to state the release intent explicitly rather than to bump anything. diff --git a/.changeset/7025-nonoptional-enum-cast.md b/.changeset/7025-nonoptional-enum-cast.md deleted file mode 100644 index cdf00e8bcf..0000000000 --- a/.changeset/7025-nonoptional-enum-cast.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -Internal test-only change, no user-visible behaviour (objectui#7025). - -The SIXTH spelling of the Zod-internals reader hazard objectui#5872 catalogues: -9 spec-parity test files in 7 packages cast an enum node NON-optionally to an -options-bearing shape and read `.options` straight off it. They converge onto -`@object-ui/test-support`'s `enumOptions(node)` — the same walk objectui#6924 -converged the optional-cast family onto in PR #7024. - -Unlike that family, these sites failed LOUDLY (spreading `undefined` throws), and -the shared reader deliberately answers `[]` rather than raising. So every site -keeps its own non-vacuity duty: a throwing wrapper at the seven module-scope -reads, and the suite's own pre-existing non-vacuity assertion at the two in-test -reads. A bare conversion would have traded a loud failure for a silent empty -vocabulary — a regression, not a convergence. - -Only test files, two `devDependencies` edges and the lockfile change; no -package's shipped `dist/` and no public type moves. diff --git a/.changeset/7027-gantt-date-brand-gate.md b/.changeset/7027-gantt-date-brand-gate.md deleted file mode 100644 index 49f4be4ac1..0000000000 --- a/.changeset/7027-gantt-date-brand-gate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-timeline': patch ---- - -Gantt date type gate: judge the `[[DateValue]]` slot, not the prototype chain -(objectui#7027). - -`isGanttDateType` asked `value instanceof Date`, which answers "does this -inherit from `Date.prototype`?" and not "is this a `Date`?". An object that -inherits the prototype without owning the internal slot passed the gate, -reached `new Date(value)`, ran ToPrimitive, and threw -`TypeError: Method Date.prototype.toString called on incompatible receiver` — -uncaught, mid-render, so the author got a blank screen where #6781's named -diagnostic belongs. Three spellings crashed on `main`, measured: -`Object.create(Date.prototype)`, that impostor behind a `Proxy` that throws on -every get, and a `Proxy` with a throwing `getPrototypeOf` trap (`instanceof` is -not total on its own terms either). - -Both sites now ask a total brand test that invokes the builtin -`Date.prototype.getTime` with `.call`: it reads the receiver's `[[DateValue]]` -slot and nothing else, so no author getter runs, no `Symbol.toStringTag` is -consulted, and no proxy trap fires. The two brand tests the finding suggested -were measured and rejected — `Object.prototype.toString.call` performs -`Get(O, @@toStringTag)` unconditionally, and `Number.isFinite(value.getTime())` -calls the author's `getTime`, which would refuse a real `Date` subclass by -dying on it. Both are pinned as red rows. - -No change to which values are accepted: #6781's accept set -(`string | finite number | Date`) is untouched, `new Date(NaN)` still passes -the type gate and is still refused by the parse check with its `Invalid Date` -spelling, and every newly-refused value is one no authored document can carry -(ObjectUI metadata is JSON, which cannot spell a prototype). diff --git a/.changeset/7029-no-invented-calendar-fields.md b/.changeset/7029-no-invented-calendar-fields.md deleted file mode 100644 index 37fef232ea..0000000000 --- a/.changeset/7029-no-invented-calendar-fields.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor ---- - -Calendar views no longer render on invented field names (objectui#7029; ruled on -objectstack#13748, director batch #19, option A). - -A view that carried no `calendar:` block used to have a complete-looking calendar -configuration synthesized for it. `ObjectCalendar` has always decided whether it -has a usable configuration by asking whether a start-date binding is PRESENT, so -the fabrication short-circuited its own refusal screen — "Calendar configuration -required. Please specify startDateField and titleField." — which existed all -along and was simply unreachable. Measured on a leave-request object whose real -fields are `start_date` / `end_date`: every record piled onto today's cell under -titles resolved through the display-name chain. A plausible, fully wrong screen, -with zero signal to the author. - -Three faces were fabricating, on two independent routes to the same renderer: - -- `app-shell/ObjectView` emitted `startDateField: 'due_date'` and - `titleField: 'name'` into `options.calendar` for every object view; -- `plugin-list/ListView`'s calendar branch floored the same two bindings at - `'start_date'` / `'end_date'` one layer down; -- `plugin-view/ObjectView.generateViewSchema` — the authored `object-view` - element route, which bypasses `ListView` entirely — carried its own copy. - -All three now forward only what the author declared. This converges the calendar -on the shape its siblings already had: `timelineViewOptions` (objectui#3129 -retired this very literal from the timeline axis), the kanban lane detector -(ADR-0085, "never invents a field the object doesn't have"), and -`defaultCalendarFromObject` (a binding, or nothing). - -**Behaviour change, loud over silent.** With no binding to forward, ADR-0047's -capability gate stops offering the Calendar toggle to views that configured -none, and a view forced onto the calendar renderer reaches the refusal screen -instead of a wrong one. A view that happened to sit on an object carrying a real -`due_date` field was rendering by luck; it now refuses until its `calendar:` -block is written. Correctly configured calendars are unaffected — same fields, -same render. The same deletion also stops the fabricated name from answering for -the Timeline switcher, which accepts a calendar binding as a legitimate axis. - -The spec half — cross-field validation rejecting a half-written declaration at -authoring time — is objectstack#13817. This half makes the runtime honest -independent of which spec version the host pins. diff --git a/.changeset/7063-widget-empty-state-default.md b/.changeset/7063-widget-empty-state-default.md deleted file mode 100644 index 3de7b6ba56..0000000000 --- a/.changeset/7063-widget-empty-state-default.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor -'@object-ui/i18n': minor ---- - -Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated -once for the surface (objectui#7063). - -Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that -renders a bare row-placeholder on an empty result is the PLATFORM's defect and -must be fixed uniformly — apps must not compensate widget by widget -(objectstack#13848). The measured scenario is a fresh flagship-demo install: -eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads -as "the dashboard failed to load" even though the widget, its declaration and -its (not yet produced) data are all legitimate. - -- New `WidgetEmptyState` is the seam the three dashboard surfaces now share. - There was no shared placeholder to fix: `DatasetWidget` wrote - `dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote - `dashboard.noDataAvailable` — three renders, two strings, no common code. -- The default now reads as a STATE, not a failure: `role="status"` (the empty - branches previously carried no role at all, while the failure branches beside - them are `role="alert"`), muted treatment with an inbox glyph rather than a - warning triangle, and a title plus an explanation where the placeholder was a - single terse fragment. -- It names WHAT is empty with zero authored copy — the widget's data source, - which is the half the reader cannot already see (the tile's title is rendered - by the card header directly above). That is `widget.dataset` on the dataset - path and `schema.objectName` on the object-bound table/pivot; `PivotTable` - takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable` - forwards. -- Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel` - added to `en` and all nine sibling packs. No inline `defaultValue` and no - interpolation — the source renders as a labelled value, so no separator is - concatenated in code and every pack spells its own punctuation. - -No new authoring obligation and no new spec key. Note that the `emptyState` -override the card assumes for this surface does not exist: `emptyState` is a -LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no -such key — so there is nothing here for an author to override, and adding one -would be a contract question rather than a rider. diff --git a/.changeset/7064-empty-section-default.md b/.changeset/7064-empty-section-default.md deleted file mode 100644 index 146b859d39..0000000000 --- a/.changeset/7064-empty-section-default.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -**Behaviour change.** `record:details` no longer forces `hideEmpty` on the -sections it synthesizes, so a sparse record keeps its section skeleton instead -of collapsing. Applications relying on the old auto-hide of *unauthored* -sections will now see headings, field labels and empty-value placeholders where -rows used to vanish. This is the loud-over-silent direction, ruled by the -maintainer on 2026-08-31: an empty detail body is a platform concern, and a -metadata application should not have to author its way out of one. - -`RecordDetailsRenderer` mapped every authored section with -`hideEmpty: s.hideEmpty ?? true`. `DetailSection` already states the correct -rule in its own heuristic — *"If a section is entirely empty (e.g., loading -state, brand-new record), do NOT auto-hide — the labels themselves are useful -as a structural skeleton"* — and the forced default overrode exactly the case -that sentence reserves. On a hand-created record whole sections disappeared and -the body collapsed to a couple of rows; seeded demo data hid it. Every -application then had to hand-write `hideEmpty: false` per section to stop -looking broken, which is per-app tax for a platform defect. The renderer now -passes the authored value through untouched and lets the heuristic own the -default. - -What changes, precisely: - -- an **all-empty** section renders its heading, every field label and one - empty-value placeholder per field (it used to render nothing at all); -- a **small** partly-empty section — below `DetailSection`'s auto-hide - threshold of 4 fields / 25% empty (3 / 20% on mobile) — now shows its empty - rows; -- a **large** mostly-empty section with at least one filled row still - auto-hides, with the "Show N empty fields" toggle unchanged: the - label-graveyard guard is intact and this is not a return to dense-by-default; -- empty rows are now visible while inline-editing a section, so an unwritten - field can be filled in place. - -What does **not** change: an authored `hideEmpty` keeps its exact former -meaning. `hideEmpty: true` remains the explicit opt-in to hiding, and -`hideEmpty: false` remains what it always was — "not `true`", not an override -of the auto-hide heuristic (measured, and pinned as pre-existing). - -Reference-app hit inside this repo: the Studio metadata-admin page preview -(`PagePreview`) binds a real sample record, so a `record:details` block over a -sparse sample now previews the skeleton rather than a collapsed body. No -application metadata needs editing — that is the point of the change. diff --git a/.changeset/7069-parity-wider-ledger.md b/.changeset/7069-parity-wider-ledger.md deleted file mode 100644 index 06a72f97e8..0000000000 --- a/.changeset/7069-parity-wider-ledger.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-only change: adds a fifth ledger to the zod-mirror parity suite watching the -mirror-wider-than-declared direction. No published type, mirror or runtime behaviour -moves; the file is excluded from the package build by `tsconfig.json`. diff --git a/.changeset/7070-no-invented-gantt-date-fields.md b/.changeset/7070-no-invented-gantt-date-fields.md deleted file mode 100644 index aa768c6020..0000000000 --- a/.changeset/7070-no-invented-gantt-date-fields.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor ---- - -Gantt views no longer render on invented date field names (objectui#7070). - -The half PR #7062 fenced out and reported separately. A view that carried no -`gantt:` block used to have a complete-looking date axis synthesized for it: -all three faces floored `startDateField` at `'start_date'` and `endDateField` -at `'end_date'` — field names no view had written and most objects do not -carry. - -`ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props -are present, so the fabricated pair short-circuited the renderer's own refusal -screen — "Gantt configuration required. Please specify startDateField, -endDateField, and titleField." — which existed all along and was simply -unreachable from every route. The same fabrication answered ADR-0047's -capability gate in `ListView.availableViews`, so the Gantt toggle was live on -every object view in the product. - -⚠️ The premise was MEASURED before anything was deleted, because #7029's -mechanic is only correct where a refusal path exists and that had never been -established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an -absent binding — it does not render empty, and it does not throw. - -Three faces were fabricating, on two independent routes to the same renderer: - -- `app-shell/src/views/ObjectView.tsx` — the console object page. The inline - branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and - `timelineViewOptions`: the declared block spread whole, title floored at - `'name'`, no date field invented. -- `plugin-list/src/ListView.tsx` — the render branch AND the capability gate. -- `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored - `object-view` element route, which bypasses `ListView` entirely. - -**What changes for an author.** A view that declared no gantt configuration is -no longer offered the Gantt toggle, and one forced onto the renderer reaches -the refusal screen instead of a plausible, fully wrong chart. A view that -declared a binding is unaffected — the declared block is forwarded exactly as -before, every spec key included. - -Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx` -certified the gantt branch below it as already using the safe two-rung shape. -It did not. The note now states each sibling branch as measured, and says -explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the -two plugin faces, which objectui#7070 routes to a ruling rather than settling -per-face. - -Deliberately out of scope, and left in place: `progressField` / `dependenciesField` -(not date axes, different absent-value semantics) and the timeline `'created_at'` -posture conflict. diff --git a/.changeset/7070-timeline-date-axis-floors-retired.md b/.changeset/7070-timeline-date-axis-floors-retired.md deleted file mode 100644 index c38bd4a691..0000000000 --- a/.changeset/7070-timeline-date-axis-floors-retired.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor ---- - -Retire the `'created_at'` timeline date-axis floors at both plugin faces -(objectui#7070 step ③, maintainer ruling 2026-09-01, 总监批 #28). - -**Breaking, deliberately.** A timeline view that declares **no** date axis anywhere no -longer renders. `ListView`'s and `ObjectView`'s timeline branches used to hand -`ObjectTimeline` a `startDateField` of `'created_at'` for such a view; both now forward -a declared axis or no key at all, and the renderer shows its "declare a date axis" -refusal instead. - -House posture, entered with the ruling: **日期轴永不虚构** — a date axis is never -fabricated. This is the third and last step of a sequence the ruling ordered and forbade -reordering: `ObjectTimeline` gained the refusal screen and lost its own internal -`|| 'date'` floor first (objectui#7459), which by its own measurement changed nothing a -user could see — precisely because these two faces still supplied a name. They are the -supply. - -The floor was not a harmless default. `'created_at'` is a column nearly every object -carries, so downstream it was indistinguishable from a real binding and could never -resolve to nothing — while the `$select` projection is collected from the **declared** -`timeline` / `options.timeline` blocks and never from this prop. An undeclared view was -therefore given a timeline bound to a column the query had not requested, and every -record bucketed into "No date": a screen that looks built, is wrong, and gives the -author no signal. The ruling also explicitly replaced the written decision that stood on -the deleted `ListView` line ("`created_at` stays the last resort for a view that -declares no date axis anywhere") — it was a second, de-facto contract held at one face, -on the very literal objectui#3129 had retired at the app-shell face. - -**Migration.** Declare the axis on the view: `timeline.startDateField` (spec-canonical), -`timeline.dateField` (legacy alias), or a `calendar.startDateField` — objectui#3129 -established that a calendar binding is a legitimate timeline axis, and it still is. All -three keep rendering exactly as before; only the *undeclared* case changes. A view that -really did want records laid out by creation time says so in one key: -`timeline: { startDateField: 'created_at' }`. The refusal names the accepted keys on -screen, so an affected view reports its own fix. - -`titleField` is unaffected and keeps its `'name'` floor at both faces — it is not a date -axis. So do gantt's `progressField` / `dependenciesField`, which the ruling scoped out -for separate evaluation. diff --git a/.changeset/7072-catalog-menu-value-keys.md b/.changeset/7072-catalog-menu-value-keys.md deleted file mode 100644 index 342ecc54ce..0000000000 --- a/.changeset/7072-catalog-menu-value-keys.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Internal only, no release: the four overlay-menu fixtures in -`examples/schema-catalog` no longer author a `value` key on their menu items. - -Twenty-one menu items across `components-overlay-menubar/application-menubar` -(11), `components-overlay-context-menu/basic-context-menu` (4), -`components-overlay-dropdown-menu/basic-dropdown-menu` (3) and -`components-overlay-dropdown-menu/with-icons` (3) carried `"value"`. No arm of -the shipped `MenuItem` union declares it — `MenuCommandItem` -(`packages/types/src/overlay.ts:363-401`) declares `label`, `icon`, `disabled`, -`onClick`, `shortcut`, `children`, `separator?: false` and a `type?: never` -tombstone; `MenuDividerItem` (`:409-419`) declares `separator: true` and the -same tombstone. Neither declares `value`, and none of the three menu renderers -under `packages/components/src/renderers/overlay/` reads one. - -Nothing rendered differently before or after, which is the point: `MenuItemSchema` -(`packages/types/src/zod/overlay.zod.ts:147-168`) builds its union from bare, -non-strict `z.object`s, so zod stripped the key and reported success. The key -had been inert and invisible since it was authored, and the catalog is a -declared AI few-shot retrieval source, so an inert key there is a spelling the -next author copies. - -The keys are deleted rather than declared: nothing reads `value`, and -objectui#6523 deliberately narrowed this union rather than widening it. No -published package source, no schema, no renderer and no type was touched — the -diff is four JSON fixtures in a `private: true` example package. diff --git a/.changeset/7073-overlay-menu-docs-declared-surface.md b/.changeset/7073-overlay-menu-docs-declared-surface.md deleted file mode 100644 index cae4edd1b4..0000000000 --- a/.changeset/7073-overlay-menu-docs-declared-surface.md +++ /dev/null @@ -1,49 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the `## Schema` blocks of -`content/docs/components/overlay/dropdown-menu.mdx` and -`content/docs/components/overlay/context-menu.mdx` are corrected to the surface -`packages/types/src/overlay.ts` actually declares. Same defect class objectui#6521 -fixed on the third menu page, and more of it — all three pages publish a locally -named mirror of the shared `MenuItem` union, and these two were still wrong. - -`dropdown-menu.mdx`, four rows: - -| page before | authority on `origin/main` | page after | -| --- | --- | --- | -| `value?: string` | declared on **neither** arm — not `MenuCommandItem` (`overlay.ts:363-401`), not `MenuDividerItem` (`overlay.ts:409-419`), absent from both zod arms (`zod/overlay.zod.ts:149-166`); no menu renderer reads a `.value` | row deleted | -| `variant?: 'default' \| 'destructive'` | declared nowhere on the menu types. The only `variant` in `renderers/overlay/dropdown-menu.tsx` is `:130`, inside `defaultProps.trigger` — `{ type: 'button', label: 'Menu', variant: 'outline' }`, a button node, a different object than a menu item | row deleted | -| *(absent)* | `shortcut?: string` (`overlay.ts:383`, zod `zod/overlay.zod.ts:154`), **read** at `dropdown-menu.tsx:80` | `shortcut?: string` added | -| *(absent)* | `children?: MenuItem[]` (`overlay.ts:387`, zod `zod/overlay.zod.ts:155`), **read** at `dropdown-menu.tsx:58,66` — branches into `DropdownMenuSub` / `DropdownMenuSubTrigger` / `DropdownMenuSubContent`, a real submenu | `children?: DropdownMenuItem[]` added | - -`context-menu.mdx`, four rows: - -| page before | authority on `origin/main` | page after | -| --- | --- | --- | -| `value?: string` | same as above — undeclared on both arms; no `.value` read in `renderers/overlay/context-menu.tsx` | row deleted | -| *(absent)* | `shortcut?: string` (`overlay.ts:383`, zod `zod/overlay.zod.ts:154`), **read** at `context-menu.tsx:78`, rendered through `ContextMenuShortcut` | `shortcut?: string` added | -| *(absent)* | `children?: MenuItem[]` (`overlay.ts:387`, zod `zod/overlay.zod.ts:155`), **read** at `context-menu.tsx:56,64` — branches into `ContextMenuSub` / `ContextMenuSubTrigger` / `ContextMenuSubContent` | `children?: ContextMenuItem[]` added | -| `trigger: ComponentSchema` (required) | `trigger?: SchemaNode \| SchemaNode[]` — optional at `overlay.ts:486`, `.optional()` at `zod/overlay.zod.ts:191`. The renderer substitutes a placeholder (`context-menu.tsx:95`), so a trigger-less document is legal today, and the declaration's own doc comment records the divergence in words: "Declared OPTIONAL although the docs page shows it required" (objectui#6150) | `trigger?: ComponentSchema` | - -`children` and `shortcut` are the rows that matter: declared capabilities with -working runtime that both pages hid outright, so an author reading either page -had no way to learn these menus draw nested submenus at all, or that an item can -carry a keyboard shortcut. That is the reverse of the usual docs defect and -strictly worse for an AI author, which will not emit a key the reference page -does not list. `value` and `variant` are the forward direction — keys the pages -taught and nothing on the platform has ever read. - -Interface names stay localized (`DropdownMenuItem`, `ContextMenuItem`) and the -`type` tombstone stays taught as `## Dividers` prose rather than a block row — -both are house convention, measured across all three menu pages by objectui#6521's -PR and unchanged here. - -Not in this diff, and deliberately not: declaring `value` or `variant` on -`MenuCommandItem`. objectui#6523 narrowed that union on purpose; widening a -published type to match a doc page would invert the fix. - -No fence moved: each page holds one `plaintext` fence marker pair before and -after, so objectui#5867's SHRINK-ONLY declared population is unchanged. - -Part of objectui#7073. diff --git a/.changeset/7077-button-group-retire-authored-surface.md b/.changeset/7077-button-group-retire-authored-surface.md deleted file mode 100644 index a645e7941a..0000000000 --- a/.changeset/7077-button-group-retire-authored-surface.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`button-group` honors per-button `disabled`, and the catalog stops authoring 29 keys -nothing reads (objectui#7077, maintainer ruling 2026-09-04, decision batch #25). - -**The renderer change.** `ButtonGroupButton` declares `disabled?: boolean` and the -renderer read it nowhere — it mapped `schema.buttons` to `Button` elements passing -`variant`, `size`, `className` and `label` and nothing else, so a button declaring -`disabled: true` rendered live and clickable. It now forwards the value, matching every -sibling that declares item-level `disabled`: `tabs`, `select`, `dropdown-menu`, -`menubar`, `context-menu`, and `toggle-group` since objectui#4632. This is mechanical -consistency with an already-declared contract — no new key, no new type member, and no -change to what any schema accepts or refuses. - -**Migration.** None required. A `disabled: true` you already author starts taking -effect; that is the declared meaning of the key, and nothing in the shipped corpus -authored it. - -**The corpus change.** `button-group` stays a **presentational** group: no selection -state, no `selectionMode`, no group-level `value`, no per-button `value`, no per-button -`icon`. All six catalog fixtures authored those four undeclared keys — 29 occurrences — -and every one parsed green, because `BaseSchema` is `.passthrough()` and carries -`[key: string]: any`: admitted unexamined, never refused. The keys are gone from the -fixtures. Implementing selection instead was weighed and rejected: it is a capability -addition with zero measured pull, and the only things authoring it were fixtures we -wrote ourselves — the corpus reverse-defining the product. It remains available as a -fallback if a real consumer (an app or example needing a segmented control) is produced -first. - -This matters more than a demo tidy-up because the catalog is the corpus AI authoring -tools retrieve from. An author copying the old `single-selection.json` got a schema that -validated, published, and did nothing — a failure with no signal to self-correct from. - -`with-icons.json` is **removed** rather than emptied: with `icon` retired, a fixture by -that name authored no icons, and what remained duplicated `basic-button-group.json`. Its -demo slot on the docs page goes with it. The other five keep their labels and still -render — `icon-toolbar.json` rendered three blank buttons until objectui#6318 gave it -labels, and a pin now holds every fixture in the category to a non-empty `label` on -every button. - -`onClick` is **not** wired, and is not a gap: objectui#6124 (PR #7339) retired it two -days before this ruling. It is `onClick?: never` on the TypeScript face and a refusal by -name on the Zod mirror, so there is no declared-but-dead handler left to forward. diff --git a/.changeset/7079-schema-reference-base-props.md b/.changeset/7079-schema-reference-base-props.md deleted file mode 100644 index dfcfe3b8e3..0000000000 --- a/.changeset/7079-schema-reference-base-props.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -Docs only — the canonical `BaseSchema` "Common Properties" table in -`content/docs/api/schema-reference.md` (objectui#7079). No package source, no -published contract and no runtime behaviour is touched, hence the empty -declaration; `apps/site` is `private: true` and sits in `.changeset/config.json`'s -`ignore` list, so nothing under `content/` ships from this change. - -The table narrowed five declared unions to one limb each and omitted five declared -members outright. Measured against `packages/types/src/base.ts` and its Zod mirror -`packages/types/src/zod/base.zod.ts`, whose agreement is held by -`base-schema-zod-mirror-parity.test.ts` reading the mirror's own `.shape`: -`label` and `description` are `string | I18nLabel`, `ariaLabel` is -`string | KeyedI18nLabel` (the KEYED form, not the inline locale map), and both -`visible` and `disabled` take a predicate expression string as well as a boolean — -the expression limb is on the base key itself, not only on the `visibleOn` / -`disabledOn` siblings the paired cells implied. `placeholder`, `style`, `data`, -`bind` and `visibleWhen` had no row at all. - -The three combined cells (`visible` / `visibleOn`, `hidden` / `hiddenOn`, -`disabled` / `disabledOn`) are split into one row per member. Packing two members -with different types into a single `boolean` / `string` cell is what made the -error invisible: the pairing reads as a complete, ordered account, and that -appearance of completeness is precisely what hid the third fact. One row per -declared member, in declaration order, makes completeness checkable by reading the -table against the interface. - -Nothing in CI reads this table — `check:doc-snippet-types` compiles only `ts` / -`tsx` / `typescript` fences, `check:doc-component-types` judges `type` string -literals and key tables anchored on a `Namespaced key | Bare-name fallback` -header, and no script in the repository parses a Markdown property table. diff --git a/.changeset/7081-overlay-trigger-union.md b/.changeset/7081-overlay-trigger-union.md deleted file mode 100644 index cf244ff222..0000000000 --- a/.changeset/7081-overlay-trigger-union.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**The overlay family's `trigger` slot now declares the node array its Zod mirror, its runtime and its own shipped defaults already accept** (objectui#7081). - -`trigger` on `DialogSchema`, `AlertDialogSchema`, `SheetSchema`, `DrawerSchema`, `PopoverSchema`, `HoverCardSchema` and `DropdownMenuSchema` widens from `SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face — the spelling `ContextMenuSchema` and `TooltipSchema` already carried. Each member keeps its optionality (the first four optional, the last three required). `SchemaNode` itself is unchanged. - -This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/overlay.zod.ts` already spelled every one of these keys `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: every overlay renderer hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and every registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying a renderer's own default into a typed document is no longer a type error against the type that shipped it. The seven docs pages' `trigger` rows follow the declaration. - -Triage on the card (2026-09-03): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). diff --git a/.changeset/7082-overlay-node-slot-doc-type-names.md b/.changeset/7082-overlay-node-slot-doc-type-names.md deleted file mode 100644 index 085c52e929..0000000000 --- a/.changeset/7082-overlay-node-slot-doc-type-names.md +++ /dev/null @@ -1,73 +0,0 @@ ---- ---- - -Docs only, publishes nothing: six component pages spelled a node slot -`ComponentSchema`, a real shipped export (`packages/types/src/blocks.ts`) that -is **not** a node slot — it is the concrete `type: 'component'` block. The keys -carry `SchemaNode` (`packages/types/src/base.ts`), which also admits -`string | number | boolean | null | undefined`. A reader who looked the -published name up found a narrow, unrelated type. - -Every row is re-derived from its own declaration on `2c3cd1b`, per page — not -by a string replace, because the declarations disagree with each other on -whether the array form is admitted. - -| page row before | declaration on `main` | page after | -| --- | --- | --- | -| `AlertDialogSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:91`) | `SchemaNode` | -| `ContextMenuSchema.trigger?: ComponentSchema` | `trigger?: SchemaNode \| SchemaNode[]` (`overlay.ts:486`) | `SchemaNode \| SchemaNode[]` | -| `HoverCardSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:296`) | `SchemaNode` | -| `HoverCardSchema.content: ComponentSchema` | `content: SchemaNode \| SchemaNode[]` (`overlay.ts:292`) | `SchemaNode \| SchemaNode[]` | -| `DropdownMenuSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:433`) | `SchemaNode` | -| `SheetSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:150`) | `SchemaNode` | -| `SheetSchema.content: ComponentSchema` | `content?: SchemaNode \| SchemaNode[]` (`overlay.ts:146`) | `SchemaNode \| SchemaNode[]` | - -**`dropdown-menu` stays singular on purpose.** Its Zod mirror -(`zod/overlay.zod.ts:176`), its sibling `ContextMenuSchema`, and its own shipped -`defaultProps` (`renderers/overlay/dropdown-menu.tsx:130`) all use the array -form, and the published TS type refuses it. Which side is right is objectui#7081, -open and awaiting a maintainer decision on a published-type widening. Publishing -the array form here would have put a claim in the docs that the shipped type -rejects and pre-empted that ruling; the page therefore follows the type an -author's editor reads. The incoherence is real, and it *is* #7081. - -**Two of the nine rows the card named are NOT renamed**, because no honest -docs-only edit resolves them, and both are filed instead: - -- `AlertDialogSchema.actions?: ComponentSchema[]` — declared on neither the TS - interface (`overlay.ts:78-127`) nor the mirror (`zod/overlay.zod.ts:42-55`), - and read by nothing. Renaming it would have kept a phantom key alive under a - second wrong type. -- `EmptySchema.action?: ComponentSchema` — declared on neither - (`feedback.ts:210-224`, `zod/feedback.zod.ts:120-125`), but *read* by the - shipped renderer through a cast (`renderers/feedback/empty.tsx`), which - requires `typeof === 'object'`. `SchemaNode` admits `string | number | - boolean`, so publishing it there would have been a new false claim, not a - correction. - -Requiredness is likewise left alone and filed: `AlertDialogSchema.trigger`, -`SheetSchema.trigger` and `SheetSchema.content` are declared optional and -published required. That is objectui#7073's defect class, already fixed by it on -`context-menu.mdx`, and is out of this card's fence. - -`packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts` pins the -result, and this is a page where the pin is the only evidence there is: all -these rows sit in `plaintext` fences, `check:doc-snippets` compiles -`ts`/`tsx`/`typescript` only and `check:doc-types` reads only the `type` string -literals, so no gate parses them (objectui#5250, objectui#5867). A green CI run -here says "nothing else broke", not "the correction is right". - -The pin's authority is the **TS declaration**, not the Zod mirror — the one -deliberate departure from the objectui#6347 model, because on this tree the two -disagree on four `trigger` rows and pinning against the mirror would have -published #7081's answer. Its type-level leg is compiled by -`packages/types/tsconfig.test.json`, so widening `DropdownMenuSchema.trigger` -turns it red and whoever lands #7081 is told this page owes an update. The two -undeclared rows and the three requiredness divergences are pinned as -divergences, so a fix on either side of any of them also turns it red. - -No fence moved: each page holds one `plaintext` fence marker pair before and -after, so objectui#5867's shrink-only declared population is unchanged. No -declaration, mirror, renderer or fixture was edited. - -Part of objectui#7082. diff --git a/.changeset/7086-blockquoted-fence-collector.md b/.changeset/7086-blockquoted-fence-collector.md deleted file mode 100644 index 2676771491..0000000000 --- a/.changeset/7086-blockquoted-fence-collector.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -Doc-snippet gate tooling only, no published package source changed. - -`check-doc-snippet-types` collects blocks with `scanFences`, whose fence-opening -anchor accepted a run of leading spaces and tabs and nothing else. A fence -opened inside a Markdown blockquote carries a `> ` prefix, so the anchor never -matched, the block was never collected, and the gate compiled nothing for it. -There was no diagnostic: an uncollected block appears in no count, and its page -still reports as covered. A callout is a natural home for an import example, -which is exactly the snippet class that rots when an export is renamed — the one -class this gate exists to catch. - -The opener now tolerates a blockquote prefix and carries the opener's quote -DEPTH through the rest of the walk: the search for the closing fence reads -candidate lines at that same depth, and every body line is stripped of that many -markers before it reaches the compiler. Depth 0 — every unquoted fence in the -corpus — takes an identity path that returns the line unchanged byte for byte, -so the other 773 collected blocks scan exactly as they did before. Stripping -consumes at most one space after each marker, per CommonMark, so indentation -belonging to the snippet survives. - -Carrying the depth to the CLOSING fence is what makes this safe in both -directions. Without it a blockquoted fence would find no close and swallow the -rest of the file; and a plain fence would be closed early by any `> ` + backticks -line quoted inside it as prose. Both directions are pinned. - -Measured over the gate's own 224-document population: collected blocks 773 → 774. -The one newly-collected block is the import callout at -`content/docs/api/schema-reference.md` line 12, and it compiles — the gate's -semantic phase judges 272 of 272 with 0 failures. Nothing left the population. diff --git a/.changeset/7087-disabled-twin-symmetry.md b/.changeset/7087-disabled-twin-symmetry.md deleted file mode 100644 index a4e1a68a54..0000000000 --- a/.changeset/7087-disabled-twin-symmetry.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`disabled` accepts a predicate string — `boolean | string`, the `BaseSchema` union — on -the 18 concrete schemas that used to narrow it back to `boolean` (objectui#7087, -maintainer ruling 2026-09-01: option 1, scoped to `disabled`). - -`visible` and `disabled` are twins: objectui#4581 widened both on `BaseSchema` on the same -evidence — `SchemaRenderer` evaluates both through `evaluator.evaluateCondition` rather -than reading either as a boolean. After that widening, 0 of the 124 `extends BaseSchema` -interfaces redeclared `visible`, while 18 still carried a pre-widening -`disabled?: boolean` of their own, with matching `z.boolean()` mirrors. So -`disabled: "${data.status === 'locked'}"` — the capability the renderer implements and -the base type advertises — was a type error and a zod refusal on `ButtonSchema`, -`InputSchema`, `TextareaSchema`, `SelectSchema`, `CheckboxSchema`, `RadioGroupSchema`, -`SwitchSchema`, `ToggleSchema`, `SliderSchema`, `FileUploadSchema`, `DatePickerSchema`, -`CalendarSchema`, `InputOTPSchema`, `FormSchema`, `ComboboxSchema`, `ActionSchema`, -`CollapsibleSchema` and `ToggleGroupSchema`. - -Those 18 redeclarations are removed, on both faces. The interfaces inherit -`BaseSchema.disabled` the way they always inherited `visible`; the zod mirrors inherit -`base.zod.ts`'s `z.union([z.boolean(), z.string()])` through `.extend()`'s merged -`.shape`, so there is no second spelling of the union to drift from — the route -`ChatbotSchema` took in objectui#6169. - -**Additive for authors**: a predicate string is now accepted where it was refused; every -boolean that parsed before parses unchanged, and a number is still refused at path -`disabled`. Runtime behaviour does not change — the renderer already evaluated both twins. - -**Out of scope, per the ruling**: `label` (29 narrowings) and `description` (32) carry -`string | I18nLabel` i18n semantics and wait for their own ruling; the independent -`disabled?: boolean` declarations on shapes that do not extend `BaseSchema` -(`SelectOption`, `RadioOption`, `FormField`, `ComboboxOption`, `AccordionItem`, -`ToggleGroupItem`, and the rest of that family) are not narrowings and are untouched. diff --git a/.changeset/7088-hidden-jsdoc-single-hide-path.md b/.changeset/7088-hidden-jsdoc-single-hide-path.md deleted file mode 100644 index aca98a42e6..0000000000 --- a/.changeset/7088-hidden-jsdoc-single-hide-path.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Correct `BaseSchema.hidden`'s JSDoc: it hides by NOT RENDERING, exactly as -`visible: false` does (objectui#7088, maintainer ruling 2026-09-01). - -The declaration promised "Controls whether the component is hidden (but still -rendered) … component is rendered but not visible (visibility: hidden)". The -renderer has never done that. `visible`, `visibleWhen`, `visibleOn`, -`visibility`, `hidden` and `hiddenOn` are legs of one `shouldHide` chain in -`SchemaRenderer`; every leg feeds the same `_hidden` flag, and `_hidden` has -exactly one consumer — `if (evaluatedSchema._hidden) return null`. No node -survives for either key, and nothing in the repo emits a `visibility` style. The -sibling `visible` comment claimed `display: none` on the same false premise and -is corrected with it. - -**Comment-only — no behaviour moves.** The other reading, keeping the node in the -tree and hiding it visually, was weighed and **declined**: it is a behaviour -change on a published prop with zero named consumers, so an accessibility or -animation use-case that wants it reopens the question as its own feature card. -The JSDoc now records the synonymity as a decision, so the next reader does not -read "two keys" as "two behaviours", and notes that synonymous in OUTCOME is not -synonymous in PRECEDENCE — a declared `visible` short-circuits `hidden`, which -is unchanged and pinned elsewhere. - -Why a comment was worth a changeset: the JSDoc is the authority a later docs -correction is measured against, and this one nearly propagated. While splitting -the schema-reference `hidden` row, a reader checked it against `base.ts` and -almost "corrected" the table's "Inverse of `visible`" — the half that describes -shipped behaviour — toward the declaration. That row is unchanged and stays. -`SchemaRenderer.hiddenVisibleSynonymy.test.tsx` now pins the claim the comment -makes: the two keys produce the same rendered output, and `hidden: true` leaves -no node that could carry a `visibility` style. diff --git a/.changeset/7092-launcher-exclusion-reason.md b/.changeset/7092-launcher-exclusion-reason.md deleted file mode 100644 index 19513e04b1..0000000000 --- a/.changeset/7092-launcher-exclusion-reason.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Corrects the `PALETTE_EXCLUSIONS` reason string for `app:launcher` in the Studio -page-palette ledger. It read "shell singleton — the app shell renders it, not a page", -and objectui#7091 registered a real `app:launcher` renderer, so a page can now render it -and the "not a page" clause became false. The reason now reads "shell singleton — lives -in the app shell chrome", mirroring the neutral form its sibling `global:notifications` -has carried since objectui#6757 shipped that type a renderer without its wording rotting: -the entry describes WHERE the thing lives, not whether a renderer exists, so a later -renderer cannot falsify it. - -The exclusion itself is an unchanged decision — `app:launcher` stays out of the page -palette, and its key and position in the ledger are untouched. Whether it should become -palette-authorable is a separate product question this does not answer. No published -behaviour moves: the reason strings are developer-facing ledger prose, read by no runtime -code path, and `PALETTE_EXCLUSIONS` is not exported from `@object-ui/app-shell`'s entry. diff --git a/.changeset/7104-alert-dialog-read-dialect-declared.md b/.changeset/7104-alert-dialog-read-dialect-declared.md deleted file mode 100644 index 6ecda53472..0000000000 --- a/.changeset/7104-alert-dialog-read-dialect-declared.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`AlertDialogSchema` now declares the four keys the `alert-dialog` renderer actually reads -(objectui#7104): `content` (the dialog body, `SchemaNode | SchemaNode[]` like every sibling -overlay), `cancelText` and `actionText` (the footer's two button labels — each button renders -only when its label is set; there is no renderer default) on BOTH faces, and `onAction` (the -confirm button's click handler) as a RUNTIME SLOT in the objectui#6124 shape: callable on the -TypeScript face, refused by name in the zod mirror because JSON has no function value. - -Until now none of the four was declared anywhere. They were accepted only through -`BaseSchema`'s `[key: string]: any` and the mirror's `.passthrough()` — no editor completed -them, no page named them, and a wrong-typed value rode through unexamined — while the keys -the type DID declare for the same affordance (`cancelLabel` / `confirmLabel` / -`confirmVariant`) are read by nothing, so a document written strictly against the shipped -type rendered an empty footer. The renderer's own registered `inputs` and `defaultProps` were -already written in the read dialect; this change makes that single de-facto contract legible -instead of minting a second one (AGENTS.md #0.1: one strict contract, not N dialects). - -**Accept-set change on the published zod mirror — breaking, shipped as `minor` per this -repo's version-alignment policy (majors track `@objectstack`).** Declared keys are validated -even under `.passthrough()`, so three documents that parsed green yesterday are refused -today, each at its own path: `cancelText` or `actionText` carrying a non-string -(`cancelText: 123` — the renderer drew it as button text), `content` carrying a value that is -not a node or node array (an object without `type`), and `onAction` carried at all (a JSON -author cannot write a function; a string or object there was accepted and forwarded to the -button, where it did nothing or threw at click). A document in the read dialect with -well-typed values parses exactly as before and its values now survive the parse typed. -Undeclared keys still pass through unchanged. On the TypeScript face, `cancelText: 123` is -now a compile error at the key where the index signature used to absorb it. - -**No renderer change; no runtime behaviour changes.** The `alert-dialog` renderer, its -`inputs` and its `defaultProps` are untouched. The three declared-but-unread keys are -deliberately NOT retired here — that is a narrowing with its own card and its own grade; -their per-key liveness readings are on objectui#7104. - -Docs: `content/docs/components/overlay/alert-dialog.mdx` now publishes the read dialect in -its Schema block and no longer lists `actions?: BaseSchema[]`, a key no surface ever carried. -The four schema-catalog examples that page embeds still author `actions` and render an empty -footer — filed as objectui#7693, not converted here (the conversion is lossy). diff --git a/.changeset/7113-chart-data-model.md b/.changeset/7113-chart-data-model.md deleted file mode 100644 index f39125ffaf..0000000000 --- a/.changeset/7113-chart-data-model.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ChartSchema` declares the data model it renders — chart-level `data` and `xAxisKey`, with -the bare-string `xAxis` folded onto the latter — and `ChartDataSeries` accepts both binding -dialects (objectui#7113 option B, 项目总监席 总监批 #28 2026-09-01 「同意」; and -objectui#6939's `chart` row, maintainer ruling 2026-09-02 「同意」 — both rulings -independently instructed declaring these two keys, so they land as one change). - -⚠️ Shipped as `minor`, not `patch`, because two document classes that validated before now -REFUSE. objectui#6939 grades this class "patch where the accept set only widens toward what -already renders"; this change is not a pure widening, so it takes the level objectui#6896 -set for the same transition in this same file — the mirror starting to refuse — and for the -same reason: this repository's `major` is a cross-repo pin to `@objectstack`'s major rather -than a severity dial, so the break is announced here, which is the channel that carries it. - -## What now refuses (the narrowing, named) - -**Three** classes validated before and refuse now. The first two survived only on -`BaseSchema`'s `.passthrough()`; the third was silently STRIPPED by the non-strict -`ChartDataSeriesSchema` object. - -```jsonc -// 1. chart-level `data` that is not an array of row objects -{ "type": "chart", "chartType": "bar", "data": "oops" } // now: [data] expected array -{ "type": "chart", "chartType": "bar", "data": [1,2,3] } // now: [data.0] expected object - -// 2. a non-string `xAxisKey` -{ "type": "chart", "chartType": "bar", "xAxisKey": 123 } // now: [xAxisKey] expected string - -// 3. a non-string `series[].dataKey` ⚠️ THIS ONE DRAWS A REAL CHART TODAY -{ "type": "chart", "chartType": "bar", - "series": [{ "name": "a", "dataKey": 123 }] } // now: [series.0.dataKey] expected string -``` - -⚠️ **Class 3 is the sharp one and is called out separately.** Classes 1 and 2 are malformed -documents whose chart was already broken. Class 3 is not: at base it parsed to -`series: [{ name: 'a' }]` (the non-string `dataKey` stripped in silence) and -`normalizeChartSchema` renders it — `str(123)` is `undefined`, so the read falls back to -`name` and yields `series: [{ dataKey: 'a' }]` (`normalizeChartSchema.ts:239`). So this is a -narrowing away from a document that **renders today**, which is precisely the distinction -objectui#6939's grading language turns on. `dataKey: null` behaves identically. Measured on -both states; the declaration itself is right, and this note is the disclosure it was owed. - -## Corrected: what class 2 actually did - -An earlier draft of this changeset said `xAxisKey: 123` "drew an EMPTY CHART". The read -sites do not support that: `ChartRenderer.tsx:133` takes `schema.xAxisKey` raw and the rows -still reach `data` at `:164`, while the normaliser drops the key (`str(123)` is `undefined`). -Measured through `normalizeChartSchema`, the result keeps the series and loses only the -category binding — **a drawn chart with a broken category axis**, not an empty one. Class 1 -(`data` malformed) is the one that leaves nothing to plot. - -## Also changed on the published surface: combinators - -Both consts now carry a check (`ChartSchema` the `xAxis` fold, `ChartDataSeriesSchema` the -at-least-one-binding refinement), and on zod 4.4.3 that makes three combinators **throw** -where they previously returned a schema: - -``` -ChartSchema.pick(…) / .omit(…) / .partial() -> throws "cannot be used on object -ChartDataSeriesSchema.pick(…) / .omit(…) / … schemas containing refinements" -``` - -`.extend()` with a NEW key still works and preserves the fold and the refinement; -`.optional()`, `z.discriminatedUnion`, `z.toJSONSchema` and `safeValidateSchema` are all -unaffected. Nothing in this repository calls the throwing combinators on either const, and -the published surface already ships refined mirrors (`objectql.zod.ts`, `complex.zod.ts`, -`form.zod.ts`, `app.zod.ts`), so the class is not new — but it is a real behaviour change on -a published export and it belongs in the release note rather than in a reviewer's file. - -## What now validates (the widening) - -`series: [{ dataKey: 'revenue' }]`. `normalizeSeries` reads -`str(raw.dataKey) ?? str(raw.name)`, so `dataKey` alone has always been a complete binding -— but the mirror REQUIRED `name` and refused it. That is why both catalog chart fixtures -(`advanced-line-chart.json`, `area-chart.json`) failed validation: they are the `chart: 2` -entry in `objectui check`'s 28-file census. `name` is now optional, `dataKey` is declared, -and a series binding to NEITHER is refused by name at `series.N.name` — the same path the -required flag used to report, so the diagnostic did not move. - -## `xAxis` folds; it does not become a second name - -`xAxis: 'month'` is accepted at input and is ABSENT from the output, having landed on -`xAxisKey`. When both are written the canonical key is kept and the alias dropped — not a -precedence rule minted here, but the one already running at `normalizeChartSchema.ts:292`, -where `xAxisKey` is the first limb of `str(schema.xAxisKey) ?? xAxisSpec?.field ?? -str(xAxisRaw)`. No chart that renders today changes what it renders. - -⚠️ The `xAxis` **config object** (`{ field, format, title, showGridLines }`) is NOT folded. -Only the bare string is a sibling spelling of `xAxisKey`; the object's presentation keys -survive separately into `out.xAxis` (`normalizeChartSchema.ts:289-291`), and folding it -would discard them. - -## Not done, deliberately - -objectui#6939's `chart` row also says "`series[].data` stops being required". On this base -it already is not: objectui#6896 replaced it with `retirementTombstone(...)` — -`z.never({ error }).optional()` — which is optional AND refuses any authored value by name. -Implementing the clause literally would re-widen a retired key and reverse a landed ruling, -so it is not done. - -## FROM → TO - -```ts -// ChartDataSeries -- name: string; -+ name?: string; -+ dataKey?: string; - -// ChartSchema -+ data?: Array>; -+ xAxisKey?: string; -``` diff --git a/.changeset/7115-root-readme-doc-gate-surface.md b/.changeset/7115-root-readme-doc-gate-surface.md deleted file mode 100644 index fe4c97f883..0000000000 --- a/.changeset/7115-root-readme-doc-gate-surface.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Docs and gates only: the root `README.md` — the repository's landing page and the -most-read authored file in it — was outside the scan surface of every doc gate, -and had been teaching `stat-card` four times in its flagship "dashboard in JSON" -example. Nothing registers `stat-card`, so a reader who copied the headline -snippet got four OBJUI-001 "Unknown component type" panels. - -The file now joins the scan surface of `check-doc-component-types` and -`check-doc-snippet-types`, and the four widgets are retargeted onto `statistic`, -which is registered and declares a `value` carriage row, so the example keeps its -expressions. Per the maintainer's 2026-09-01 ruling on the (A)/(B) fork — -option (B), no new carriage rows — four documentation sites that authored `${…}` -in keys with no carriage row now teach what actually renders instead. - -No package source changed, so this ships nothing: `check-changeset-presence` -independently reports "no changeset is owed" for this diff. The declaration -states the release intent rather than bumping anything. diff --git a/.changeset/7121-studio-canvas-leaf-affordances.md b/.changeset/7121-studio-canvas-leaf-affordances.md deleted file mode 100644 index fee18dc171..0000000000 --- a/.changeset/7121-studio-canvas-leaf-affordances.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio Interfaces: no Design mode and no "click a block" rail on leaves that have -no block canvas (objectui#7121). - -`registerStudioCanvasPreview(type, …)` opts a type into a surface-specific canvas -that renders the running app rather than an editable draft — a contract, not a -habit: `StudioCanvasPreviewProps` carries no `selection`, `onSelectionChange`, -`onPatch` or `editing`. Two affordances beside such a leaf ignored that. - -- The Design/Run switch (objectui#5800) was still offered, though `editing` is - handed to exactly one canvas branch (`Preview`). On a studio-canvas leaf the - switch moved `canvasMode` and reached no renderer — a live-looking control - wired to nothing. It is now gated. -- The right rail fell through to "Click a block on the canvas, and edit its - properties right here." beside a canvas that has no blocks, so the instruction - could not be followed. It now states what the canvas is, and — because this - canvas has no blocks by contract — promises no recovery. -- The rail's new branch is ordered ahead of the selection branch, so a block - selected on a *different* leaf no longer opens a scoped inspector for a block - this canvas does not contain; the header's "clear selection" button is gated - with it. - -The discriminator is `StudioCanvas`, not `isEditable`. `isEditable` is -`!!Preview && !StudioCanvas` — a conjunction of two independent causes — so -gating on it would also strip these affordances from leaves whose only fault is -that their own type has no designer, the state objectui#6795 part C pinned as -still deserving the ordinary rail. Behaviour on every leaf with a block canvas -is unchanged. diff --git a/.changeset/7122-capability-manage-org-presentation.md b/.changeset/7122-capability-manage-org-presentation.md deleted file mode 100644 index 1c0c47e35b..0000000000 --- a/.changeset/7122-capability-manage-org-presentation.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/i18n': patch ---- - -Carry `manage_org_presentation`, the ninth platform capability (objectui#7122). - -`@objectstack/spec` 17.3.0 declares a ninth member of `PLATFORM_CAPABILITIES` -and the capability picker's curated set carried eight, so -`CapabilityMultiSelectField` fell back to the `sys_capability` registry's -English label for it in every locale — the exact defect objectui#6285 filed -when `manage_sharing` did the same thing. - -The label is the spec artifact's own (`Manage Organization Presentation`), read -off the installed build rather than invented, and it is authored everywhere the -widget's docblock requires of any edit to that list: `useFieldTranslation.ts` -and all ten locale packs. Each non-English string is composed from that pack's -own established sibling vocabulary (`manage_org_users`, -`manage_platform_settings`) rather than machine-translated; a native review pass -is welcome on the nine, and nothing about the capability's behaviour depends on -the wording. - -The parity pin is unchanged and still fails on ANY difference in either -direction, which is what made this visible before it reached a screen. diff --git a/.changeset/7122-eager-closure-ceiling-raise.md b/.changeset/7122-eager-closure-ceiling-raise.md deleted file mode 100644 index 3faac05948..0000000000 --- a/.changeset/7122-eager-closure-ceiling-raise.md +++ /dev/null @@ -1,50 +0,0 @@ ---- ---- - -Raise the console eager-closure ceiling once, on explicit maintainer -authorisation, to the measured cost of `@objectstack/spec` 17.3.0 -(objectui#7122). Tooling only; no package is released by this change. - -Raising a gate ceiling is a gate weakening and sits on the manual floor, so it -is a human's decision. The authorisation, verbatim: - - 维护者 sam@objectstack.ai 于本轮明确授权:「抬上限,把 7685 弄绿」 - -recorded on objectui#7122 as decision batch 1 item 1 = "B + A": a one-time, -cause-recorded adjustment of exactly the measured delta, with the upstream card -filed alongside it. - -**What the bytes buy — this is NOT routine growth.** The residue is -`@objectstack/spec` 17.3.0's own browser-dist growth: +292.2 KB gzip on the spec -package alone, whose measured mechanism is that 17.3.0 lengthened the Zod -`.describe()` doc strings shipped in the browser build. It is authoring -documentation prose, delivered on every page load. - -**It is not duplication.** That was the larger, separate problem and it is -already fixed: resolving the spec alone to 17.3.0 shipped two copies of it, and -moving the family together in the lockfile collapsed that (−671 KB; markers -unique to 17.2.0 fell from 92.3% to 1.0% of 104, the single survivor accounted -for). No chunk entered or left the closure and the eager chunk count is -unchanged. - -**The honest long-term fix is upstream**, and this ceiling is the marker for it -rather than the answer to it: a `describe()`-stripped browser build returns -~292 KB to every consumer of the spec, not just this console. Filed as -objectstack#16063, and recorded in the gate as the restore condition — when it -lands, re-measure and bring the ceiling and its baseline back down together. - -Four constants move as two ceiling/baseline pairs, measured by `pnpm build` -(exit 0, 43/43) on `34a1578ef`: - -| constant | from | to | headroom vs the 91,136-byte regression | -|---|---|---|---| -| `MAX_EAGER_CLOSURE_GZIP_BYTES` | 3,268,000 | 3,597,000 | 45,809 = 0.50x | -| `BASELINE.gzipBytes` | 3,222,314 | 3,551,191 | — | -| `PER_CHUNK_GZIP_CEILINGS['vendor-objectstack']` | 967,000 | 1,254,000 | 18,971 = 0.21x | -| `PER_CHUNK_BASELINE['vendor-objectstack']` | 948,461 | 1,235,029 | — | - -Both ratios match the proportions the retiring pairs carried (0.50x and 0.20x), -so the gate keeps failing on a repeat of the 89 KiB regression that motivated -it. No other exemption was added, no import was made lazy, and the three -per-chunk lines that still pass — `i18n-locales`, `framework`, `ui-components` — -were left exactly as they are. diff --git a/.changeset/7122-objectstack-family-17-3-0.md b/.changeset/7122-objectstack-family-17-3-0.md deleted file mode 100644 index 7fe38e7a4e..0000000000 --- a/.changeset/7122-objectstack-family-17-3-0.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -Reconcile `@object-ui/data-objectstack` with the `@objectstack/*` family at -17.3.0 (objectui#7122). - -`@objectstack/client`, `core`, `formula` and `lint` each pin `@objectstack/spec` -EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO -copies of it. Moving the family with it in `pnpm-lock.yaml` collapses the -duplicate; every declared range already admitted 17.3.0, so no manifest moved. - -The one source change the family bump forces is a type reconciliation, not a -behaviour change. `client.analytics.query` resolved to `Promise` at 17.2.0 -and resolves to `Promise` at 17.3.0, so the pre-envelope -branches of `aggregate`'s row-shape fallback stopped type-checking. Those -branches are read through a widened alias rather than deleted: the client's own -docblock records the runtime change behind the narrower type ("BREAKING since -objectstack#13079 — read `result.rows`, not `result.data.rows`"), and deleting -them is a runtime compatibility decision about servers older than that, not a -type repair. The alias restores exactly the compile-time latitude 17.2.0 gave -the same expression and changes no runtime byte of it. diff --git a/.changeset/7122-postcss-dedupe.md b/.changeset/7122-postcss-dedupe.md deleted file mode 100644 index 71203b4c10..0000000000 --- a/.changeset/7122-postcss-dedupe.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Dev-time only: collapse the `postcss` duplicate this branch's first lockfile -bump introduced (objectui#7122). - -`aabc527cb` resolved `@objectstack/spec` to 17.3.0 and, as a side effect of that -install, re-resolved `autoprefixer`'s auto-installed `postcss` peer to 8.5.28 -while `@tailwindcss/postcss` kept 8.5.26. `@object-ui/cli` depends on both and -declares `postcss` itself, so its type-check program held TWO postcss copies -with two structurally distinct `Plugin` types — and comparing -`Plugin & ExportedAPI` against `Plugin` across them exceeded the compiler's -instantiation budget: `TS2321: Excessive stack depth`, in a file this branch -never touched. `origin/main` carries one copy for that pair and is green. - -The split is resolved in the lockfile only. All eight manifests that declare -`postcss` already declare `^8.5.26`, which admits 8.5.28, so no declared range -moved and no published dependency declaration differs by a byte. Empty -frontmatter is the deliberate "no release" declaration. diff --git a/.changeset/7122-spec-17-3-0-parity-reconciliation.md b/.changeset/7122-spec-17-3-0-parity-reconciliation.md deleted file mode 100644 index 33c86a70e6..0000000000 --- a/.changeset/7122-spec-17-3-0-parity-reconciliation.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': patch -'@object-ui/cli': patch -'@object-ui/plugin-detail': patch ---- - -Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122). - -⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.** -The exported `ObjectSchemaClientExtensions` narrows from - -```ts -export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' } -``` - -to - -```ts -export type ObjectSchemaClientExtensions = Record; -``` - -Two breaking consequences for a consumer that names the type directly. **(1)** It -no longer declares `editMode`; the key is now carried by the spec's -`ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against -the extension type ALONE loses it. **(2)** `interface` → type alias also ends -**declaration merging**: a consumer that reopened -`declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }` -to add its own client-side member no longer compiles, because an alias cannot be -reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule — -objectui's own breaking changes are graded `minor` with the semantics stated in -the body, since any `major` in the fixed group would push all 39 packages off -`@objectstack`'s major. - -**`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured: -the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty, -declared as the same `'page' | 'modal'` union objectui carried). Its local copy -is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin -prescribed for this event, leaving the client delta empty. Nothing is removed -from the product: `editMode` stays authorable and stays typed on -`ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a -local member — and a published, spec-validated object document may now carry it, -which at 17.2.0 was refused by name. - -**`user:profile` is retired across all three sites.** 17.3.0 dropped it from -`PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly -`['user:profile', 'element:form']`, gained set empty). objectui went on knowing -it in three places, so all three moved together: the Studio palette exclusion -ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the -regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing -user-reachable went with it — neither type had a renderer, `user:profile` had -only the dashed "Component Placeholder" scaffold, and the app shell's own -profile affordance is a React slot, never this block type. A page schema still -naming it now draws the loud "Unknown component type" panel rather than a silent -grey box, which is this repo's standing treatment for a type outside the -supported surface. - -**`record:details` sections document the eight keys 17.3.0 added.** -`group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`, -`description` and `headerColor` are now declared on a section entry (4 → 12 -members). Six of the eight are already honoured by `DetailSection`, so the -`sections` input description now teaches all of them, and says plainly which two -are not read here. Designer controls for them are a separate feature and are -deliberately not added. - -**`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` → -`^17.3.0`, and this is the second half of its `minor`.** The package's emitted -`dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which -`17.0.0` does not export, so the old range was a claim the artifact did not -support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]` -and names `^17.3.0` as the lowest version carrying every symbol the package -references. Breaking for a consumer pinned below 17.3.0: it can no longer -resolve this package. That is the range stating the truth rather than a new -restriction — the artifact already required those symbols — and it is the -remedy the gate itself prescribes ("Raise that package's range to the lowest -version that exports the symbol… Do not add a tolerant re-declaration on this -side: the range is the claim, and the claim is what is wrong", objectui#5793). -`@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0` -and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is -this repo's normal state, not an exception. - -⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing -and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main -workflow that cannot red a pull request, and `main` is green on it — the last -eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1 -with CI's own `--cross-check` invocation, and exits 0 with this raise, judging -278 (subpath, symbol) pairs across 19 published packages either way. Its blocking -copy runs on the publish path, so leaving it would have surfaced as a cancelled -release rather than as a red check. The correction is recorded on objectui#7688. diff --git a/.changeset/7122-spec-17-3-0-pin-repointing.md b/.changeset/7122-spec-17-3-0-pin-repointing.md deleted file mode 100644 index 86678dc722..0000000000 --- a/.changeset/7122-spec-17-3-0-pin-repointing.md +++ /dev/null @@ -1,49 +0,0 @@ ---- ---- - -Re-point the pins `@objectstack/spec` 17.3.0 moved, and harvest three -exemptions the contract outgrew (objectui#7122). Test only; no package is -released by this change. - -- **The `#7496` family, 14 assertions across three files.** 17.3.0 kept every - `submitBehavior.url` refusal and its reasoning and only restated provenance as - `(ruled 2026-08-11)`. The pins asserted the citation FORM, not the citation — - objectui#7702's defect shape — so they now assert the durable property via a - documented `CITES_ITS_RULING`, which discriminates the spec's prose from a - locally hand-written sentence. Deliberately not re-pinned to the new prose - verbatim, which would move the brittleness rather than remove it. -- **`CITES_ITS_RULING` is `/\(ruled \d{4}-\d{2}-\d{2}(?: on #\d{3,})?\)/`, - and its first spelling was not good enough.** That draft was - `/\(ruled \d{4}-\d{2}-\d{2}\)|#\d{3,}/`, whose loose alternative - discriminated NOTHING — this repo's own hand-written messages routinely cite - `objectui#NNNN`, so a local sentence satisfied it, and telling those two apart - is the assertion's whole job. It was also unnecessary: measured on both - installed artifacts, 17.2.0 says `(ruled 2026-08-11 on #7496)` and 17.3.0 says - `(ruled 2026-08-11)`, so the issue number never appears outside that - parenthesis and `(ruled ` + a date already matched both. The optional - ` on #NNNN` tail keeps the 17.2.0 spelling admissible without admitting a bare - local `#7122`. -- **The fourth file joined the other three.** - `packages/plugin-form/src/submitRedirect.test.ts` had been re-pointed to the - literal `'ruled 2026-08-11'` while its three siblings used the shape — the - same brittleness one word along, a date instead of an issue number, and four - files disagreeing about what the pinned property is. It now uses - `CITES_ITS_RULING` too. -- **`object-grid.defaultSort` became an ADR-0087 D2 tombstone**, so its - unpublished-key exemption, its GA-pending entry and its carve-out row are - deleted — the ninth harvest by the mechanism the eight before it were designed - to die by. The block's prose claiming a tombstone would NOT make an entry - stale is corrected; its own cited counter-example was harvested that way. -- **`OFF_SPEC_ARM_EXEMPTIONS` is now empty.** `element:number.filter` accepts - the array form (objectui#6206) and `object-grid.data` accepts the - discriminated `ViewDataSchema` object (objectui#6207) — both converged - upstream in the direction their own reasons named, with no declaration edited. - Both issues are closable as resolved-upstream. -- **Two negative pins re-derived, not inverted.** The `object-master-detail-form` - `formType` boundary now refuses what it used to admit (`invalid_value`, plus a - bespoke ADR-0001 prescription for `wizard`), and the location value schema is - now strict (`unrecognized_keys` naming the retired `latitude`/`longitude` - pair). Each pin records the new fact, keeps a control opposite it, and pins the - refusal's SHAPE. The `LocationField` fence explicitly re-derives why its - key-level assertions stay: no parse stands on the emit path, so strictness - changed the consequence of a spread regression, not the widget's exposure. diff --git a/.changeset/7122-spec-17-3-0-reconciliation.md b/.changeset/7122-spec-17-3-0-reconciliation.md deleted file mode 100644 index d098104278..0000000000 --- a/.changeset/7122-spec-17-3-0-reconciliation.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/auth': patch ---- - -Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122). - -`SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external -datasource validation panel labels those kinds through a map that is TOTAL over -the union on purpose — so an upstream addition fails the build rather than -rendering a blank cell. That mechanism fired: the package did not compile against -17.3.0 until the kind was labelled. It now reads "Not checked — remote -unreachable", following the spec's own ruling that this kind asserts nothing -about the remote schema and must never be surfaced as "schema changed": it means -introspection could not complete, which is often transient, and labelling it like -a mismatch would tell an operator to repair a schema nobody has read. - -Two internal flow-inspector types were renamed (`FlowNodeLike` → -`InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own -`FlowNodeLike`; neither name is in this package's published entry, so no consumer -import changes. - -`@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with -the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The -`previewMode` prop itself is host-supplied, unchanged, and unaffected. diff --git a/.changeset/7125-dashboard-empty-state-keys-retired.md b/.changeset/7125-dashboard-empty-state-keys-retired.md deleted file mode 100644 index a71b407ce7..0000000000 --- a/.changeset/7125-dashboard-empty-state-keys-retired.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`dashboard.noRows` and `dashboard.noDataAvailable` are retired — two rows removed from -each of the ten locale packs, 20 entries, zero readers (objectui#7125). - -objectui#7063 routed the three dashboard empty-state renders (`DatasetWidget`, -`ObjectDataTable`, `PivotTable`) through one shared `WidgetEmptyState`, which resolves its -own copy from the `dashboard.empty.*` family. The two keys the old per-widget placeholders -used outlived their call sites in `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. - -Removed under objectui#4658's evidence standard, re-measured on this branch rather than -inherited from the card: zero `t()`/`tt()` call sites for either fully qualified key, no -dynamic `dashboard.` head a substitution could resolve onto them, and every surviving -textual occurrence in `packages/` is a comment recording the consolidation. `pnpm -check:i18n-keys` stays green across the deletion with the `en` pack at 2,962 keys (2,964 -before) and every in-scope call-site key still resolving. - -Not touched: `table.noRows` (`'No rows to display'`) and `engine.form.noRows` -(`packages/app-shell/src/views/metadata-admin/i18n.ts`, read at `widgets.tsx`) — two -different, same-named keys in different namespaces. Nor the comments in -`WidgetEmptyState.tsx`, `DatasetWidget.tsx`, `ObjectDataTable.tsx` and `PivotTable.tsx` -that record WHY three widgets with three strings became one shared empty state; the packs' -own comment keeps that rationale and now names the retirement instead of a row that is -gone. - -`packages/i18n/src/__tests__/dashboard-emptyState-keys-retired-7125.test.ts` pins the removal -by name, following the five prior retirements (objectui#4145, objectui#4392, objectui#4730, -objectui#5504, objectui#6310). Every i18n gate here runs call site → key, so none of them can -see a dead key come BACK into the packs: the reverse sweep that found these is report-only by -design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and -`check:i18n-drift` reported the deletion as `2 removed — those are all-locales-key-parity's`. diff --git a/.changeset/7126-inline-widgets-read-error.md b/.changeset/7126-inline-widgets-read-error.md deleted file mode 100644 index 2229bcffde..0000000000 --- a/.changeset/7126-inline-widgets-read-error.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -The last five inline edit widgets read the delivered `error` slot, so a failed -required `text` / `boolean` / `date` / `datetime` / `time` control finally -reports `aria-invalid` (objectui#7126). - -objectui#7008 made `FieldEditWidget` DELIVER the declared `error` key to -whichever widget it resolves. Of the 27 distinct components in `EDIT_WIDGETS`, -21 read it; five did not — `TextField`, `BooleanField` (serving both `boolean` -and `toggle`), `DateField`, `DateTimeField` and `TimeField` — so for their field -types the delivery was inert and the attribute was still never set. - -`text` being in that set is what made this a live defect rather than tidiness. -It is the most common field type in any object, so it is the likeliest thing a -kanban column makes required: `RequiredFieldsDialog` computed the failure, drew -the red "Required" hint, handed the state to the control, and the control said -nothing to assistive tech. The grid's inline cell editor and the detail page's -inline edit (`InlineFieldInput`) compose the same seam. - -Each of the five now computes `aria-invalid={!!error}` **after** its DOM -pass-through spread — one existing idiom, the objectui#3222 discipline the other -21 already share, so a valid field says an explicit `"false"` rather than staying -mute. Two judgements worth stating: - -- **The FORM path was never broken and is unchanged.** `` is a - Radix `Slot` whose `aria-invalid` reached each control through the props - spread; the form also produces `error`, so the widget's own computation now - agrees with the value it replaces. The gap was every host WITHOUT that Slot. -- **`BooleanField` is the one composite here, and the mark goes on the - control.** Its Radix `Checkbox` / `Switch` renders a real - `button[role=checkbox]` / `button[role=switch]`; the wrapping flex `div` is - deliberately not the target, because a wrapper mark satisfies a subtree query - while telling a screen-reader user nothing (objectui#5223). The three - date/time widgets each render one native input, so the browser's picker raises - no second-element question. - -This buys the MARKING only. The objectui#3222 slot drives `aria-invalid` and -renders no text: the visible message stays with the host, and nothing that was -invisible becomes visible. diff --git a/.changeset/7127-record-details-refuses-not-strips.md b/.changeset/7127-record-details-refuses-not-strips.md deleted file mode 100644 index fb463e50c6..0000000000 --- a/.changeset/7127-record-details-refuses-not-strips.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Comment-only. The `record:details` and `record:highlights` registration comments in -`@object-ui/plugin-detail` stated that the spec STRIPS an undeclared key on parse. -Measured against the installed `@objectstack/spec` 17.2.0, it REFUSES it — `safeParse` -returns `success: false` with `unrecognized_keys` naming the key — so both comments now -state the mechanism they actually rely on. No published behaviour changes: the decision -both comments record (do not publish those keys as authorable inputs) is unchanged, and -no declaration, type or renderer was touched. diff --git a/.changeset/7129-retire-detailviewsection-hideempty.md b/.changeset/7129-retire-detailviewsection-hideempty.md deleted file mode 100644 index 0273356e77..0000000000 --- a/.changeset/7129-retire-detailviewsection-hideempty.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-detail': minor ---- - -**Retired: `DetailViewSection.hideEmpty`.** The `record:details` section key is -gone from `@object-ui/types` and `RecordDetailsRenderer` no longer reads it. -Emptiness on a detail section is now decided entirely by `DetailSection`'s -auto-hide heuristic — hide empty rows only while the section still has at least -one filled row, never on an all-empty section — with the reader's -"Show N empty fields" toggle as the escape hatch. - -**Minor, not major, and deliberately so.** The key was never authorable on any -validated page: `@objectstack/spec` `RecordDetailsProps` REFUSES it, returning -`unrecognized_keys: ['hideEmpty']` on the `sections[]` element (measured on the -installed 17.2.0, against a `columns: 2` control that parses and whose value -survives). So a spec-compliant document could not carry the key, and a document -that carried it anyway failed to parse before it ever reached the renderer. -What this release removes is a *declaration* that invited authors — and code -generators reading the published `.d.ts` — to write a key the platform refuses. -That narrows a published type surface, which is what makes it a minor rather -than a patch; it retires no capability anyone could exercise. - -One key had four contracts and three answers: `@object-ui/types` declared it, -`RecordDetailsRenderer` honoured it, the `DetailViewSectionSchema` zod mirror -omitted it, and the spec refused it. The maintainer converged the four on the -spec's answer (2026-09-01): the spec keeps refusing, the mirror stays absent, -and the declaration and the read are retired. All four are now pinned together -in `record-details.hideEmptyRetired-7129.test.tsx`. - -Going with it is the paradox the key carried: `DetailSection` tested -`!section.hideEmpty`, so an authored `hideEmpty: false` was indistinguishable -from an unauthored section and overrode nothing. There is no longer a lever to -misread. - -**Supersedes one paragraph of the `record:details` empty-section changeset in -this same release.** Its closing "What does not change: an authored `hideEmpty` -keeps its exact former meaning" no longer holds — an authored `hideEmpty` of -either polarity is now inert, and the release notes should read that way. -Everything else in it stands: the unauthored default is unchanged, and so is -the label-graveyard guard. - -**Migration:** delete `hideEmpty` from any `record:details` section you author. -A section that used `hideEmpty: true` to hide an all-empty block will now show -that block's skeleton — headings, field labels and one empty-value placeholder -each. That is the platform's answer for a sparse record, and it is a UI -decision, not something metadata should have to make. - -**Not affected**, despite the shared name: `record:reference_rail`'s own -`hideEmpty` prop, which is a different surface and still live; and the -`detail.hideEmptyFields` i18n label behind the toggle. diff --git a/.changeset/7132-empty-state-role-default.md b/.changeset/7132-empty-state-role-default.md deleted file mode 100644 index 4b6cce93ac..0000000000 --- a/.changeset/7132-empty-state-role-default.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/plugin-list': patch ---- - -`DataEmptyState` now declares `role="status"` by default, so an empty result is -distinguishable from a failed one on every surface that renders it -(objectui#7132). - -This is the convergence half of the two rulings that landed as objectui#7063 and -objectui#7064, both resting on objectstack#13848: uniform behaviour belongs to -the platform, and per-surface compensation is the per-app tax being ruled -against. Those two fixed their own surfaces deliberately and locally; this card -measured whether the shared primitive should carry the property. It did not. - -**Measured, not assumed.** All the surfaces were rendered and their empty boxes -read directly: - -| surface | `role` before | -|---|---| -| `DataEmptyState` bare default | *none* | -| `plugin-list` empty list | *none* | -| `plugin-list` load-error panel | *none* | -| `plugin-detail` activity timelines | *none* | -| `ui:empty` schema renderer | *none* | -| `plugin-dashboard` `WidgetEmptyState` (#7063) | `status`, typed at the call site | -| `plugin-kanban` empty board | `status`, typed at the call site | - -The sibling states in the same file had always declared themselves — -`DataLoadingState` is `role="status"`, `DataErrorState` is `role="alert"` — and -the empty state alone declared nothing. So the surfaces were not legitimately -differing: the ones that wanted the property had each hand-typed the same line, -and the ones that had not yet done so were silently missing it. That is one -platform default, copied by hand, at package level. - -**It is a default, not a fixed attribute** — `role` is spread from props, so a -call site keeps the last word. That is what makes this inert for the two ruled -surfaces: both already pass `role="status"` explicitly and receive the identical -attribute with or without it. Neither surface's behaviour changes. - -**One real defect fell out of the measurement.** `plugin-list` renders its load -FAILURE through `DataEmptyState`, borrowing it for layout — so a 403 saying "You -don't have access" and a young object saying "Nothing here yet" were the same -node shape, with no role on either. That panel now declares `role="alert"`, -which both fixes the pre-existing indistinguishability and stops the new default -from announcing an outage as a routine status. - -Metric/KPI widgets are untouched: their carve-out (`rows.length === 0 && -!isMetric`) gates whether an empty state is rendered *at all*, upstream of this -component, so a KPI still reads `0` rather than "no data". diff --git a/.changeset/7137-selection-expires-with-leaf.md b/.changeset/7137-selection-expires-with-leaf.md deleted file mode 100644 index d05e6f0f57..0000000000 --- a/.changeset/7137-selection-expires-with-leaf.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio Interfaces pillar: a canvas block selection no longer outlives the leaf -it was made on (objectui#7137). - -`InterfacesPillar`'s only clear of `selection` sat inside the draft-load effect, -*after* its `if (!current || !isEditable) … return` guard, so it never ran on the -early-return path. Since `isEditable = !!Preview && !StudioCanvas` is a -conjunction, that is two populations of leaf: a studio-canvas leaf (`object`), -and a leaf whose own type has no registered designer. Walking to either from a -leaf with a block selected carried the selection across, still describing a block -on the previous leaf's canvas. - -Two symptoms, both measured before and after: - -- the scoped inspector opened for a foreign block — recorded three renders as - `page:home_page:block:blk_1`, with `blk_1` a dashboard block — and the header - offered to clear a selection belonging to another leaf; -- in the folded (chat-dock) layout, `hasInspectorTarget` stayed true across the - leaf change, so `nextCenterTab` saw no edge and stranded the author on the - Properties tab of a leaf with no properties to show. - -The selection is now stamped with its leaf and read back through that key — the -same "expires by construction" shape `blockingReport` already uses against -`inspectorKey` in this component. It goes null in the *same* render as the leaf -change rather than one committed render later, and there is no imperative clear -left for a future guard to strand. Within a leaf nothing changes: the Design/Run -round trip still keeps its selection, and a same-leaf reload (`publishNonce`) -still clears it. - -objectui#7121's gating of the rail and the Design/Run switch is untouched, and -its discriminator remains `StudioCanvas` — not `isEditable`. diff --git a/.changeset/7140-sankey-no-positive-flow.md b/.changeset/7140-sankey-no-positive-flow.md deleted file mode 100644 index ff8b43ca82..0000000000 --- a/.changeset/7140-sankey-no-positive-flow.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-charts': minor ---- - -A sankey with no positive flow says so, instead of rendering an empty div -(objectui#7140). - -`AdvancedChartImpl`'s sankey arm keeps only strictly positive measures, so a -chart handed **real rows** whose measure is all `0`, all `null`, all negative, -or unparseable built no links and returned a bare `
`. Measured in Chromium -against a populated control: the control drew 1 `` / 7 `` / -26 descendants; each of those four tiles rendered `descendantCount: 1`, -`svgCount: 0`, `textContent: ''`, and their screenshots hashed identical to one -another. No marks, no text, no `role` — a tile indistinguishable from a widget -that had crashed, which is the one distinction the file's other refusals exist -to make. - -It now renders through the `ChartRefusal` shell those refusals already use — -same box, same `role="status"`, and a new `data-chart-error="no-positive-flow"` -— reading *"This chart has no flow to draw: no row's `` is above -zero."* - -Two boundaries are deliberate and pinned: - -- **No rows at all is untouched.** That is the empty-result question, answered - upstream in `ObjectChart` where the query outcome is known; a sentence about - what the rows contain would be false about a dataset with no rows in it. -- **One positive row among zeros still draws.** The refusal fires on an empty - link set, never on a thin one. - -One code and one sentence for three causes (a genuinely all-zero flow, values a -flow cannot represent because they are negative, and measures `Number(…) || 0` -folds to zero): naming any single cause would be false for the other two, so -the copy names the predicate the filter actually applies, which is true for all -three. No recovery is promised. Every other chart family is byte-identical — -eight of the twelve tiles in the browser sweep hashed unchanged. diff --git a/.changeset/7142-activity-timeline-empty-i18n.md b/.changeset/7142-activity-timeline-empty-i18n.md deleted file mode 100644 index 268d7ec355..0000000000 --- a/.changeset/7142-activity-timeline-empty-i18n.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`ActivityTimeline`'s empty state speaks the session locale (objectui#7142). - -The title was a raw English JSX literal — `title="No activity recorded"`, not a -`t()` call and not an inline `defaultValue` — so it never reached the pack -system and stayed English in all ten locales. Measured before the fix by -rendering `activities={[]}` under a zh `I18nProvider`: the card read -`"Activity(0)No activity recorded"`, while its sibling `RecordActivityTimeline` -rendered `"活动(0)全部动态暂无活动记录"` from the same packs. - -The call site now reads `detail.noActivity`, the key the sibling already uses. -Reusing it rather than minting a second key is a measured decision: the `en` -pack value for that key is `'No activity recorded'`, byte-identical to the -literal it replaces, so both surfaces were already saying the same words in -English and a new key would have forked one sentence across ten packs for no -copy difference. No pack was edited — the key is already translated in all ten, -verified by reading `detail.noActivity` out of each pack object. - -Both routes to the box are covered: an empty `activities` array, and a -populated timeline filtered down to a type with no entries. diff --git a/.changeset/7143-data-error-state-migration.md b/.changeset/7143-data-error-state-migration.md deleted file mode 100644 index 1d490b3e2d..0000000000 --- a/.changeset/7143-data-error-state-migration.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/plugin-list': minor ---- - -`DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s -load-failure panel is now rendered by the error state instead of the empty state -(objectui#7143; maintainer ruling 2026-09-01, director decision batch #27). - -`ListView` rendered its load FAILURE through `DataEmptyState` — the component named for -the *empty* case — passing it a destructive icon, error copy and a retry action, while -`DataErrorState`, in the same file and with the same layout, had no consumer anywhere in -the repo. objectui#7132 closed the accessibility half of that collision (the panel now -declares `role="alert"` over the empty state's `role="status"` default) and deliberately -left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a -props-surface question plus a visual change rather than a rename. - -**`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored -from `DataEmptyState` in the same file rather than spelled a second way: - -- `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle` - glyph the component has always drawn. -- `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely. -- `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than - merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??` - against its own default and this does the same, against - `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive - square `DataErrorState` already drew. - -Same names, same types, same default semantics as the empty state's; nothing existing on -`DataErrorState` changed, and a call site that passes none of the three renders exactly -what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the -ruling pins three props, and this component's retry affordance is already spelled -`onRetry` / `retryLabel` (plus `children` for a call site that needs its own control). - -One non-prop addition rides along, called out rather than folded in: the icon wrapper now -carries `data-slot="data-error-state-icon"`, mirroring the empty state's -`data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no -name — untestable and unstylable — and migrating a call site off `DataEmptyState` would -DROP that identifier rather than rename it. - -**`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call -site passes the same custom icon through the new `icon` prop, the same -`iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the -error state's spelling of `description`); its retry `` element cannot express, so the two identifiers lose their monospace -styling. That is the deliberate cost of making those sentences translatable. - -Evidence: an `en`-only assertion cannot discriminate here, because each key's `en` -value is byte-identical to the literal it replaced. The suite asserts in **zh and -ar**, and the provider-less path separately, in its own file (`createI18n` installs -itself as react-i18next's module-level global, so a provider-less render in a file -that has already mounted a provider silently reads that pack instead of the defaults -map). No inline `defaultValue` anywhere (objectui#3517). - -Two consequences of the sweep, both landed here rather than left for CI to find: - -`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its -`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph -reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so -the frozen surface made that read `undefined` and the file died during COLLECTION — the -objectui#6849 shape, which does not look like a test failure. It now spreads -`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of -the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed -this to be the only file whose graph reaches the package. - -The ten pack blocks are locale DATA, and locale data lands in the console's eager -`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's -ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh -measurement (502,405 to 514,863). Attributed by three console builds: the merge parent -reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and -this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing -else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression -the gate must catch) rather than widened; most of the overage was pre-existing drift, with -the merge parent already at 510,192 of the 512,000 allowed. diff --git a/.changeset/7175-downstream-consumer-census.md b/.changeset/7175-downstream-consumer-census.md deleted file mode 100644 index 329ae19621..0000000000 --- a/.changeset/7175-downstream-consumer-census.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Docs-only: records the downstream-consumer census for -`@object-ui/plugin-detail`'s `PointInTimeRestore` as -`docs/audits/2026-09-plugin-detail-downstream-consumer-census.md` (objectui#7175). - -objectui#7163 measured that the component has zero *in-repo* consumers. That is not the -same statement as zero consumers, because it is barrel-exported public API, so this audit -reads the downstream populations instead: `hotcrm` measured at `a6be39a3d` (zero on every -spelling, against a positive control of 104 `plugin-detail` node references in the same -repo on the same instrument), and `cloud` reported as **NOT MEASURED** — refused on three -independent channels, two of them beside a live control. - -No source, behaviour, or public-surface change; the audit records evidence so the -ADR-0049 enforce-or-remove question can be decided later. It deliberately retires nothing. diff --git a/.changeset/7177-studio-switcher-server-writable-verdict.md b/.changeset/7177-studio-switcher-server-writable-verdict.md deleted file mode 100644 index 61bc87a692..0000000000 --- a/.changeset/7177-studio-switcher-server-writable-verdict.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's package switcher reads the server's `writable` verdict instead of guessing -from `manifest.scope` (objectui#7177, ADR-0130 Consequences row 6, server half in -objectstack#14375). - -`GET /api/v1/packages` now stamps every row with `writable: boolean`, computed by -`isWritablePackage` (ADR-0070 D2) — the same predicate the server's authoring and -lifecycle gates enforce. `parsePackages` uses it when the row carries one, so the -lock badge and the gate cannot disagree. - -The old `scope !== 'project'` expression stays as the fallback for servers that -predate the field, and its output is pinned byte-identical. It is wrong for exactly -one row, which is why the verdict had to move server-side: a `type: module` -sub-package of a multi-package artifact (ADR-0130 D4) is served with no `scope` key -at all — the schema default is applied at parse time, while the artifact load path -hands the raw manifest body to `registerApp`. The heuristic reads that as a writable -database base, while the server refuses every write to it. Nothing in the raw row -separates it from a scope-less Studio-created base, which really is writable — only -the server's `engine.manifests` does, so a client-side "missing scope means -read-only" rule would have flipped every Studio base read-only instead. - -Kernel packages (`scope: system` / `cloud`) stay hidden whatever verdict they carry: -that filter is about visibility, not writability. diff --git a/.changeset/7178-format-measure-date.md b/.changeset/7178-format-measure-date.md deleted file mode 100644 index e5426cc40a..0000000000 --- a/.changeset/7178-format-measure-date.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch ---- - -Render a dataset measure over a date field as a date (objectui#7178, maintainer -ruling 2026-09-02, director summon #8 — option A). - -`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`, -placed **before** `format` was ever read. So a `min` / `max` measure over a date -or datetime field printed its stored value verbatim — a 24-character ISO string -in the KPI tile's `text-2xl font-semibold`, wrapping to two lines — and the -`format` that `DatasetMeasureSchema` accepts was unreachable for those values. -A date-shaped value now routes to the date display path before that -short-circuit, so all four dataset-bound surfaces are served at once: the metric -tile, chart values, dataset table cells, and the metadata-admin dataset preview. - -`min` / `max` over a date stays a legal measure; nothing in `@objectstack/spec` -narrows. `PivotTable` takes a `number` outright and is unchanged. - -**No second date formatter was written.** `formatDate`, `formatDateTime`, -`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`' -barrel down into `@object-ui/core` (`utils/date-display.ts`), which is the same -remedy objectui#4576 applied to `formatDisplayNumber` and for the same reason: -`core` is the React-free engine and could not import from a React package, so -the alternative was a parallel date convention in `dataset-format.ts` — exactly -the drift that once had a list cell rendering `1.234,5 %` beside a dashboard -measure's `1.234,5%`. `@object-ui/fields` re-exports all four names unchanged, -so no consumer's import path or behaviour changes, and a reference-identity test -pins that the cell renderer and the measure formatter call the same function. - -**What `format` can say for a date measure, measured rather than assumed.** The -shared date path takes a named STYLE, not a date pattern: `'short'` and -`'relative'` are honoured — the same words `DateCellRenderer` honours from -`field.format` — while a pattern such as `'YYYY-MM-DD'` renders the locale -default. That limit is unchanged by this release (`plugin-dashboard`'s -`recordFields` already routed a date-shaped `format` into the same style slot) -and is now pinned by a test instead of being silent. - -**Numeric measures are byte-identical.** 33,696 argument forms -(value × format × currency × percentScale × locale) were compared against a -verbatim copy of the pre-fix function: the only values that moved were the four -ISO-shaped, parseable ones. Numbers, numeric strings (`'1751612400000'`, -`'2026'`), the nullish em dash, arbitrary prose and non-strings all render -exactly as before. diff --git a/.changeset/7179-grid-grouping-projection.md b/.changeset/7179-grid-grouping-projection.md deleted file mode 100644 index b59fb5a296..0000000000 --- a/.changeset/7179-grid-grouping-projection.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-list': patch ---- - -Fix: a grid grouped by a field it does not also show as a column no longer collapses -every row into one `(empty)` group (objectui#7179). - -`$select` was built from the view's `columns` and nothing else, so a view declaring -`grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns -never asked the server for that field. It was `undefined` on every row by the time -grouping ran, and the grouping label builder — correctly, for a genuinely empty value — -answered `(empty)` for all of them. The result was one collapsible group holding every -record, with no error, no warning and no empty state: a grid that looked like it grouped -and did not, reading as "these records have no value for this field". - -The grouping fields are now unioned into the projection, at both places it is built — -`ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the -rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that -fetches a bare foreign key without populating it buckets by raw id instead of by name, -which is a different wrong answer rather than a fix. - -Authors do not need to mirror a grouping field in `columns` any more. That was never -required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of -it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their -`groupByField` with no column needed. Refusing the configuration at author time was -considered and rejected: it would make the grid the odd one out and reject working -intent that the schema explicitly allows. - -The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]` -entry carries a bare string that has never been through column validation, and some -backends answer an unknown `$select` key with an empty result set rather than ignoring -it. Unioned unguarded, a grouping field naming something the object does not declare -would have turned this bug into a strictly worse one — no rows at all, equally silently. -Grouping fields are therefore intersected with the object's declared fields and passed -through the same field-level-security gate as columns and predicate operands before they -reach the query. diff --git a/.changeset/7182-declared-action-ids-one-rule.md b/.changeset/7182-declared-action-ids-one-rule.md deleted file mode 100644 index 4ccde267f2..0000000000 --- a/.changeset/7182-declared-action-ids-one-rule.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor -'@object-ui/plugin-detail': minor ---- - -Mixed id/object action arrays are refused; use all ids or all objects -(objectui#7182, maintainer ruling 2026-09-02, option C). - -An `actions` array on `page:header` or `record:quick_actions` (and the bar's -spec-declared `actionNames`) is either **all action ids** or **all inline -`ActionDef` objects**. A mixed `['convert', { … }]` array is now refused on both -surfaces: none of its authored actions is rendered, and the console names the -offending index (`… refused at index 1 — element 1 is an inline action object -but element 0 is an action id …`). Before this change the two renderers -disagreed on exactly that input — `page:header` normalised per element and drew -both halves, `record:quick_actions` switched on the whole array and rendered -nothing for the id — so one authored array meant two things depending on which -surface drew it, and the mixed form is precisely what a half-migrated page under -the objectstack#11592 ids ruling produces. - -**Breaking, deliberately, and narrowing.** `@objectstack/spec` has always -declared `PageHeaderProps.actions` as `z.array(z.string())` — the spec already -refuses an object element at validation, naming its index — and -`RecordQuickActionsProps` declares `actionNames` (ids) only. What narrows is -the renderers' undeclared tolerance: an all-object array still passes through -(transition tolerance for the migration, retired on its own card once the last -inline array is converted), a mixed one no longer does. **Migration:** convert -each array whole — every element an id naming an action declared on the -object — never one element at a time. - -New on `@object-ui/types`, beside `actionRendersAt`: the pure -`resolveDeclaredActionIds(elements, registeredActions)`, with the -`DeclaredActionsResolution` / `DeclaredActionsRefusal` result types (the shape -classifier stays module-internal: called with no registry, the function already -returns the registry-independent verdict a renderer needs before its lookup). Both -renderers call it; the whole-array switch in `record-quick-actions.tsx` and the -per-element normalisation in `containers.tsx` are gone. The rule is closed: a -string is an id, a non-null non-array object is an inline definition, and any -other element (`null`, a number, a nested array) is refused at its index too. -An all-id array resolves by `name` in authored order, first registration -winning on a duplicate name; ids that name nothing are reported back with their -index for the caller to warn about once its lookup has settled. - -**Three further behaviour changes ride on the one rule, all on published -packages:** on `page:header`, a padded id (`' convert '`) no longer resolves — it -was previously trimmed before the lookup, and ids are now compared exactly as -authored; on `page:header`, a blank `''` id is now reported by the unresolved-id -warning instead of being silently skipped; on `record:quick_actions`, an all-id -`actions` array with no object bound now renders nothing (the ordinary empty -placeholder) instead of handing the bare strings to the action engine as action -definitions. diff --git a/.changeset/7183-dist-vitest-project.md b/.changeset/7183-dist-vitest-project.md deleted file mode 100644 index 4d10ef7fd9..0000000000 --- a/.changeset/7183-dist-vitest-project.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Test and CI infrastructure only — nothing this change touches is published, so no package -releases from it. - -Adds a fourth vitest project, `dist`, holding built-artifact pins: tests that import their -package's BUILT bundle instead of its `src`. The root config aliases every workspace package -to `src`, which is correct for the ~2000 tests that want fast source feedback and leaves -"does the shipped bundle still do X" structurally unanswerable. Until now such a test could -not be committed at all — turbo's `test` task is `dependsOn: ["^build"]`, the DEPENDENCIES' -builds and not the package's own, so a `dist`-importing test landed in CI with no `dist` to -import. That reads as NOT MEASURED rather than as a red pin, and the usual repair (delete it, -or let it skip when `dist` is missing) leaves a green suite that measures nothing. - -The one file the changeset-presence gate flags, -`packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx`, is under `src/` but -ships nowhere: the package publishes `files: ["dist", …]`, and its `tsconfig.json` build -excludes `src/__tests__` outright. The rest of the change is `turbo.json`, `vitest.config.mts`, -the root and `@object-ui/components` `package.json` scripts, and one CI step — no runtime -source, no published contract, no behaviour change for any consumer. diff --git a/.changeset/7187-relational-copy-set-cell-scoped.md b/.changeset/7187-relational-copy-set-cell-scoped.md deleted file mode 100644 index 12fb10de22..0000000000 --- a/.changeset/7187-relational-copy-set-cell-scoped.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Scope `ObjectGrid`'s relational copy-set derivation to the consumer the copied bag actually -reaches (objectui#7187). No published behaviour changes: the copy set is byte-identical -before and after — the same seven keys, in the same order. - -The gate in `packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts` -re-extracted a read set from three consumers and collapsed it into a UNION. Only one of the -three, `LookupCellRenderer`, is handed the bag `applyRelationalMeta` writes; the two inline -editor widgets are fed `{ name, ...fieldDef }` straight off the object schema. So membership -in that union meant "some consumer reads this key" and never "this bag is how that consumer -gets it" — and a copy-set entry asserts the second. The union was read as the first claim's -evidence for the second twice, and both verdicts shipped: objectui#6875 put `descriptionField` -and `lookupColumns` on the copy set on it, and objectui#7166 had to take them back off. - -Three changes make that mistake unavailable: - -- The reader axis is now recorded PER CONSUMER on every entry (`readers`), and the gate checks - each declared list against that consumer's own source in both directions — a hand-widened - declaration is an orphan, a new spelling in a chain is unclassified, and both are red. -- The copy set is derived from `CONSUMERS_FED_THIS_BAG` — the cell alone. The one deliberate - exception, the three snake_case spellings kept on an unanswered producer-side question, has - to name itself per key (`copiedWithoutCellReader`), and the gate confines that exit to keys - `FieldSchema` does not declare, so no authorable key can take it. -- The `deferred` verdict is gone. It meant "read only by an editor widget", which is now - measured rather than declared; its seven keys are `spec` (which is what they are) and stay - off the copy set because no consumer fed the bag reads them. - -Consequence worth stating: the hand-written non-membership pin that had been holding -objectui#7166's retirement on its own is now redundant as a guard — re-adding any of the three -retired keys turns the gate red on a derived assertion under every available verdict. The pin -is kept so a regression is reported by name, not because it is the only hold. diff --git a/.changeset/7188-components-data-table-pending-row.md b/.changeset/7188-components-data-table-pending-row.md deleted file mode 100644 index 51b42be54d..0000000000 --- a/.changeset/7188-components-data-table-pending-row.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/components': minor ---- - -feat(data-table): pass `pendingRow` to the host cell editor — the row with its staged edits merged over it (#7188) - -`data-table` already computed each row's `pendingChanges` entry in the row loop that -renders the editor; it now hands the injected editor that row merged with those staged -values as `pendingRow`, next to the persisted `row`. Nothing about `row`, `value`, -`stage`, `commit` or `cancel` changed. A host editor that scopes itself by a sibling -field can read `pendingRow` and follow an edit the user has made but not yet saved. diff --git a/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md b/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md deleted file mode 100644 index 6390e33e1b..0000000000 --- a/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -Fix: a `dependsOn` lookup edited inline follows a parent edited in the same row **before** -it is saved (#7188, finishing #7165). - -#7165 shipped an explicitly-labelled interim: the grid's inline editor scoped a -`dependsOn` picker by `ctx.row`, the **saved** record. Edit the parent cell, do not save, -open the child — and the child still listed candidates for the parent's persisted value -(or stayed gated if that value was empty). The grid now scopes by `ctx.pendingRow ?? ctx.row`, -where `pendingRow` is the row merged with its staged, unsaved edits, so picking a parent -re-scopes the child immediately — the form's live-record semantics from #2216. The interim -marker that named #7188 is gone with the interim. diff --git a/.changeset/7188-types-render-cell-editor-pending-row.md b/.changeset/7188-types-render-cell-editor-pending-row.md deleted file mode 100644 index af3a4be0b8..0000000000 --- a/.changeset/7188-types-render-cell-editor-pending-row.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/types': minor ---- - -feat(types): `renderCellEditor`'s context gains `pendingRow` — the persisted row merged with its staged, unsaved edits (#7188) - -The inline cell editor's context on `DataTableSchema` (declared by #6882) carried the row -once, as `row` — the **persisted** record. A widget that scopes itself by a sibling field -(a `dependsOn` lookup) had no way to see a parent that was edited in the same row but not -yet saved, so it kept listing candidates for the old parent. - -The context now carries the row twice, deliberately: - -- `row` — unchanged: the persisted record, what the data source last returned. -- `pendingRow` — **new**: `row` shallow-merged with the row's staged, unsaved edits. The - same object as `row` when nothing is staged. - -`row` was **not** redefined to mean the merged record — that would silently change an -already-published member, and a host that needs the persisted value would have lost its -only source. Both are addressable. The zod mirror's `renderCellEditor` is `z.function()` -and encodes no parameter shape; its description records the delta and names the member -on `DataTableSchema` as the authority. The #6882 exact-shape pin was extended (not -weakened) in the same change, with a control proving the pin can tell the seventh member's -presence from its absence. diff --git a/.changeset/7189-grouped-grid-partial-disclosure.md b/.changeset/7189-grouped-grid-partial-disclosure.md deleted file mode 100644 index 16e6a9461b..0000000000 --- a/.changeset/7189-grouped-grid-partial-disclosure.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/plugin-grid': minor -'@object-ui/i18n': minor ---- - -A grouped grid now says, where the group counts are, that it grouped a **page** -(objectui#7189). - -`useGroupedData` buckets the rows the browser already holds and computes every -per-group aggregate from that same array, so both the set of groups and every -number in a group header are properties of the fetched page, not of the query. -That is a correct implementation of client-side grouping and is **unchanged** -here — what was missing is any statement that client-side grouping is what you -are looking at. Measured on a 186-record store distributed 86 / 61 / 31 / 7 / 1 -across five business units with a 100-row page: with contiguous rows the grid -rendered **two** group headers (`86`, `14`) and three units were absent from -the screen entirely; with interleaved rows all five resolved but every count -was a page slice (31 / 31 / 30 / 1 / 7). Nothing on screen said either. - -The paging footer is not a statement about what was grouped, and it -demonstrably did not prevent the wrong reading — a wrong number invites a -second look, an absent row invites none. So the disclosure goes where the -authoritative-looking number is: - -- a short `Partial` marker beside **every** group count, at every nesting - depth, carrying the full sentence as its `title` and its accessible name; -- one line directly above the group list, inside the grouped region rather - than in the footer: *"Grouped over the first 100 of 186 records. Group counts - are page-scoped, and a group whose records all fall beyond the loaded rows is - missing here."* - -The trigger is the strongest thing the component can actually know, and the -wording never outruns it. With a real match total to compare against -(`resolvedTotalMatching` — the one derived value the pager and both bulk-bar -sites already read, reached either from the grid's own fetch or from a host's -`rowCount`) it states the fact with both numbers. With no total but a window -that came back full it may only say *"more may match"* — the same inference -`plugin-list`'s own footer draws when no total is known. Rows handed in inline -are not a page and are never marked, and **a grouped grid whose result set fits -in one page shows nothing at all**: the marker is conditional, which is what -makes it worth reading. - -Server-side grouping — the durable fix — is deliberately NOT part of this. It -is an API-surface decision still open on objectui#5560, and nothing here builds -toward it or changes the fetch. - -`@object-ui/i18n` carries the three new `grid.grouping.*` strings across all -ten locale packs; `GroupRow` gains two optional props (`partialLabel`, -`partialTitle`) and is otherwise unchanged. No metadata schema key was added: -the condition is derived from data the grid already has. diff --git a/.changeset/7190-detail-dependson-lookup-probe.md b/.changeset/7190-detail-dependson-lookup-probe.md deleted file mode 100644 index a7995766f5..0000000000 --- a/.changeset/7190-detail-dependson-lookup-probe.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Measurement-only change in `@object-ui/app-shell`: objectui#7190 asked whether a -`dependsOn` lookup gates on the DETAIL page, and deliberately left it unanswered -— a detail page renders ONE record, which is exactly the "record scope" -`LookupField`'s `ctx.data` channel exists to carry, so a host that populates it -would make the cascade resolve and there would be no defect. Measured in the -real host, it gates. - -New `views/RecordDetailView.lookupDependsOn-7190.test.tsx` mounts the app-shell -record page, loads a record carrying the parent value, enters inline edit by -double-click, and reads the picker's own trigger. The declared lookup comes back -`lookup-trigger-gated`, disabled, "Select region first" — while the `region` -field it names is on screen in the same edit session carrying `emea`. The -control lookup beside it (same reference, same record, no `dependsOn`) is -asserted enabled, so the gated reading is a measurement and not a broken -fixture. Both of `InlineFieldInput`'s call sites are covered — the details body -and the highlights strip — and both gate. - -Pinned as the current behaviour, not fixed: which record the host should feed -(the saved record, or the inline session's in-flight staged edits) is a design -question this measurement does not answer, and the sibling fix for the grid is -still in flight. - -No behaviour change. diff --git a/.changeset/7194-scatter-one-measure.md b/.changeset/7194-scatter-one-measure.md deleted file mode 100644 index 4f3725cf8b..0000000000 --- a/.changeset/7194-scatter-one-measure.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-charts': patch -'@object-ui/i18n': patch ---- - -A scatter handed more than one series now refuses instead of drawing a false picture. - -Scatter binds one measure: `series[0].dataKey` is the y axis, and every series was -handed the same rows through that one axis. A second series therefore added a -colour and a legend entry and nothing else — measured, two series over two rows -painted four symbols at two positions, each drawn twice, and the second measure's -values appeared nowhere on the plot. The data was valid and the picture was -confidently wrong, which no existing refusal could see. - -A `chartType: 'scatter'` with two or more `series` now renders the renderer's -refusal shell under `data-chart-error="scatter-multi-series"`, stating that a -scatter plots one measure, naming the fix (keep exactly one series) and listing -the series keys it was handed. A single-series scatter is unchanged. - -This refusal counts authored `series` only. `compareTo` on scatter is out of -its scope: objectui#7402 ruled (b) that scatter joins pie / donut / funnel in -excluding `compareTo` — `supportsCompareTo` and the dashboard widget path stop -synthesising a comparison series for it, so no `…__comparison` overlay is ever -built for a scatter and this guard is never reached by a compare-to document. -That exclusion ships as a separate change; until it lands, a `compareTo` -document still reaches the renderer as two series and refuses here today. - -No multi-measure projection is built (maintainer ruling, 2026-09-02): nothing -in-repo authors a two-series scatter, so that capability waits for a real caller. -The refusal copy is `chart.scatterOneMeasure` in all ten locale packs. diff --git a/.changeset/7196-plugin-grid-held-key-census.md b/.changeset/7196-plugin-grid-held-key-census.md deleted file mode 100644 index 9cb04a82ba..0000000000 --- a/.changeset/7196-plugin-grid-held-key-census.md +++ /dev/null @@ -1,47 +0,0 @@ ---- ---- - -Re-derives the held-key censuses in `packages/plugin-grid/src/ObjectGrid.tsx` and corrects -the seven claims that no longer match the tree. Comments and one test docblock only — no -runtime code, no exported type, no declared member moves, so nothing publishes. - -The card (objectui#7196) was filed on ONE measured entry: the schema-level census still -listed `renderCellEditor` as an undeclared-but-live HELD key and called the `packages/types` -ruling on it "pending", when objectui#6882 had declared it on 2026-08-30. The card -deliberately did not claim the other entries were correct, only that nobody had checked, so -all sixteen claims across both censuses in the file were re-derived against `origin/main` -rather than read. Seven were defective: - -Stale — correct when written, drifted since: - -- `renderCellEditor` — declared by objectui#6882 on three surfaces (the member, the Zod - mirror, an `Equal` exact-shape pin). The `(schema as any)` cast the census cites went - with the declaration. -- `cellClassName` — declared by the SAME ruling. The card did not name this one; the - re-derivation did. The census called it a hold with a "pending ruling" too. -- "leaves exactly TWO undeclared keys" — diffing the 46 flat-literal keys plus the 8 - group-literal keys against `DataTableSchema`'s declared members now leaves ZERO. -- the consumer read set listed fourteen `col.` reads in `data-table.tsx` including - `name`; objectui#6963 retired that alias on 2026-08-31, so it is thirteen. - -Wrong when written, not drift: - -- the schema-level `cellClassName` was described as folded "into every body cell's - `className`". It reaches exactly three UTILITY cells (selection, row-number, - row-actions) and never a data cell, which folds the per-column twin. The failure mode - the census names is wrong in the same way. objectui#6882's declaration carries the - correct version upstream; this makes the local copy agree with it. -- "the 7-literal union `TableColumn` declares" — it has been eight since objectui#6370 - (2026-08-25), a day before the docblock was written; that commit's own subject says - "8-literal". -- the downstream read list omitted four keys the chrome passes read to re-express - (`className`, `cellClassName`, `sortable`, `cell`). All three passes predate the list. - -Nine claims re-derived clean and are recorded as such, including every column-level -verdict (`headerIcon`, `pinned`, `wrap`, `options`, `essential`, `name`) and the -`FieldType` count of 49. - -The holds type is left in place, with its docblock rewritten to record that it is now -redundant rather than load-bearing and what was measured toward removing it. Deleting a -member of an exported type is a different kind of change and gets its own card, the way -objectui#6615 was followed by objectui#6424 for `headerIcon`. diff --git a/.changeset/7199-listview-description-relay.md b/.changeset/7199-listview-description-relay.md deleted file mode 100644 index 2b080749cc..0000000000 --- a/.changeset/7199-listview-description-relay.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-list': patch ---- - -fix(app-shell,plugin-list): a list view's own `description` now reaches the screen - -A `description` authored on a per-list-view entry (`listViews..description`) -was validated, built and served correctly, then silently never rendered. Two -independent cuts, both fixed here: - -- **app-shell** — `ObjectView`'s `renderListView` relay copied ~46 keys off the - active view onto the schema it hands `ListView` (`label`, `sort`, `filter`, - `hiddenFields`, `inlineEdit`, `color`, `allowExport`, …) but had no rung for - `description`, so the renderer could only ever see the object-level list's - description and a per-view one was unreachable. It is relayed now, with the - same two-rung shape as `label`. This is *not* the object's own - `objectDef.description`, which stays the page header's subtitle. -- **plugin-list** — `ListView` rendered `typeof description === 'string' ? … : ''`, - a type test rather than a resolution. `ListViewSchema.description` is - `I18nLabel`, so an inline locale map (`{ en, 'zh-CN' }`) — metadata the spec - entitles an author to write — rendered a blank strip in every locale. It now - resolves through the same shared helper the sibling `label` uses, and the - visibility guard reads the resolved text, so a map with no usable entry drops - the strip instead of reserving empty space for it. - -`appearance.showDescription: false` still suppresses the description in both arms. diff --git a/.changeset/7200-object-form-section-style-keys-undeclared.md b/.changeset/7200-object-form-section-style-keys-undeclared.md deleted file mode 100644 index 5f3603b124..0000000000 --- a/.changeset/7200-object-form-section-style-keys-undeclared.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ObjectFormSection` no longer declares `className` / `gridClassName` (objectui#7200 — -the declared-but-inert remainder of objectstack#13626). - -**Breaking, deliberately.** A TypeScript literal annotated `ObjectFormSection` (or an -`ObjectFormSchema.sections` entry) that carries `className` or `gridClassName` is now a -compile error at the authoring site. Before this change the two members were declared -with doc comments promising a wrapper / grid class, while — since objectstack#13626 -retired the seven renderer reads (`@object-ui/plugin-form` 2026-09-01) — nothing -delivered it: an author could write either key, have it type-check, and get nothing. - -The authored-metadata type now agrees with `@objectstack/spec`, whose `FormSectionSchema` -is a strict object declaring neither key, and with the ruling's rationale (maintainer -2026-09-01, verbatim): "retire the reads … Declaring the keys was weighed and not adopted: -it would formally invite free Tailwind strings into authored metadata, the exact class -the boundary exists to keep out." A `?: never` tombstone was not used: `ObjectFormSection` -has no zod mirror (`ObjectFormSchema` in `zod/objectql.zod.ts` does not declare -`sections`), so there is no parse door to refuse at, and a tombstone is still a -declaration in completion and in the published `.d.ts`. - -**Not changed.** The five per-layout section config types in `@object-ui/plugin-form` -(`ModalFormSectionConfig`, `SplitFormSectionConfig`, TabbedForm's `FormSectionConfig`, -`WizardStepConfig`, `DrawerFormSectionConfig`) keep their `className` / `gridClassName`: -their renderers read them for programmatic React mounts, which the authorable boundary -does not govern. The form ROOT `className` (`ObjectFormSchema.className`) is a different -key on a different node and is unaffected. Runtime behaviour is unchanged — JSON metadata -carrying either key was already ignored. - -**Migration.** Remove the two keys from any `ObjectFormSection` literal; they did nothing. -Style sections through the host application's own CSS or the form ROOT `className`. -Section *layout* stays authorable through `columns`. diff --git a/.changeset/7201-schema-slot-census-gate.md b/.changeset/7201-schema-slot-census-gate.md deleted file mode 100644 index 947874b2a4..0000000000 --- a/.changeset/7201-schema-slot-census-gate.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Gate the `plugin-grid` schema-level held-key census on its ENTRY CONDITION -(objectui#7201). The census's only pin asserted that the seam ACCEPTS -`renderCellEditor` and `cellClassName` — equally true while the keys are held -locally and once they arrive through `DataTableSchema` — so it stayed green -across objectui#6882, the ruling that expired both holds. The suite now also -carries the column-level twin's shape (`columnHoldsExpiry-6424.test.ts`): -a compile-time verdict per member of `ObjectGridDataTableSchemaHolds` recording -whether `DataTableSchema` declares that key, plus a pin on the member set so the -gate is per HOLD rather than per key somebody remembered. - -No behaviour change and no published API change: compile-time assertions in -`packages/plugin-grid/src/__tests__/`, plus the one census paragraph in -`ObjectGrid.tsx` that this change falsifies — it said nothing above it was -mechanically checked. diff --git a/.changeset/7203-gantt-toolbar-period-label.md b/.changeset/7203-gantt-toolbar-period-label.md deleted file mode 100644 index 1d6447a4f2..0000000000 --- a/.changeset/7203-gantt-toolbar-period-label.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -Gantt toolbar: the period label names the visible window, and the prev/next -buttons step it (objectui#7203). - -The label formatted `timelineRange.start` — the memo spanning the whole dataset -— so it named the first unit of the entire result set and could not change while -the chart was scrolled, because it was not derived from scroll position at all. -On a dataset running January to December it therefore read "January 2026" at -every scroll position, four pixels above a band header correctly reading -"Aug 2026". Measured on the demo fixture in Chromium at 1440x900: on first paint, -after the chart auto-scrolls to Today, the label read `December 2025` over -columns `28F 29S 30S 31M 1T 2W 3T` with the band beneath them reading `Aug 2026`. -Two month labels four pixels apart, disagreeing — and the wrong one is the -prominent one, so the chart reads as if the columns were mislabelled. - -The label now names the period at the left edge of the viewport, snapped to the -same tier `headerGroups` bands the timeline by: a month under day and week view, -a year under month and quarter view, a decade under year view, the shift-day -under shift-segmented day view. The toolbar and the band header therefore agree -by construction rather than by two derivations that can drift. Wording is -unchanged for the month tier — the toolbar still spells the month out -("August 2026" beside the header's "Aug 2026"). - -The `‹` / `›` buttons rendered an `aria-label` and an icon and carried no -`onClick`. They now scroll the visible window one period backwards/forwards at -that same tier, clamped to the ends of the timeline (ADR-0049 enforce-or-remove: -wiring is the branch the label change makes available). They step the label's -tier rather than one column, so a click always changes what the label says. - -The band header is untouched. It was already correct; it is the reference here. diff --git a/.changeset/7204-gantt-tasklist-width-and-date-predicate.md b/.changeset/7204-gantt-tasklist-width-and-date-predicate.md deleted file mode 100644 index 516b083e92..0000000000 --- a/.changeset/7204-gantt-tasklist-width-and-date-predicate.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-gantt': minor ---- - -Size the gantt task list from its container, and give a row's dates ONE predicate -(objectui#7204, objectui#7224; maintainer ruling 2026-09-02, option Y). - -**A row could show no dates at all.** Two gates decided whether a row's dates were -painted, and they read two different widths. The Start/End columns were gated on the -container-derived task-list width; the `8/26 → 9/2` sublabel under the title was gated -by the component's own `@media (min-width: 640px) { .gantt-sm-hidden { display: none } }` -rule, which reads the viewport. Between a 640px and a 1023px container both were shut, -so the row's dates existed in the DOM twice and were painted zero times. The same hole -opened at any width once the splitter was dragged under the threshold. - -The sublabel now renders on exactly the complement of the Start/End columns, both from -the same container-derived width, and the media rule is gone. A row always carries its -dates one way or the other. - -**And the task list no longer caps at 320px.** From a 1024px container up, the pane -takes 3/8 of the container clamped to `[320, 560]` instead of a flat 320. At 1440 that -is 540px, which leaves the title 287px with the Start/End columns still painted — a -40-character title measures 262px in the row's font, so real-world task names stop -truncating to about seven characters while several hundred pixels of chart sit empty. -Measured in Chromium at a 1440px container: title 53px before, 287px after. - -The Start/End threshold moves from an estimated 280 to a derived **412** — 32 row -padding + 160 for the two columns + 28 for the open-details slot + 32 of title -furniture + 160 minimum title, each term traced to the markup that spends it. Below it -the sublabel carries the dates. One consequence worth stating: between a 1024px and a -1097px container the columns are now off and the dates ride the sublabel, which trades -two 80px date cells for a title that grows from 67px to 291px. - -Not fixed here, and unchanged: a row's `depth * 14` indent is unbounded, so no single -default keeps a deeply nested row legible. diff --git a/.changeset/7206-dead-context-tail-comments.md b/.changeset/7206-dead-context-tail-comments.md deleted file mode 100644 index ac1ddad187..0000000000 --- a/.changeset/7206-dead-context-tail-comments.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Comment-only fix, no release: thirteen comments across ten files described the -tail of `dependentValues ?? ctx.formValues ?? ctx.data ?? {}` as a live channel -a host could supply. It is not one, and never has been. - -`SchemaRendererContextType` (`packages/react/src/context/SchemaRendererContext.tsx`) -declares exactly `dataSource`, `debug`, `debugFlags` and `apiFetch`, and -`SchemaRendererProvider` accepts no other prop. There is no `data` member and no -`formValues` member, so that tail is **unconditionally empty in production** — -unsettable rather than merely unset, because no host can populate a member the -type does not declare. `dependentValues` is today the only channel that can -carry a record. - -The corrected comments now state what is measured. The strongest of them claimed -the fall-through reached "the OUTER page's record"; another credited -`dependentValues` with a context fallback that only `dataSource` actually has; -a third framed the chain as three suppliable links when only the first has ever -been suppliable. Two of the thirteen were written the same day this was -measured, so they were wrong when written rather than gone stale. - -No runtime, type or test behaviour changes: the reads themselves are left -exactly as they are, and the diff contains zero non-comment lines. Whether the -channel should be made real or retired is the open question on objectui#7206 and -is deliberately not answered here. diff --git a/.changeset/7210-gantt-paging-chrome.md b/.changeset/7210-gantt-paging-chrome.md deleted file mode 100644 index 7f8ff53d67..0000000000 --- a/.changeset/7210-gantt-paging-chrome.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -A gantt list view no longer shows the record-count bar, because the bar describes a -request that view does not draw (objectui#7210, half 1). - -`ListView` renders one record-count bar for every `viewType`: the row count, the -"Showing first N records. More data may be available." warning that goes with its own -`$top: pageSize` query, and a rows-per-page selector that re-issues that query. On grid, -kanban, calendar, gallery, timeline and map the bar is accurate — those renderers draw -the `data` `ListView` hands down. - -On `gantt` it is not. Measured, not inferred: the registered `object-gantt` renderer -forwards no prop but `schema`, so the `data` prop never reaches the chart and -`ObjectGantt` issues its own query — one that carries no `$top` at all. In a harness of -18 rows with `pagination.pageSize: 6`, the chart drew all 18 while the bar under it read -"6 records · Showing first 6 records. More data may be available." Because it is the -only paging disclosure on the screen, a reader takes it as describing the chart; that -reading already produced a wrong finding in an application repo, which cost a browser -session to disprove. Authoring `pagination.pageSize` could not have fixed it either — -the chart's request never carried a page size to begin with. - -Scoped to `gantt` alone. Every other surface keeps its bar unchanged, warning included; -a kanban over the same result set still reports its page honestly. - -Not changed here, deliberately: the gantt's query still has no ceiling, and `ListView` -still issues its own paged query for that view (its rows still drive the loading -skeleton and load-error panel, `UserFilters`' option counts, and the client-side CSV / -JSON export). Capping a non-grid view's fetch is an open maintainer decision — adding -one would turn a complete schedule into a quietly truncated one — and this correction is -right whichever way that lands. diff --git a/.changeset/7210-non-grid-row-ceiling.md b/.changeset/7210-non-grid-row-ceiling.md deleted file mode 100644 index 7adf0e20b2..0000000000 --- a/.changeset/7210-non-grid-row-ceiling.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/i18n': patch -'@object-ui/plugin-gantt': minor -'@object-ui/plugin-calendar': minor -'@object-ui/plugin-map': minor -'@object-ui/plugin-tree': minor ---- - -A non-grid view's fetch now carries a platform row ceiling, and crossing it is -never silent (objectui#7210, maintainer ruling a′, 2026-09-02). - -Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each -issued a `find` with **no `$top` at all**, so the request returned the entire -filtered result set. At the 186 rows the card was filed from that is invisible; -on an object with 100k scheduled rows it is the whole table into the browser, -and nothing an author could write — `pagination.pageSize` included — could -bound a request that never carried a cap to begin with. - -**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` -rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was -larger they render a footnote naming both numbers, verbatim as it renders: -*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the -ceiling nothing changes: the full set draws and no footnote appears. - -The four view packages take a **minor**, not a patch: a result set above the -ceiling is no longer drawn in full, which is a behaviour break whatever the -fixed group does to the released version number. - -**The ceiling is a platform constant, not an authorable key** — `2000`, exported -from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or -`dataSource: { limit }` still does not reach these queries, by the same ruling; -three alternatives were rejected with it (a documentation note only — still the -whole table; truncating at `pageSize` — silent, and a complete schedule capped -at one page; an authorable `maxRows` — a new permanent key every author sets). - -**Why 2,000.** One constant for all four, so the binding view sets it. Measured -in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows -grow (virtualised task list; four events per day cell; auto-clustering above -100 markers), while `ObjectTree` flattens every expanded node into the document -at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows -is where the worst of the four lands at ~10,400 elements — an order of -magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the -real application result set this card came from. - -New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, -`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` -and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the -footnote copy in all ten packs. diff --git a/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md b/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md deleted file mode 100644 index b3c6f63412..0000000000 --- a/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md +++ /dev/null @@ -1,41 +0,0 @@ ---- ---- - -Repo tooling and build config only — no published package changes, so no version bump. - -Published build tsconfigs must now exclude tooling DIRECTORIES, not just tooling FILE NAMES, -and a new per-PR gate enforces it: `pnpm check:published-tsconfig-exclude` -(`scripts/check-published-tsconfig-tooling-exclude.mjs`, wired into `ci.yml`'s Type Check job). - -The same defect had been repaired three times and gated never — objectui#4006 (73 `*.test.d.ts` -published from two packages), objectui#4836 (9 more, one of them an emitted module whose first -statement imports `vitest`) and objectui#6943 (the same package as the first, because that fix -wrote the name form and the directory form was never generalised). objectui#7212 measured the -standing exposure instead of another instance: 29 published packages carried the name form with -ZERO offending files, green because nobody had yet added a shared helper to a `__tests__/` -directory, not because the config would stop one. The maintainer ruled for the ratchet on -2026-09-02. - -The gate reads `exclude` arrays and nothing else — no build, no artifact, no emit model. That -narrowness is the point: objectui#4846 measured the cheap static gate "no build tsconfig program -may contain a tooling file" and rejected it, because deciding whether a tooling file in a program -is a defect means reimplementing three third-party emit semantics. Asking only DOES THE CONFIG -NAME THE DIRECTORY needs none of that. The artifact-level `pnpm check:published-dist` is -untouched and stays the second line of defence — it is the only criterion that cannot be wrong -about what actually ships. - -28 build tsconfigs were converted in the same commit so `main` is green on merge. The conversion -is emit-neutral, and that was measured rather than assumed: each package's build program file -list was resolved with TypeScript's own config resolver before and after, and all 33 enforced -packages came back byte-identical (0 files added, 0 removed). Identical program input means -identical emit, so no published artifact and no bundle moves. - -Six published packages are named carve-outs rather than converted, because their emitter never -builds its program from this file list, and each re-proves its own reason on every run instead of -being trusted: `@object-ui/cli`, `@object-ui/create-plugin` and `@object-ui/data-objectstack` emit -from a `tsup` entry graph; `@object-ui/plugin-charts` keeps its tooling exclude in the `dts()` -options and carries no `exclude` key in its tsconfig; `@object-ui/console` and `@object-ui/runner` -are Vite applications with `noEmit: true` and no `dts()` plugin, so they write no declarations at -all. Two of those six — `create-plugin` and `runner` — were found at authoring time and were -inside the ruling's initial red set; requiring a shape in a config their emitter never consults -would have been a check that is green about nothing. diff --git a/.changeset/7215-expand-fls-gate.md b/.changeset/7215-expand-fls-gate.md deleted file mode 100644 index d962648ae3..0000000000 --- a/.changeset/7215-expand-fls-gate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-grid': patch -'@object-ui/plugin-list': patch ---- - -FLS-gate the `$expand` projection at both build sites (objectui#7215). - -objectui#6898 closed field-level security on `$select`. `$expand` was left ungated at -both projection sites — `ObjectGrid`'s own fetch and `ListView`'s `expandFields` memo — -so a `lookup` / `master_detail` / `user` / `tree` field the current principal cannot -read was still handed to the server for expansion. `$select` on a denied lookup asks for -its bare foreign key; `$expand` on the same field asks the server to resolve it and -return the related record, so the larger of the two disclosures was the ungated one. - -**Reproduced before it was fixed**, as failing tests at both sites, and the same leak -reaches further on the `ListView` path: that builder's `$select` gate drops the denied -column and then adds the expand roots back unconditionally, so the denied field walked -back into `$select` as well. Gating the expansion closes both halves. - -**Grading, measured rather than assumed.** Against ObjectStack's own server this is -defence-in-depth, exactly as objectui#6898 is: `plugin-security`'s -`FieldMasker.maskRecord` deletes every unreadable key from each returned row, and -objectql's expand path writes the resolved record back under that same key, so one -statement removes the expanded object and the bare id alike; the expansion sub-read is -itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS -treatment). It is load-bearing for any backend that does not strip. - -**Nothing a permitted view did stops working.** The gate judges the OUTPUT of -`buildExpandFields`, which is already a subset of the object's declared -reference-bearing fields, so the "`checkField` answers false for an undeclared key" -trap cannot be reached and derived / host-joined columns are untouched. An unanswered -permission policy filters nothing. `buildExpandFields` itself is unchanged. diff --git a/.changeset/7216-listview-speculative-fls.md b/.changeset/7216-listview-speculative-fls.md deleted file mode 100644 index 29ba6a188f..0000000000 --- a/.changeset/7216-listview-speculative-fls.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -FLS-gate the speculative half of `ListView`'s `$select` projection (objectui#7216). - -`ListView` builds `$select` from two populations and, until now, asked them different -questions. The user-declared `columns` were passed through `perms.checkField(...)` by -objectui#6898. Everything the builder adds ON TOP — the kanban / gantt / timeline / -calendar / gallery field bindings, the timeline's auto-added `status` / `priority` -badge fields, and the operands harvested from row-action and conditional-formatting -predicates (objectui#3501) — went through `addSpeculative`, which intersected them -against the object's declared fields and then added them unconditionally. - -The two gates it needed answer unrelated questions and neither substitutes for the -other. The known-field gate keeps an **unknown** key out, because some backends answer -an unknown `$select` key with an empty result set rather than ignoring it. The FLS gate -keeps a **known but denied** key out, because sending it leaks the value at the server -boundary even though the UI hides it. A field can be perfectly well-declared and still -denied, and that was the case this path did not handle: a kanban grouped by a denied -field, or a gantt bound to a denied date, still named it in the request. - -**Reproduced before it was fixed.** Eight of fourteen new pins fail on the unmodified -tree, each reaching the denied field only through a view binding — a denied *column* has -been dropped since objectui#6898, so a pin naming one would have proved nothing. - -The gate goes inside `addSpeculative` rather than at the five call sites, so every route -into the speculative union is covered at once; gating call sites one at a time is how -the asymmetry arose. It runs **after** the known-field intersection, the ordering -objectui#7179 established: `checkField` answers false for an undeclared key, so asking -it first would drop derived and computed bindings — and would be the reason they were -dropped. The platform record columns (`created_at`, `owner_id`, the audit FKs) are -carved out for the reason they already are elsewhere in this builder: every object -carries them and none declares them, so no field policy mentions them and an FLS answer -about them is always false. Without the carve-out a calendar bound to `created_at` would -go blank for everybody. - -**Nothing a permitted view did stops working.** A permitted binding is still projected, -an unanswered permission policy filters nothing, and the projection is rebuilt when the -policy answers — pinned, because a gate that only runs before `/me/permissions` resolves -is a dead gate that passes almost every test written for it. - -objectui#7179's `addGroupingField` wrapper is removed: its predicate was identical to -the one now inside `addSpeculative`, and two spellings of one gate is the shape that -lets them drift. That path keeps its behaviour and gains the FLS pin it never had. diff --git a/.changeset/7217-grouping-null-entry-guard.md b/.changeset/7217-grouping-null-entry-guard.md deleted file mode 100644 index ba37e2c0bd..0000000000 --- a/.changeset/7217-grouping-null-entry-guard.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -Grid: a malformed entry in `grouping.fields[]` no longer crashes the whole grid. - -A `null` (or `undefined`) hole in the array was dereferenced with no guard at -two places — `ObjectGrid`'s `groupValueFormatter` memo and `useGroupedData`'s -`buildLevel` — throwing `TypeError: Cannot read properties of null (reading -'field')` during render, before any projection was built. Both sites now read -one normalized entry list, admitting exactly the entries `collectGroupingFieldRefs` -harvests into the query projection, so the usable grouping levels still group -and an unusable entry is simply dropped rather than taking the view down. diff --git a/.changeset/7218-rowcolor-host-relay.md b/.changeset/7218-rowcolor-host-relay.md deleted file mode 100644 index a82511ee66..0000000000 --- a/.changeset/7218-rowcolor-host-relay.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-view': patch ---- - -Relay a per-view `rowColor` through the two object-view hosts (objectui#7218). - -`rowColor` is a declared member of `ListViewSchema` — imported by reference from -`@objectstack/spec`, shape `{ field, colors? }` — and `ListView` reads it to -seed its `rowColorConfig` state, which colours whole rows from the named field's -value. Neither object-view host relayed it: `app-shell`'s -`ObjectView.renderListView` builds its list schema by spreading the host's and -then relaying 47 named keys off the active view, and `plugin-view`'s -`ObjectView` assembles 46 inside its `object-view HOST-COMPOSITION SURFACE` -fence. `rowColor` had a rung in neither. - -So an authored per-view row colour was unreachable on the object route: -authored, validated, built and served correctly, then dropped at the relay. -Nothing errored and every authoring gate passed — the only symptom was that the -rows were not coloured, which an author cannot notice short of diffing the DOM. -Same "declared and inert" shape objectui#7199 fixed for `description`. - -**This is a relay, not a new surface.** The interface route -(`InterfaceListPage.tsx`) has shipped `rowColor: view.rowColor` next to -`grouping` and `pagination` since ADR-0047, into a schema typed -`ListViewSchema`, with no fence of any kind — so the key was already -author-reachable and already had a delivery path; two of three hosts simply did -not use it. The legacy shorthand for the same feature (bare `color`) already had -a rung in both literals; only the spec-canonical spelling was missing. - -**No published surface moves.** Both rungs are view-sourced only, and neither -adds a cast read off the object-view node — that would have added a 28th name to -the objectui#5097 HOST-COMPOSITION exemption whose count the 2026-08-18 ruling -fixed at 27, which is a ruling and not a refactor. `grouping` is the in-fence -precedent for a view-only rung. - -⚠️ Not `userActions.rowColor`, a boolean permission toggle sharing this name at -a different nesting level ("may the user open the colour panel" versus "what the -colours are"). That key is untouched, and the new pins hold the two apart. diff --git a/.changeset/7219-retire-viewdescription-catalog-key.md b/.changeset/7219-retire-viewdescription-catalog-key.md deleted file mode 100644 index a8c7b42d84..0000000000 --- a/.changeset/7219-retire-viewdescription-catalog-key.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/i18n': minor ---- - -Retire `useObjectLabel().viewDescription()` and the `_views..description` -catalog convention it resolved (objectui#7219, maintainer ruling 2026-09-02, -option B — enforce-or-remove). - -**Breaking for translation bundles, deliberately — and this text is the notice.** -Out-of-repo translation bundles that authored -`.objects.._views..description` cannot be seen from -this repo, so there is no census to point at and no migration script to run: -that key now resolves nowhere, and an entry left under it is simply ignored. -Nothing throws, and nothing else on that node changes. - -**What replaces it.** A list view's description has exactly ONE channel: the -`I18nLabel` value authored on the view entry itself — a string, or an inline -locale map: - -```ts -listViews: { - by_unit: { - label: 'By business unit', - description: { en: 'Open work only.', 'zh-CN': '仅未完成的工作。' }, - }, -} -``` - -`ObjectView` relays that value to the renderer and `plugin-list`'s `ListView` -resolves it against the display locale (objectui#7199, shipped before this -change), so the authored channel already works end to end. **Migration:** move -the sentence out of the translation bundle and onto the view entry as a locale -map. - -**Why removed rather than wired in.** The member was declared and resolved but -had zero callers and zero in-repo bundle usage — an entry authored under the -catalog key reached no screen. Wiring it in would have put two vocabularies on -one concept (`I18nLabel` on the entry, and the catalog key) and required a -precedence rule between them, which is the ambiguity rather than the fix. - -The two sibling members on the same node are **unaffected**: `viewLabel` and -`viewEmptyState` still resolve `_views..label` and -`_views..emptyState.{title,message}`, and the shared `viewSuffixes` key -builder they use is unchanged — only the `'description'` tail is gone. Pin tests -in `@object-ui/i18n` and `@object-ui/app-shell` were retargeted onto those two -survivors plus a case that authors the catalog `description` and asserts the -authored value is what a consumer resolves. diff --git a/.changeset/7221-filter-dialect-equivalence.md b/.changeset/7221-filter-dialect-equivalence.md deleted file mode 100644 index fa22884616..0000000000 --- a/.changeset/7221-filter-dialect-equivalence.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only: pin the measured behaviour of the two filter dialects one authored view filter reaches the wire in (objectui#7221). Records the lowering `mergeFilterNodes` produces, the row sets each dialect selects through `ValueDataSource`, and the wire strings and server-side reading of each through the ObjectStack adapter. No published behaviour changes. diff --git a/.changeset/7225-settled-schema-convergence.md b/.changeset/7225-settled-schema-convergence.md deleted file mode 100644 index c8b9c16de3..0000000000 --- a/.changeset/7225-settled-schema-convergence.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-view': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-gantt': patch ---- - -The settled-schema convergence, and the gantt's duplicate query gated -(objectui#7225, maintainer ruling B, 2026-09-02). - -`useSettledSchema` was extracted and published in PR #6690 with exactly **one** -non-test adopter (`ObjectTree`, the component that had an actual defect — -objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and -`ObjectCalendar` kept their own hand copies of the same shape, so a published -export was owed compatibility forever **and** the duplication it was named for -stayed. All three now call the hook. - -The migration is a pure deduplication with no behaviour delta — the hook was -extracted *from* these three shapes, so each becomes a one-line call. -`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the -recipe the hook's own doc comment prescribes for it by name: pass the data -source as `undefined` for a render that must not read metadata -(`hasInlineData ? undefined : dataSource`), so "inline value data set" is -expressed as "there is no source to read from" rather than as a second enable -flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and -what made the calendar's obstacle a non-obstacle: it was about the gate half. - -**One observable change:** `ObjectKanban`'s rejected definition read now logs -on `console.error` with a `[useSettledSchema]` prefix instead of -`console.warn`. Its test spy moves with it, and now asserts on the channel -rather than merely silencing it. - -**The gantt's duplicate query is gated** (ask 2 of the card; #6482's -undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s -dependency list, so every load issued two unbounded queries — the first with no -`$expand` at all. Measured on this component across three latency profiles, the -cost is not the mild "round trip bought and thrown away": when the metadata -read is the slower of the two, which is the common case on a cold -`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids, -back to the loading placeholder, then the expanded rows. It now issues one -query, already expanded. - -Gating the gantt required its schema resolution to settle on EVERY exit -(objectui#7232): the hand-rolled effect returned without settling on -`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while -nothing waited on it, and a chart that never loads once something does. -`useSettledSchema` settles on all three by construction, which is what makes -the gate safe; both exits are pinned. - -⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's -separate ruling, in its own commit on the same branch. diff --git a/.changeset/7228-gantt-column-width-flat-floor.md b/.changeset/7228-gantt-column-width-flat-floor.md deleted file mode 100644 index 6a34e9ce14..0000000000 --- a/.changeset/7228-gantt-column-width-flat-floor.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -The gantt's timeline column width says what it does: a flat 110px floor, not a -container-width breakpoint table (objectui#7228). **No behaviour changes** — this is -why the changeset carries no package bump. - -`columnWidthForContainer(width)` branched on the container width three ways and returned -110 from all three arms, so its parameter was read only to be compared against thresholds -nothing acted on. That shape is not neutral: the two siblings directly beneath it, -`taskListWidthForContainer` and `showStartEndColumns`, branch on the *same* 640/1024 -breakpoints and really do vary, so the dead table read as a live responsive policy. A -reader — human or agent — reasonably concluded the gantt narrows its columns in small -embeds. It does not, and has not since objectui#1870. - -The history settles what the arms were for. The function was born a real curve -(`35/50/60` off `window.innerWidth`), kept it through the rename to a container-derived -width, and was bumped to `44/64/80` inside objectui#1870 — then, in a later commit of that -same PR titled *"floor timeline columns at 110px so day/week/month stay readable"*, the -whole curve was deliberately replaced by a single floor. The comment added in that commit -names the value's provenance ("user-specified minimum") and why a flat floor costs nothing -at the wide end (the fit-stretch and manual zoom both move the width upward). The -branching was leftover shape, not an unfinished table. - -So the arms are gone and the value all three returned is now a module constant, -`BASE_COLUMN_W`, documented as the floor it is — matching how objectui#7420 hoisted the -task-list geometry in this file: fixed px values become named module constants, helpers -stay functions only where they genuinely vary with the container. - -Nine pins now hold the behaviour on both sides of each retired breakpoint (320 / 500 / -639 / 640 / 800 / 1023 / 1024 / 1280 / 1920), plus one that requires a single distinct -value across all of them. Nothing pinned this before: every sibling suite that reads a -column width first forces the container to 1280 "so columnWidth=110 (deterministic)", -pinning the widest arm alone — so neither re-curving the width nor moving the floor would -have been caught. diff --git a/.changeset/7230-expand-fls-gate-five-sites.md b/.changeset/7230-expand-fls-gate-five-sites.md deleted file mode 100644 index 17fe56f95b..0000000000 --- a/.changeset/7230-expand-fls-gate-five-sites.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/app-shell': patch ---- - -FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). - -objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope -(`ObjectGrid`, `ListView`). The helper is reached from more places than that. This -closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, -`DetailView`, and `ObjectDataTable` (which builds its own whitelist in -`computeLookupExpand` rather than calling `buildExpandFields`). - -**Three of them pass no column list at all**, which makes them the sharp ones: -`buildExpandFields` reads an absent column list as "no column restriction" and falls -back to **every declared relation on the object**, denied ones included. So a standalone -calendar, a gantt, and every record page in the console asked the server to resolve the -object's full relation set by default rather than by configuration. - -**`DetailView` was input-gated, and that is the defect rather than the fix.** Its column -list is already FLS-filtered field by field, which is exactly the route PR #7229 measured -as unsound: an emptied column list reads as "no column restriction", so a detail view -whose authored fields are all denied had its `$expand` **widened** from the relations it -asked for to every relation the object declares. The principal who may read least was -asking for the most. - -**Reproduced before it was fixed**, as a failing test per site. - -**Grading, measured rather than assumed.** Against ObjectStack's own server this is -defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s -`FieldMasker.maskRecord` deletes every unreadable key from each returned row and -objectql's expand path writes the resolved record back under that same key, so one -statement removes the expanded object and the bare id alike; the expansion sub-read is -itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS -treatment). It is load-bearing for any backend that does not strip, and the -client-request side is real regardless. - -**Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, -which contains only the object's declared reference-bearing fields, so the "`checkField` -answers false for an undeclared key" trap cannot be reached and derived / host-joined -columns are untouched. An unanswered permission policy filters nothing. Neither -`buildExpandFields` nor `computeLookupExpand` is changed. diff --git a/.changeset/7231-gantt-stale-reload-finally.md b/.changeset/7231-gantt-stale-reload-finally.md deleted file mode 100644 index e1221f261d..0000000000 --- a/.changeset/7231-gantt-stale-reload-finally.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -`ObjectGantt` no longer blanks the chart when one reload supersedes another - -`reload()` already sequenced concurrent runs with `reloadSeqRef` and guarded every -result write with `isCurrent()`, but its `finally` was unguarded — so a **superseded** -reload still flipped `loading` / `refreshing` off. The stale run only had to finish -first, which is the ordinary case whenever a second reload is issued while the first -is still in flight: the placeholder was released, no rows had arrived, and the user -saw an empty chart until the fresh response landed. - -The `finally` now clears the flags only when the run reaching it is still the current -one. It clears **both** flags rather than only the one its own `silent` mode set: -being current at that point means nothing is in flight any more, so clearing only its -own mode would strand the other flag whenever the superseded run used the other mode -— a silent toolbar refresh overtaken by a filter-change reload would have left the -refresh button busy for the life of the component. - -This is the reload guard alone. Nothing about which queries are issued, how they are -projected or how they page changes. diff --git a/.changeset/7238-disabled-one-carrier.md b/.changeset/7238-disabled-one-carrier.md deleted file mode 100644 index 2961511fc5..0000000000 --- a/.changeset/7238-disabled-one-carrier.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/components": patch ---- - -fix(components): eight widgets now consume `SchemaRenderer`'s evaluated `disabled` verdict instead of re-reading the raw authored key - -`disabled` on a schema node is `boolean | string` — the string being a predicate. `SchemaRenderer` evaluates `disabled` / `disabledOn`, strips the raw key from the props it spreads, and forwards the answer as a real `disabled` prop. `ui:form`, `ui:button`, `ui:input`, `ui:textarea`, `ui:checkbox`, `ui:select`, `ui:combobox` and `ui:collapsible` re-read the raw key beside that verdict, and an expression string is truthy however it evaluates. - -Two user-visible defects go away: - -- `ui:form` — a form declaring `disabled: "${...}"` greyed out every field, the submit button and the cancel button even when the predicate was FALSE. -- `ui:button` — `loading: true` did not disable the button when it was rendered through `SchemaRenderer`: the computed state was overwritten by the forwarded verdict arriving through the DOM pass-through spread, so the spinner ran on a live control. - -The six DOM pass-throughs kept their behaviour through `SchemaRenderer` and now read the verdict by name rather than depending on spread order. diff --git a/.changeset/7243-colorfield-shared-ladder.md b/.changeset/7243-colorfield-shared-ladder.md deleted file mode 100644 index f2fdbb5e31..0000000000 --- a/.changeset/7243-colorfield-shared-ladder.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-timeline': patch ---- - -`colorField` now means the same thing in the gantt, the calendar and the timeline -(objectui#7243). - -**The inversion this fixes.** `gantt.colorField` is documented as "field that drives the -bar color", and the renderer passed the stored value straight into the bar's -`backgroundColor`. Pointing the key at a select field therefore emitted -`backgroundColor: "open"` — not a colour, so the browser dropped the declaration and -every bar rendered identically. OMITTING the key was strictly better: the absent-key -branch derived a real colour per status. Declaring the documented key was worse than not -declaring it, with no error, warning or console message either way. - -The same key also meant three different things across the three lenses: the timeline -resolved the field's authored option `color`, the calendar hashed the raw value onto a -fixed palette, and the gantt emitted the raw value. An author colouring three views by -one field got three unrelated results, one of which was no colour at all. - -**The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's -resolver, lifted so all three call it: - -1. the field's own option `color` for the record's value; -2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`, - `#rrggbbaa`, `rgb(...)`, `hsl(...)`); -3. else each renderer's own last rung, which is deliberately NOT shared — the gantt - derives a semantic-token hex (a bar must be painted), the calendar keeps its - theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its - default marker. - -**What changes for authors.** A gantt or calendar whose `colorField` points at a select -field with authored option colours now paints those colours. A gantt value that is -neither an option colour nor a colour literal now derives a colour instead of emitting -an invalid CSS value — including a palette NAME (`red`), which now resolves to that -palette's hex, the behaviour the key's own contract has always promised ("hex or -semantic name") and the one `borderColorField` already had. `gantt.borderColorField` -takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps -today's behaviour otherwise and deliberately gains no derivation rung, since the stroke -is opt-in and deriving one for every record would draw an alert on records that have -none. - -Calendars whose `colorField` points at a plain categorical field are unchanged: that -value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline -is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar -already accepted. diff --git a/.changeset/7245-default-view-namefield-lead.md b/.changeset/7245-default-view-namefield-lead.md deleted file mode 100644 index 01458fa56d..0000000000 --- a/.changeset/7245-default-view-namefield-lead.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-grid': patch ---- - -A synthesized default list view now always leads with the object's name field -(objectui#7245). - -**The defect.** An object that declares no list view gets its default grid columns -synthesized from `highlightFields`, taken verbatim. But `highlightFields` is ADR-0085's -*"most important fields"* role, not a column list — and its first consumer, the -detail-page highlight strip, **deliberately removes the title field**, because the page -H1 directly above it already shows one. So metadata that is entirely correct routinely -omits the record's name from `highlightFields`. The showcase `showcase_account` declares -`nameField: "name"` and `highlightFields: ["status", "industry", "annual_revenue"]`, and -its default `所有记录` grid rendered 14 rows whose columns were `#` / Lifecycle / Industry -/ Annual Revenue / actions — no name column, and no way to tell one account from another. - -A list has no H1 to lean on, so the same declaration needs the opposite treatment here. -This is not a new convention: `deriveLookupColumns` in `@object-ui/fields` already leads -its record-picker columns with the display field and filters it out of the declared list. -The list faces now agree with it. - -**What changed.** `@object-ui/core` gains two exports on the ADR-0079 title ladder: - -- `resolveNameField(objectDef)` — *which field* titles an object: the declared - `nameField` (then its deprecated `displayNameField` / `NAME_FIELD_KEY` aliases), else - the type-aware derivation. The name-space twin of `getRecordDisplayName`, which answers - what that field *says* on one record. Both now read one spelling of the declared - pointer, so they cannot drift into naming different fields. -- `leadWithNameField(objectDef, columns)` — moves that field to the front of a - **synthesized** column list. - -All three faces that synthesize default list columns call it: `ObjectView` -(`defaultListColumnsFromObject`), `InterfaceListPage` (`defaultColumnsFromObject`) and -`ObjectGrid`'s own derivation. The name field is **moved**, not merely appended, so an -author who lists it third still gets it first — "the column that identifies the row" -means first. On the two capped faces the lead is applied *before* the 5 / 6-column slice, -so an object declaring its name field late no longer loses it off the end. - -**Scope, deliberately narrow.** Author-declared column lists are untouched — a view or -grid that declares `columns` / `fields` said what it wants, and reordering it would be -renderer-side second-guessing of metadata. Three cases also decline to lead: a name field -the object carries no field def for (never fabricate a column), one marked -`hidden: true` (the author said don't show it), and a *derived* pick that lands on a -system-managed column — `deriveTitleField` filters by type only, and leading a default -list with a raw id is the regression objectui#2702 / #2777 fixed. A *declared* -`nameField` pointing at a system field still leads: `sys_migration` really does point at -`id`, and an explicit designation is not a heuristic misfire. diff --git a/.changeset/7248-scatter-legend-names-its-series.md b/.changeset/7248-scatter-legend-names-its-series.md deleted file mode 100644 index fa76eda861..0000000000 --- a/.changeset/7248-scatter-legend-names-its-series.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -Name the scatter legend's series, so its swatch stops reading as a stray data point -(objectui#7248). - -The Chart Gallery scatter ("Estimate vs Progress") appeared to draw a seventh point -below the x-axis, outside the plot area. It was not a point. `ChartLegendContent` -resolves a label as `config[nameKey || item.dataKey || 'value']`, and a `` -carries **no `dataKey`** — scatter's keys live on the XAxis/YAxis, not on the mark — so -the key collapsed to the literal string `'value'`, missed a config keyed by measure -name, and the legend entry rendered its colour swatch with no text beside it. An 8x8 -square in `--chart-1`, the same colour as the marks, sitting under the x-axis. - -Measured on the running showcase in real Chromium: the swatch sat at cy 341 against a -plot area ending at cy 295, on a y scale of 4.835 px per unit — y = -9.5, at x ≈ 45. -That is the "x≈40, y≈-10" the report described, to the pixel, and all six real marks -were inside the plot area at every viewport width swept from 1440 down to 480. - -**The y domain was not the defect and is unchanged.** Clamping it — the fix the report -asked for — would have created the bug it described: mixed-sign and all-negative -fixtures are pinned here drawing every mark, because recharts already extends the -domain to cover negative values. - -Two changes. The scatter now passes `nameKey` so its legend resolves the measure's -label, and `ChartLegendContent` falls back to the series `name` recharts itself put on -the legend item when the config lookup misses. The second closes the class rather than -this one instance: the swatch renders unconditionally, so a config miss must never -leave it anonymous. Charts whose config already resolves are unaffected — only a -currently-empty label changes. diff --git a/.changeset/7252-form-outcome-toast-supersede.md b/.changeset/7252-form-outcome-toast-supersede.md deleted file mode 100644 index 07abcf2c6c..0000000000 --- a/.changeset/7252-form-outcome-toast-supersede.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/console': patch ---- - -A form no longer ends on a screen asserting both a failure and a success (objectui#7252). - -A refused submit raised an error toast that nothing ever retired, so when the user -fixed the input and submitted again the confirmation of that second attempt appeared -*beside* the refusal of the first — a wizard's last step showing "Invalid project -status transition." and "Your new project is ready…" at the same time. - -Every outcome toast a form raises now travels under one stable per-form id, so the -later outcome supersedes the earlier one instead of stacking beside it: - -- `@object-ui/components`' form renderer publishes its three outcome toasts (the - field-level rejection, an `onAction` error, and a rejected write) under that id, and - retires the previous attempt's toast in the same place it already cleared the - previous attempt's in-form banner. This is what fixes the reported wizard flow: the - refusal comes from this renderer while the success toast is raised by the host - (`WizardForm` / `ObjectForm`), so no single raiser could supersede the other before. -- the console's own `FormPage` publishes its confirmation and its submit failure under - one id, for the same reason. - -Toast durations are unchanged — this is about supersession, not lifetime. The -objectui#4190 arm is deliberately excluded: a refused redirect *destination* still gets -its own toast, because the write succeeded and that refusal has to stay readable beside -the confirmation it qualifies. diff --git a/.changeset/7256-chrome-home-follows-declared-landing.md b/.changeset/7256-chrome-home-follows-declared-landing.md deleted file mode 100644 index 60ca128b76..0000000000 --- a/.changeset/7256-chrome-home-follows-declared-landing.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The console chrome's Home affordances follow the product's declared landing (objectui#7256). - -`/` has honored `app.isDefault` since the hardcoded `PREFERRED_APPS = ['cloud_control']` -redirect was retired; the chrome had not. The top-bar logo, the sidebar's Home row, the -mobile sheet's Home row and the app-switcher's Home entry each named `/home` literally — -the ENVIRONMENT layer's launcher (ADR-0075). So a deployment that declares a landing -offered the customer two homes in two voices, and one click on the logo left the declared -one. - -On cloud's control plane that second home is actively wrong: its "Build an app" / "Start -from a template" cards are environment-side actions that cannot work from the control -plane, and its "Your apps" tiles are the control plane's own internal management apps. - -- `@object-ui/app-shell` adds `resolveDeclaredHomePath()` — the one reader of the - declaration — plus `useHomePath()`, which the four chrome sites now consume. The signal - is the App metadata the server already sends: no hostname sniff, and no product name - baked into the shared bundle. -- `@object-ui/console`'s `/` resolver is unchanged, and a behavioural matrix now pins its - answer equal to the chrome's for every declared app list, so the post-login landing and - the logo cannot drift apart. - -Deployments that declare no landing are unaffected: every Home affordance still resolves -to `/home`. The "this app is gone" recovery redirects in `AppContent` / `ConsoleShell` are -deliberately untouched — they are error paths, not Home affordances. diff --git a/.changeset/7257-metadata-hmr-give-up-after-404.md b/.changeset/7257-metadata-hmr-give-up-after-404.md deleted file mode 100644 index 94039eedd7..0000000000 --- a/.changeset/7257-metadata-hmr-give-up-after-404.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`MetadataHmrReloader` stops flooding a production-posture deployment with -`GET /api/v1/dev/metadata-events` 404s (objectui#7257). - -The dev-only HMR component subscribes via `EventSource`, gated on -`import.meta.env.DEV`. That gate is not airtight against every rig: a -"prod-like" build/serve setup that forces `NODE_ENV=development` for the -*build tooling* while running the server itself in production posture can -bake `DEV === true` into the shipped bundle even though the server never -mounts the dev route there — and the old reconnect loop treated every closed -connection as transient, retrying on a fixed `reconnectDelayMs` (2s) -forever. On an env host that is ~30 requests/minute of 404s per open -record/list page, drowning out the legitimate `sys_inbox_message` / -`sys_notification_receipt` polling in the same console. - -The first `connect()` attempt now doubles as the real capability probe: if -the stream closes before it ever reaches `open`, the component gives up for -good instead of retrying (and specifically not a longer interval either — -that would still spam 404s, just slower). A stream that DID open at least -once and later drops — a real dev-server restart or network blip — keeps -reconnecting exactly as before. - -No production-side replacement is introduced here: this component's only -job is turning "a metadata file changed on disk" into a full reload, and -production deployments have no such file-system event to watch. The -separate Studio-left-nav-doesn't-refresh caching issue does not go through -this SSE stream and needs its own fix. diff --git a/.changeset/7258-chart-builtin-aggregate-label.md b/.changeset/7258-chart-builtin-aggregate-label.md deleted file mode 100644 index 1d200a3fb6..0000000000 --- a/.changeset/7258-chart-builtin-aggregate-label.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/i18n': minor -'@object-ui/plugin-charts': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-report': patch ---- - -Localize the server's built-in aggregate measure titles on dataset charts -(objectui#7258 — consumer half of the objectstack#14492 contract; maintainer -ruling B, 2026-09-02). - -A dataset-bound chart's aggregate axis / legend title read the analytics -service's hard-coded English `Count` on a zh console whose category labels were -already Chinese. The renderer was passing `fields[].label` through verbatim — -correctly, for an author-declared measure (objectui#4106) — and had no way to -tell the server's built-in default apart from an author's label. - -The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural -discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | -'count_distinct'`, populated only on the server-side built-in defaults -(objectstack#14492). This change is the consumer side of that contract: - -- `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — - `ChartResultField` plus the optional `builtinAggregate` carrier - (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than - on it because the spec this release is built against does not carry the key - yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / - `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries - the locale strings in (core stays React-free and i18n-free — the same - division as `nullCategoryLabel`). A field carrying a recognised - discriminator resolves through that map; every other field keeps its wire - `label` verbatim — never by matching the label's text or the field's name - (the rejected option A). -- `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings - through the existing `report.aggregate.*` keys (zh already carried 计数 / - 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). -- `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), - `plugin-report` (`DatasetReportRenderer`): pass the resolved map to - `buildChartSeries`. - -Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same -chart titled `计数`; an `en` session still reads `Count`; an author-labelled -measure (`Tasks`) and a measure literally named `count` without the -discriminator are byte-for-byte unchanged. Until the upstream field is -populated the wire carries no discriminator and every chart renders exactly as -before. diff --git a/.changeset/7271-localstorage-probe-node26.md b/.changeset/7271-localstorage-probe-node26.md deleted file mode 100644 index 103d6f8573..0000000000 --- a/.changeset/7271-localstorage-probe-node26.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Repair the test-environment storage globals so a `Storage.prototype` instrument -cannot go silently blind on a newer Node (objectui#7271). Test only; no package -is released by this change. - -From Node 26 the experimental Web Storage globals are on by default, and -Vitest's `populateGlobal` copies happy-dom's `Storage` class but not its -`localStorage`/`sessionStorage` — so the class and the stores came from two -different implementations, and instrumentation of `Storage.prototype` observed -nothing. `vitest.setup.base.ts` now states the rule positively (in a DOM -environment every storage global is that environment's own `Storage`) and -verifies it, and `anonSeedScope-5746.enumeration.test.tsx` names the failure -mode instead of reporting seven anonymous zeros. diff --git a/.changeset/7279-object-view-split-route-pin.md b/.changeset/7279-object-view-split-route-pin.md deleted file mode 100644 index b94765a695..0000000000 --- a/.changeset/7279-object-view-split-route-pin.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Re-derive `ObjectViewSchema`'s side of the objectui#6058 SPEC-DERIVED / LOCAL split in -`zod-mirror-parity.test.ts` (it is LOCAL: the mirror takes no shape from the spec), correct -the split's counts, and pin them against the ledger and `SPEC_DERIVED_PAIRS` (objectui#7279). -Test only; no package is released by this change. diff --git a/.changeset/7282-console-docs-fragment-markers.md b/.changeset/7282-console-docs-fragment-markers.md deleted file mode 100644 index f17a41a24e..0000000000 --- a/.changeset/7282-console-docs-fragment-markers.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Console documentation only (`apps/console/docs/**`) — nothing published moves, so no version bump. - -`UI_IMPROVEMENT_PROPOSAL.md` section 3.1 taught ``. -The exported component takes `orientation`, not `direction` -(`packages/components/src/ui/resizable.tsx:44`, pinned by -`packages/components/src/__tests__/resizable-orientation.test.tsx` and used in-tree at -`packages/components/src/custom/navigation-overlay.tsx:519`), so a reader who copied the guide's -first prop got a type error. The prop is corrected in place. - -The other ten red `ts` / `tsx` blocks across that tree are excerpt fragments rather than defects, -and each now carries `check-doc-snippet-types.mjs`'s per-block fragment declaration with a written -reason naming the identifiers the excerpt leaves to its host. No `UNGATED_DOCS` entry was added: -the 2026-09-02 ruling on objectui#6600 rejects whole-file exemptions, and this card is that -ruling's step 1 — content first, then the gate roots move (objectui#6600 step 2). - -Why the empty frontmatter rather than a bump: `apps/console/docs/**` is not in `@object-ui/console`'s -`files` (`dist`, `plugin.ts`, `plugin.js`, `plugin.d.ts`, `README.md`), so no published artifact -changes. `scripts/check-changeset-presence.mjs` agrees — it reports no changeset owed for this -range. Note for the record that `@object-ui/console` is **not** `private: true`: it is a published -member of the 40-package `fixed` group at 17.6.0, so the "no release" declaration rests on the -unshipped path, not on privacy. diff --git a/.changeset/7287-resolve-title-field-shared-ladder.md b/.changeset/7287-resolve-title-field-shared-ladder.md deleted file mode 100644 index 6bf99f0bf0..0000000000 --- a/.changeset/7287-resolve-title-field-shared-ladder.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`resolveTitleField` delegates to the shared ADR-0079 ladder (objectui#7287). - -ADR-0079 collapsed ~6 divergent record-title resolvers onto one — `@object-ui/core`'s -`record-title.ts`, whose header tells the "Untitled everywhere" story that produced it. -`plugin-detail` grew one back. `resolveTitleField` now calls core's `resolveNameField` -and nothing else, so the detail page, the list column and the lookup chip cannot -disagree about which field titles an object. - -**Two rungs are gone.** - -`def.primaryField` — a `DetailViewSchema` key (`@object-ui/types` `views.ts`), read off -an OBJECT def and ranked ABOVE the canonical `nameField` ADR-0079 Phase 2 made the -pointer (AGENTS.md Commandment #0.1). No producer can put it there: `@objectstack/spec`'s -object schema is a `strictObject` that answers `unrecognized_keys: ['primaryField']`, -and `ObjectSchema.create()` throws — which is why objectstack#6326 deleted the identical -read from two lint rules. A census across both repos found **zero** object payloads -carrying it (the only writers are three test fixtures), and `primaryField` appears in -**zero** files of the shipped `@objectstack/spec@17.2.0` dist against 68 for `nameField`. -Same shape as the undeclared `objectDef.titleField` read objectui#6531 measured and -#6557 removed. `DetailViewSchema.primaryField` is untouched and still honoured by -`DetailView`'s own header — it is a view key, and on a view it is legitimate. - -The literal `['name','full_name','title','subject','display_name']` walk — a NAME match -with no type check. - -**User-visible, deliberately.** Two shapes retitle: - -- An object with no conventional name field — `{ due_at: datetime, headline: text }` — - resolved to `null` here while every other surface titled the record `headline`. The - detail page now agrees, and the highlight strip stops repeating the H1 as its first - chip (the objectui#2548 duplication, which until now only DECLARED titles were spared). -- A field named `title` of a non-title-eligible type (e.g. a `select`) was chosen by - name alone and is now correctly skipped, so the strip no longer hides a chip on - account of a field the H1 never shows. - -**A third user-visible change, in the highlight strip's skip set.** It previously read -`primaryField`, `nameField` and `displayNameField` separately and skipped EVERY declared -one; it now skips the single field the shared ladder resolves. These differ on one input: -an object declaring `nameField` and the deprecated `displayNameField` alias to DIFFERENT -fields. The alias's field is no longer hidden from the strip — deliberately, because -`getRecordDisplayName` reads the same ladder and renders the WINNER's value, so the loser -is an ordinary field the H1 never shows and hiding it spent a strip slot for nothing. -This also makes the heuristic branch agree with the declared branch, which has only ever -filtered a single title field. - -`deriveHighlightFields` asks the retirement gate (objectui#4914) about the title field -**before** skipping it: an object whose only retired-typed field is the one that titles -it must still get its report, which skipping-first would have silenced. - -`ObjectDefLike.primaryField` stays DECLARED but is never read. The interface is -re-exported from this package's index and this package is published, so removing the -member would narrow a shipped `.d.ts` — a contract change owed its own card, not a rider -on a behaviour fix. diff --git a/.changeset/7292-turbo-owns-build-order.md b/.changeset/7292-turbo-owns-build-order.md deleted file mode 100644 index 7f2d617f5c..0000000000 --- a/.changeset/7292-turbo-owns-build-order.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Build orchestration only; no published behaviour changes. - -`@object-ui/components` and `@object-ui/site` each carried a hand-written -`prebuild` (and, for components, a `pretest` pointing at it) that listed the -workspace packages their build needed. That list is a second, unchecked copy of -the package's own `dependencies`, and the components one had drifted: it named -three packages while the closure turbo derives is seven, so -`pnpm --filter @object-ui/components build` failed inside `@object-ui/react` on -any tree where `@object-ui/i18n` and `@object-ui/data-objectstack` were not -already built. Both hooks are deleted; turbo's `build.dependsOn: ["^build"]` is -now the single source of build order, as it already was for every path CI takes. - -Trade-off, recorded here rather than left implicit: the bare per-package forms -`pnpm --filter @object-ui/components build` and `pnpm --filter @object-ui/components test` -no longer build their upstream packages first. They were never a supported entry -point on a clean tree — that is the defect this removes, not a capability it -takes away — but on a warm tree they used to be self-sufficient and now are not. -The supported forms are `turbo run build --filter=@object-ui/components` and -`turbo run test --filter=@object-ui/components` (turbo's `test` task also -declares `dependsOn: ["^build"]`), plus the repo-root `pnpm test`. Every -in-repo document and hint string that taught the direct form now teaches the -turbo form, and the root `site:build` script goes through turbo, matching what -CI already ran. diff --git a/.changeset/7295-chat-message-avatar-keys.md b/.changeset/7295-chat-message-avatar-keys.md deleted file mode 100644 index 96c162b710..0000000000 --- a/.changeset/7295-chat-message-avatar-keys.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Declare `avatar` and `avatarFallback` on `ChatMessage`, on both faces -(objectui#7295 — the residue of objectui#4424, whose `RuntimeOnlyMessageKeys` -named only the three keys API mode lifts out of the stream, never the two a -human author writes by hand). - -`packages/plugin-chatbot/src/index.tsx:173–178` reads -`message.avatar || userAvatarUrl` and -`message.avatarFallback || userAvatarFallback` (and the assistant twins), the -authoring-to-runtime seam spreads every unlisted key through -(`chatMessageAdapter.ts`, `...passthrough`), and the SDUI renderer feeds the -authored `messages[]` straight in — a per-message avatar override renders, is -documented, and no authoring-facing type declared it. `ChatMessage` in -`packages/types/src/complex.ts` has no index signature (objectui#5155, -deliberately — none is added here), so an author annotating -`ChatbotSchema.messages` was told a value that renders is an error (TS2353); the -zod mirror `ChatMessageSchema` is a plain strip-mode `z.object`, so the value -parsed green and was silently DROPPED from the parsed output. - -**patch, not minor: the accept set only widens toward what already renders.** -Both keys are optional; no document that validated before stops validating, and -no TypeScript value that compiled before stops compiling. Two verdicts move, -both measured on `446d93d` before the change and both toward the renderer's -behaviour: - -- The mirror now KEEPS an authored `avatar` / `avatarFallback` through - `ChatMessageSchema`, `ChatbotSchema` and `safeValidateSchema` — it stripped - them before. A consumer that renders the PARSED document (none in this - repository does; the renderer receives the authored one) sees the override for - the first time. -- The mirror now REFUSES a non-string value at the key (`avatar: 42` was - admitted-and-stripped before) — enforcement of the declared type, not a new - capability. - -Same precedent as `CheckboxSchema.wrapperClass` (objectui#6938) and the -objectui#6150 batch. `RuntimeOnlyMessageKeys` in `plugin-chatbot` is untouched; -`SeamChatMessage` inherits the two keys through its `ChatMessage` half. The -three example blocks on `content/docs/plugins/plugin-chatbot.mdx` that PR #7294 -left unannotated (`supportChat`, `salesBot`, `multiAgentChat`) are annotated -`ChatbotSchema` again. diff --git a/.changeset/7313-object-calendar-record-source.md b/.changeset/7313-object-calendar-record-source.md deleted file mode 100644 index 50d5f4eae2..0000000000 --- a/.changeset/7313-object-calendar-record-source.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ObjectCalendarSchema` declares the record-source ladder its renderer already -reads — `data`, `staticData`, `objectName` — on both faces, in the shape -objectui#6939 landed on `object-map` and `object-gantt` (objectui#7313). - -`ObjectCalendar` resolves its records through the shared ladder -(`resolveRecordSourceConfig` in `@object-ui/core`, called from -`plugin-calendar/src/ObjectCalendar.tsx`): `data` first, then `staticData`, -then `objectName`. The published TypeScript interface REQUIRED `objectName` and -declared neither `data` nor `staticData`; the published Zod mirror did the same. -So an `object-calendar` node authored on `staticData` — the route the plugin -page documents twice — rendered correctly and was refused by -`safeValidateSchema`, and could not be annotated with its own type -(`TS2741: Property 'objectName' is missing`). - -- `objectName` becomes optional on the TypeScript interface and on the mirror - in the same stroke; it stays the object-provider key. -- `data` (`ViewData` / `ViewDataSchema`) and `staticData` (`any[]`) are - declared on both faces, spelled exactly as `ObjectGanttSchema` spells them. -- The member ends in `requireRecordSource('object-calendar')`: a node - authoring NONE of the three is refused by name — one root-level issue, - `params.code = 'RECORD_SOURCE_REQUIRED'`, the map/gantt message naming - `data`, `staticData` and `objectName` — instead of by a missing `objectName`. - -**A widening.** A node authoring `staticData` or `data` without `objectName` -now validates (it always rendered — the read is -`resolveRecordSourceConfig(schema)`, keyed on the three). Every document that -validated before still validates: `objectName` alone still parses, an empty -one included, because presence is `!== undefined`. The one shape the -refinement refuses (none of the three) was refused before too, at -`objectName`. The two static-data examples in -`content/docs/plugins/plugin-calendar.mdx` are now annotated -`ObjectCalendarSchema` and compile under the doc-snippet gate. diff --git a/.changeset/7319-dead-schemarenderer-retired.md b/.changeset/7319-dead-schemarenderer-retired.md deleted file mode 100644 index 225cde7350..0000000000 --- a/.changeset/7319-dead-schemarenderer-retired.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/components': patch ---- - -Delete the second, dead `SchemaRenderer` in `packages/components/src` (objectui#7319). - -`packages/components/src/SchemaRenderer.tsx` was a 28-line component carrying the same -export name as the real renderer in `packages/react/src/SchemaRenderer.tsx`. Nothing -reached it: it is absent from the package barrel, no file in the repo imports it by any -form, and the package's `exports` map has no subpath that resolves to it. - -**No behaviour changes.** The file was never in the runtime bundle — two markers unique to -it appear in zero `dist/` files, while controls for barrel-exported symbols appear in four -each. Its only shipped footprint was a stray types-only `dist/SchemaRenderer.d.ts` with no -runtime module behind it, reachable through no specifier; the published tarball loses that -file, and no importable surface changes in either direction. Hence a patch, not a minor. - -**Why deleting beat keeping.** The copy is a trap, which is what the card's triage asked -whoever took it to settle. It evaluates no predicate at all: of the real renderer's six -visibility legs (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` / -`hiddenOn`) it consults exactly one, `hidden`, and by bare truthiness rather than -evaluation — so a node declaring `hiddenOn` is never hidden, and the two enablement legs -(`disabled` / `disabledOn`) are not read at all. It then spreads `{...schema}` raw, so -`disabled` would reach the widget as an unevaluated value, which is the precise inverse of -the real renderer's contract: evaluate the predicate, strip the raw key, forward only the -verdict. diff --git a/.changeset/7322-object-kanban-component-props.md b/.changeset/7322-object-kanban-component-props.md deleted file mode 100644 index 9cc7ff4177..0000000000 --- a/.changeset/7322-object-kanban-component-props.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@object-ui/plugin-kanban': minor ---- - -`ObjectKanbanComponentProps.schema` names both node types the component is registered for -(objectui#7322 item ②, following the objectui#5903 / #5018 land shape). - -`ObjectKanbanRenderer` is registered under two keys — `'object-kanban'` and `'kanban'` — -and the two keys have different declared node types: `ObjectKanbanSchema` (`type: -'object-kanban'`, `objectName` and `groupBy` required) and `KanbanSchema` (`type: -'kanban'`, both optional). The prop named `KanbanSchema` alone, so **no `object-kanban` -node was assignable to the component that renders it**, and the discriminants are disjoint -string literals, so no cast-free annotation existed for half the boards this component -serves. It is now the union of the two. - -## What settled it: the read set - -`ObjectKanban` reads thirteen keys off `schema`. Neither declaration covers them; the two -TOGETHER cover twelve, and each arm is load-bearing: - -- `objectName`, `groupBy`, `limit`, `cardFields` — declared on both; -- `columns`, `cardTitle`, `swimlaneField`, `grouping` — `KanbanSchema` only; -- `titleField` — `ObjectKanbanSchema` only (which is why that read was spelled - `(schema as any).titleField`); -- `data`, `bind`, `className` — `BaseSchema`; -- `filter` — declared by **neither** face, still riding `BaseSchema`'s index signature. - Measured and reported, **not** changed here: this card moves the prop, not the two - published schema faces. - -So naming `ObjectKanbanSchema` alone — the remedy the original card implied — would have -been wrong in the other direction: it drops four declared reads and the `'kanban'` -registration. - -## Not affected - -Widening a member of an exported prop type is additive: every caller that passed a -`KanbanSchema` still compiles, and the union claims exactly the accept set the registry -dispatches to this component — a third node type is still turned away. The runtime is -untouched; `ObjectKanbanRenderer` still takes `schema: any`, so no shape is turned away -there either (the objectui#5903 disposition, restated). The view-level `kanban.groupField` -alias, `BaseSchema`'s index signature, and `@object-ui/types` are all untouched. - -## Casts this removes - -Inside `ObjectKanban.tsx`, three schema-key reads drop their `as any`: `titleField` (two -sites, now honest because the `object-kanban` arm declares it) and `cardFields` / -`cardTitle` (already declared; the casts were redundant). `(schema as any).navigation` -**stays** — `navigation` is declared on neither face, so removing the cast would change -nothing but the spelling of an index-signature read. - -Four of the six in-package fixtures that mount an `object-kanban` board drop their -`as never` escape for a real `satisfies ObjectKanbanSchema`. The other two are static -boards (`columns` + inline `data`, no fetch) that author no `objectName`, which -`ObjectKanbanSchema` declares required — objectui#7780's subject; their casts stay, now -carrying the reason and the card number. diff --git a/.changeset/7322-object-kanban-group-by-limit.md b/.changeset/7322-object-kanban-group-by-limit.md deleted file mode 100644 index c386047d0c..0000000000 --- a/.changeset/7322-object-kanban-group-by-limit.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Breaking for authored metadata:** `ObjectKanbanSchema.groupField` is RETIRED -(objectui#7322, ADR-0049 enforce-or-remove), and the two keys the `object-kanban` -renderer actually reads — `groupBy` and `limit` — are now DECLARED and validated -on both published faces: the TypeScript interface in `objectql.ts` and the Zod -mirror in `zod/objectql.zod.ts`. - -**What was measured, on this branch's base (`53ded82b`).** -`packages/plugin-kanban/src/ObjectKanban.tsx` reads `schema.groupBy` at thirteen -sites (lane materialisation at `:601` / `:625` / `:640`, card moves at `:747` / -`:865`, and their effect deps) and `schema.limit` at two (`:264`, -`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`, and the effect deps at `:291`). -`groupField` has ZERO read sites anywhere under `packages/plugin-kanban/` — -against a control of those thirteen `groupBy` reads in the same query, so the -zero is a reading, not a blind grep. Yet the declaration REQUIRED `groupField` -and declared neither `groupBy` nor `limit`. Measured from source: the -documented, tested, working shape — `{ type: 'object-kanban', objectName, -groupBy, limit }` — failed `ObjectKanbanSchema.safeParse` and -`safeValidateSchema` on the missing `groupField`, and only ever reached the -renderer through `BaseSchema`'s `[key: string]: any` and `.passthrough()`, -admitted unexamined. An author who followed the declaration wrote `groupField` -and got a board that grouped nothing, with no diagnostic on either face. - -**Who is affected — an `object-kanban` node authoring `groupField`:** - -```json -{ "type": "object-kanban", - "objectName": "task", - "groupField": "status" } // ← validated, compiled, grouped nothing -``` - -now fails validation at `groupField` with: - -> RETIRED (objectui#7322) — `groupField` is not read by the object-kanban -> renderer; author `groupBy`. (The view-level `kanban.groupField` alias is -> unaffected.) - -and is refused at compile time: `groupField?: never`. The tombstone is -load-bearing rather than decorative — `BaseSchema` carries `[key: string]: any`, -so DELETING the member would let the retired spelling type-check green and go on -doing nothing. **Migration:** rename the key to `groupBy`; the value — the field -whose value places a record in a lane — is unchanged. - -**What now validates that did not before:** the documented shape. `groupBy` is -REQUIRED (the retired contract required a lane field too; the renderer's -`if (!schema.groupBy)` branches are defensive early-returns, not a lane-less -mode, and every documented and tested `object-kanban` node authors the key) and -must be a string; `limit` is an optional positive integer. A wrong-typed -`groupBy: 42` or `limit: "twenty"` — previously admitted unexamined — is now -refused at its own path. - -**Who is NOT affected.** The VIEW-LEVEL kanban config is untouched: -`kanban.groupField` there is a live legacy alias of the spec's `groupByField` -(`packages/core/src/utils/normalize-list-view.ts` maps it; `plugin-list`'s -`ListView` and `plugin-view`'s `ObjectView` still read it). `groupField` is dead -only on the `object-kanban` NODE. The declarative `kanban` node (`KanbanSchema`) -is untouched, `BaseSchema`'s unknown-key policy is byte-identical (an undeclared -key still passes through), and the renderer is unchanged — boards authored the -documented way rendered before and render now. One in-repo fixture authored -`groupField` on this node (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`); -it now authors `groupBy`. No doc snippet, catalog entry, skill or app in this -repository authored `groupField` on an `object-kanban` node. - -Graded `minor`, not `patch`: this narrows the accepted input set, which is -breaking for any author who wrote the retired key. It is not `major` per this -repo's fixed-group convention (objectui's own breaking changes ship as `minor`; -the group's major tracks `@objectstack` — AGENTS.md 版本号策略, mechanically -enforced by `scripts/check-changeset-no-major.mjs`). diff --git a/.changeset/7323-adapter-factory-return.md b/.changeset/7323-adapter-factory-return.md deleted file mode 100644 index 2f4925d08a..0000000000 --- a/.changeset/7323-adapter-factory-return.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -`createObjectStackAdapter` declares the adapter it returns, not the shared `DataSource` -interface (objectui#7323). - -The factory returned `new ObjectStackAdapter(config)` while declaring `DataSource`. -A wider value is assignable to a narrower annotation, so nothing ever failed to compile -— the loss was entirely on the reading side. Measured against the shipped -`dist/index.d.ts` with the doc-snippet gate's own compiler options, nine reads through -`ReturnType` failed with TS2339: `getClient`, -`getCacheStats`, `invalidateCache`, `clearCache`, `getConnectionState`, `isConnected`, -`onConnectionStateChange`, `onBatchProgress` and `setSystemCapabilities`. Eight of those -nine reads are on this package's README API Reference list, and four whole README -sections are built on them; the ninth measured read is the one the factory's own JSDoc -links to (`[ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}`). The README -list is itself **nine** adapter-only members, not eight — `connect()` is adapter-only -too and was documented all along; it simply was not one of the reads the card's -reproduction measured. So the file's own doc comment pointed the reader at a method its -declared return hid, and the two documented ways to obtain the same object — the factory -and `new ObjectStackAdapter(…)` — handed back different type surfaces. - -**What the declared return now is: the whole class, not those nine reads.** The nine -above are what the reproduction measured, not the size of this change. The factory's -declared return is now `ObjectStackAdapter` itself, so **every public member of the -class** is part of what the factory promises. Against `DataSource` that is **20** -members, not nine — `tsc`-computed as -`Exclude, keyof DataSource>`: `clearCache`, -`connect`, `getCacheStats`, `getCached`, `getClient`, `getConnectionState`, -`getDiscovery`, `getItems`, `invalidateCache`, `invalidateViewKeys`, `isConnected`, -`listImportMappings`, `onBatchProgress`, `onConnectionStateChange`, `onSaveAdvisory`, -`onWriteWarning`, `probeAppAccess`, `queryDataset`, `setSystemCapabilities`, -`updateDashboard`. The eleven past the documented nine were already in the shipped class -type — none is `@internal` or `@deprecated`, `stripInternal` is not set, and all were -already reachable through `new ObjectStackAdapter(…)` and through every -`ObjectStackAdapter`-typed seam in `@object-ui/react` and `app-shell` — so what widens -here is what the **factory declares**, not what the package ships. Two are escape-hatch -shaped and worth knowing before building on them: `getCached(key)` is a raw cache read, -and `getDiscovery()` reaches an internal property of the underlying `ObjectStackClient`. - -**Branch taken: A (widen the factory's declared return), and why.** The card offered -three. B — moving caching, connection state and batch progress onto `DataSource` — was -rejected because those are this adapter's concerns, not every data source's; every other -`DataSource` implementation would then declare members it does not have. C — documenting -a cast — teaches a cast around a declaration that is merely narrower than the value, -which is the opposite of `declared = enforced`. A is one line and makes declared match -shipped for every documented member at once. - -Two questions decided the shape and both were answered from the code before the diff. -`ObjectStackAdapter` was **already** exported from the package's only entry -(`src/index.ts`, tsup's single entry; the class is in the shipped `dist/index.d.ts` -export list, two pin tests assert the exported spelling, and `apps/console` re-exports it -by name) — so widening the return exports nothing by implication. And the narrow return -was **not** a deliberate swappability guarantee: no comment, ADR or test pinned it, and -the commit that added `autoReconnect` / `maxReconnectAttempts` / `reconnectDelay` to the -factory's own config bag left the members that observe those features off the factory's -declared return in the same change. - -**One caller shape breaks: a structural stand-in for the factory's return.** A -hand-written object literal annotated `ReturnType` no -longer satisfies that type, because it is now a class with private members (TS2740) — -annotate such a fake as `DataSource` instead, which is what it was standing in for. -Nothing else moves: a wider return is assignable to the narrower annotation, so -`const ds: DataSource = createObjectStackAdapter(…)` keeps compiling and keeps giving -the narrow surface to anyone who wants it. - -The README's note saying the page could not yet teach the factory's shape is removed, and -the four sections built on the adapter-only members (Metadata Caching, Connection State -Monitoring, Batch Operation Progress, Troubleshooting → Cache Issues) now continue from -Basic Setup's `createObjectStackAdapter(…)` call instead of declaring the class by hand. -The docs-site page `content/docs/utilities/data-objectstack.mdx` is corrected the same -way: its prose, its factory signature fragment and its "hold the class type to reach -these" section described the old narrow return, and its Mutations and Troubleshooting -examples told the reader to construct the class by hand to reach members the factory now -declares. `src/adapterFactoryReturn.types.test.ts` pins the card's TS2339 reproduction -inverted, -with two controls: the adapter-only members stay absent from `DataSource` (fires on -option B), and the widened return stays assignable to `DataSource` (swappability kept). diff --git a/.changeset/7324-plugin-form-nameable-parameter-types.md b/.changeset/7324-plugin-form-nameable-parameter-types.md deleted file mode 100644 index fccfe94b6b..0000000000 --- a/.changeset/7324-plugin-form-nameable-parameter-types.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/plugin-form': minor ---- - -Publish the parameter types of the entry's own exported functions, so a consumer can -name what it must pass (objectui#7324). - -`ChildObjectSchemaLike` and `FieldDefaultsSchemaLike` are now exported from -`@object-ui/plugin-form`. They are **type-only** additions — no runtime name is added -to the entry, which is pinned. - -**Why `minor`, not `patch`.** Nothing breaks and no behaviour changes, but two names -join the published surface of a published package. Additions are `minor` in this repo, -and a new public export is the kind of addition a consumer's lockfile-pinned range -should be able to see. - -**What was wrong.** Five exported derive functions (`deriveDetail`, `deriveColumns`, -`deriveFormFields`, `findRelationshipField`, `resolveInlineMode`) take a `childSchema`, -and the exported `omitServerResolvedDefaults` takes an `objectSchema` — and neither -parameter type reached the entry. A host with its own form renderer (the reason -objectui#6059 published `omitServerResolvedDefaults` in the first place) has to hold -that schema in a variable or a prop, and could not annotate it. Structural typing means -such a host still compiled by writing the shape out by hand, so the cost was not a hard -failure but a producer-owned shape restated in every consumer, invisible to every gate -until the producer's shape moved. The package README carried exactly that restatement, -and now imports the real name instead. - -**Renamed at the declaration site first, deliberately.** Both types were called -`ObjectSchemaLike`, in two files, and they are **not** the same type: the defaults one -pins the four field members its rule reads (`defaultValue`, `type`, `reference`, -`reference_to`), while the child one leaves a field value as `any` because the derive -functions read much more of it. Measured with `tsc`, they are mutually assignable -**only** through that `any` — replace it with `unknown` and the child → defaults -direction fails (TS2322) — so re-exporting either under the shared name would have put -a name on the public surface that already meant something else two files over, with -nothing in the name to say which. Neither old name was reachable from outside the -package (the package `exports` map has a single `.` entry and the entry never re-exported -them), so the rename is not a break for any consumer. - -**Not** `@object-ui/types`' `ObjectSchemaMetadata`: measured, it requires `name`, -requires a `type` on every field, and has no `reference_to` member — while -`isCurrentUserSeedField` honours both `reference` and `reference_to` on purpose. Adopting -it would have narrowed what these functions accept and dropped one of the two honoured -spellings, not widened anything. diff --git a/.changeset/7337-i18n-covered-specifier-flip.md b/.changeset/7337-i18n-covered-specifier-flip.md deleted file mode 100644 index d5e1eff94b..0000000000 --- a/.changeset/7337-i18n-covered-specifier-flip.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Finish the `@object-ui/i18n` slice of the `check-vi-mock-inherit` ratchet: the -last frozen factory (`DeclaredActionsBar.test.tsx`) obtains and spreads the real -module, and the specifier joins `COVERED_SPECIFIERS`, so the gate now holds the -population at zero instead of a pin file doing it by hand (objectui#7337). Test -and CI-script only; no published behaviour changes and no package is released by -this change. diff --git a/.changeset/7337-vi-mock-i18n-inherit.md b/.changeset/7337-vi-mock-i18n-inherit.md deleted file mode 100644 index 844a186fb0..0000000000 --- a/.changeset/7337-vi-mock-i18n-inherit.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Sweep the `vi.mock('@object-ui/i18n', …)` factories that hand-list the mock's -export surface over to the obtain-and-spread form, and fix the -`check-vi-mock-inherit` recogniser's nested-generic blind spot -(objectui#7337). Test and CI-script only; no package is released by this -change. diff --git a/.changeset/7340-docs-retired-handler-keys.md b/.changeset/7340-docs-retired-handler-keys.md deleted file mode 100644 index 565c5caa64..0000000000 --- a/.changeset/7340-docs-retired-handler-keys.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Docs-only: ten rows across eight `content/docs` pages still listed one of -objectui#6124's 22 RETIRED `on*` handler keys as a callable prop, after PR -#7339 turned those keys into `?: never` tombstones with named refusal arms on -the zod mirror. Seven pages have the rows removed; -`components/basic/button-group.mdx` keeps its `ButtonGroupButton.onClick` row -spelled `never` with the node-type pointer, because -`button-group-doc-surface-6347.test.ts` asserts set equality between that block -and the mirror's `.shape` and a refusal arm is still a key of that shape. A new -test-only pin -(`packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts`) -measures the retired population off `packages/types/src` and holds every doc row -whose `(interface, key)` pair resolves to a tombstone to the `never` spelling, -with six runtime-slot rows as the blanket-sweep control (objectui#7340). No -published package behaviour changes. diff --git a/.changeset/7344-handler-string-any-mirrors.md b/.changeset/7344-handler-string-any-mirrors.md deleted file mode 100644 index 47fb1a24f5..0000000000 --- a/.changeset/7344-handler-string-any-mirrors.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The eight `on*` handler keys PR #7339's census could not see now refuse by name -(objectui#7344 — the objectui#6182 ruling of 2026-08-25 that the handler-expression -string dialect is not a supported authoring form, executed in the objectui#6124 shape). - -**The accept set of published validators moves** (`@object-ui/types/zod`): - -- Four mirrors declared the string dialect (`z.string()`) — `AppActionSchema.onClick`, - `ReportBuilderSchema.onSave` / `.onCancel`, `DetailViewSchema.onBack` — so an authored - `onBack: 'goBack'` parsed green and then reached a slot that CALLS it - (`DetailView.handleBack`), throwing `onBack is not a function` at click. -- Three declared `z.any()` — `ActionSchema.onClick`, `DetailSchema.onBack`, - `CRUDDialogSchema.onClose` — wider than the callable the TypeScript face declares, so - any JSON value parsed green (the objectui#7069 direction). -- One, `CalendarViewSchema.onEventClick`, was `z.function()` in a multi-line spelling the - anchored census missed. - -All eight now carry `handlerKeyRefusal(key, disposition, label)`: an authored string, an -authored object and a live function are each refused at the key's own path with -`code: 'custom'` and a message that names the key, says why JSON cannot author it and -points at the node-type spelling. Nothing that used to be refused parses green. - -**The TypeScript face, measured per key** — a function type only where a runtime consumer -reads a function, else `?: never`: - -- Runtime slots (callable kept): `DetailViewSchema.onBack` (now `() => void`, the prop - `DetailView` invokes — it declared `string`), `DetailSchema.onBack`, `ActionSchema.onClick`, - `CalendarViewSchema.onEventClick`. -- Retired (`?: never`): `AppAction.onClick` (no reader touches `onClick`, on the action - or on `items[]` — `AppComponentSchema.actions[]` itself IS read, by - `@object-ui/runner`'s `LayoutRenderer`, so the array being unread was never the - reason this key is inert; corrected at objectui#7721), - `ReportBuilderSchema.onSave` / `.onCancel` (no `report-builder` renderer is registered), - `CRUDDialogSchema.onClose` (no `crud-dialog` renderer is registered). - -The three `views.zod.ts` `z.string()` keys that are event NAMES (`onViewChange`, the two -`onChange`, PR #6899) are untouched; their describe text says so and the new pin reads it. -`zod-mirror-parity.test.ts` records the three new runtime-slot drift rows; -`content/docs/core/app-schema.mdx` spells its `AppAction.onClick` row `never` and drops the -string example, the two edits the #7340 docs pin and `check:doc-snippets` require. diff --git a/.changeset/7345-master-detail-outcome-toast.md b/.changeset/7345-master-detail-outcome-toast.md deleted file mode 100644 index 692b9175e3..0000000000 --- a/.changeset/7345-master-detail-outcome-toast.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -A master-detail form no longer ends on a screen asserting both a failure and a success -(objectui#7345). - -`MasterDetailForm` raised its two save outcomes — `handleSaved`'s confirmation and -`handleError`'s refusal — under sonner's auto-generated ids, so nothing held a handle on -the previous attempt's toast. A save the server refused left its error toast on screen, -and when the user corrected the input and saved again inside that toast's lifetime the -confirmation landed *beside* the refusal, exactly the objectui#7252 defect on a renderer -that fix did not touch. - -Both outcomes now travel under one stable per-form id (`React.useId()`-scoped, the same -spelling the form renderer and the console's `FormPage` publish under), and each save -attempt retires the previous attempt's toast before it starts: - -- with no host `onSuccess` (SDUI / embedded hosts), the confirmation supersedes the - refusal instead of stacking beside it; -- with a host `onSuccess` (the console), where the built-in confirmation is deliberately - skipped, the dismissal is what retires the refusal — otherwise it stood over a save - that had succeeded. - -Toast durations are unchanged: this is about supersession, not lifetime. diff --git a/.changeset/7352-drill-down-config-mirror.md b/.changeset/7352-drill-down-config-mirror.md deleted file mode 100644 index b3d40dd2e0..0000000000 --- a/.changeset/7352-drill-down-config-mirror.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`DrillDownConfigSchema` is the zod mirror of `DrillDownConfig`, and both -declarations that carry `drillDown` reference it — `ChartSchema` -(`zod/data-display.zod.ts`) and `ObjectDataTableSchema` (`zod/objectql.zod.ts`) -— so the published validator under `@object-ui/types/zod` reads the key for the -first time (objectui#7352). - -`DrillDownConfig` has been declared on the TypeScript face since objectui#6058 -seeded the parity ledger, and objectui#6576 declared it on a second type. No zod -mirror existed, so under `BaseSchema`'s `.passthrough()` a -`drillDown: { enabled: 'yes' }` parsed green and rode through to a widget that -reads `enabled` as truthy — `declared !== enforced` on a published surface. - -Accept-set change on the published validator, stated plainly: - -- NARROWS: a `drillDown` whose declared key holds a value outside its declared - type (`enabled: 'yes'`, `mode: 'jump'`, `maxRows: '50'`, `report: 'pipeline'`) - is now refused BY NAME on a `chart` or `object-data-table` node, where it - previously rode through untouched. -- Unchanged: every value the TypeScript declares still validates, including the - `{ enabled: true }` / `{ enabled: true, mode: 'record' }` blocks the dashboard - renderer synthesises, `report`'s two structural forms, and an inline report's - extra keys (the declaration's index signature is `.catchall(z.unknown())`). -- Output shape, worth knowing before you read a parsed `drillDown.report`: the - member is a union, and its two arms differ in what they KEEP. The inline arm - carries the declaration's index signature as `.catchall(z.unknown())`, so extra - report keys survive; the named-reference arm is a plain object, so a value that - reaches it keeps only `name` (`{ name: 'x', columns: [] }` is accepted, and - parses to `{ name: 'x' }`). Both were accepted and unvalidated before, and - neither is refused now. -- Unchanged: `PivotTableSchema.drillDown` has no zod mirror at all, and - `DataTableSchema` declares the key on neither face — both are untouched here. -- New export on `@object-ui/types/zod`: `DrillDownConfigSchema`. - -`DrillDownConfigSchema` is deliberately NOT `@objectstack/spec/ui`'s -`ChartDrillDownSchema`: that object models the chart-only subset strictly and -refuses `mode` and `report` by name, both of which are live keys on the table / -pivot / metric widgets that share `DrillDownConfig`. diff --git a/.changeset/7363-objectql-union-arms.md b/.changeset/7363-objectql-union-arms.md deleted file mode 100644 index b660f6a8f2..0000000000 --- a/.changeset/7363-objectql-union-arms.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ObjectGallerySchema` and `ObjectDataTableSchema` are members of -`ObjectQLComponentSchema` on both faces — the TS union in `objectql.ts` and the -zod union in `zod/objectql.zod.ts` — so `AnyComponentSchema`, and with it -`validateSchema` / `safeValidateSchema` / `objectui validate`, has an arm for -`object-gallery` and `object-data-table` nodes (objectui#7363). - -PR #7355 (objectui#6576) minted both schemas beside the other `Object*Schema` -members and deliberately left the unions alone. Until now a document carrying -either node was refused as matching NO arm — exactly as before the schemas -existed — and a wrong-typed declared key on it could never be diagnosed by name. - -Accept-set change on the published validator, both directions, stated plainly: - -- WIDENS: a well-formed `object-gallery` / `object-data-table` node now - validates instead of being refused for having no arm. -- NARROWS in effect: a malformed one (`searchable: 'yes'`, `imageField: 42`, - `onRowClick` authored as JSON) is now refused BY NAME by the arm, where it was - previously refused only as "no arm matches". -- TS face: `Extract< ObjectQLComponentSchema, { type: 'object-gallery' } >` - resolves to `ObjectGallerySchema` instead of `never`; likewise for - `object-data-table`. `SchemaByType` has no in-repo consumer, and the wider - `AnySchema` union already carried `BaseSchema`, so nothing narrows there. - -No renderer behaviour changes; both nodes rendered before and render the same. diff --git a/.changeset/7368-studio-package-list-error-posture.md b/.changeset/7368-studio-package-list-error-posture.md deleted file mode 100644 index a929e84b36..0000000000 --- a/.changeset/7368-studio-package-list-error-posture.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio top bar: a failed package-list fetch is reported and told apart from the -other two states it used to look exactly like (objectui#7368). - -`PackageSwitcher` held the list as `PkgEntry[] | null` and caught the -`fetchPackages()` rejection into an empty block, so `null` meant both "still -loading" and "the fetch failed". The trigger rendered `current?.name ?? packageId`, -which collapses a third situation into the same pixels: a package whose producer -declared no name at all (`parsePackages` falls `name` back to the id). Any failure -of `GET /api/v1/packages` therefore left the Studio top bar printing the raw -reverse-domain package id — `app.b2r4` — with no toast, no console line and no -retry, forever, and the author had no way to tell whether to go fix the manifest or -to go retry. - -The switcher now records the failure beside the list and renders the three states -apart: `loading` keeps a spinner next to the id, `failed` adds a "Failed to load" -marker carrying the error on its tooltip and replaces the popover's forever-"Loading…" -line with the reason, and `loaded` leaves the id standing alone — where a bare id now -really does mean "this package declares no name". The two sibling `fetchPackages()` -callers on the same surface (the writability courtesy gate and the object-namespace -lookup) stop swallowing their rejections too; all three report on one shared sonner -id, so a single outage produces one toast rather than three. - -The degradation itself is deliberately kept — the switcher still navigates and one -503 does not take the top bar down — and no retry was added: retry policy (how many, -what backoff, what to show after giving up) is undecided, and retrying would delay -the moment the failure becomes visible, which is the opposite of what this fixes. diff --git a/.changeset/7371-ai-usage-indicator-weekly-reset.md b/.changeset/7371-ai-usage-indicator-weekly-reset.md deleted file mode 100644 index 37fa3b379a..0000000000 --- a/.changeset/7371-ai-usage-indicator-weekly-reset.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -Fix `AiUsageIndicator` to recognize the free plan's new `resetKind: 'weekly'` and its -`resetsAt` (objectui#7371, consumer of cloud PR #1852's rolling 7-day AI quota window). - -Before this change a `weekly` meter fell through to the component's unrecognized-kind -path and rendered no reset line at all — not a crash, but silently wrong information -next to a live progress ring. The indicator now shows "Resets in N days" (or "Resets in -N hours" once inside the final day, e.g. `console.ai.usage.resetsWeeklyHours`), computed -from the endpoint's `resetsAt`, in both languages via `@object-ui/i18n` -(`console.ai.usage.resetsWeeklyDays` / `resetsWeeklyHours`, real i18next plural families -with a base key so every locale pack resolves correctly, all ten packs translated). D5 is -preserved — no token count is ever rendered, only the days/hours until reset. - -Contract-first: `resetsAt` is read verbatim from the endpoint, never re-derived or -guessed client-side. A `weekly` meter with `resetsAt: null` (nothing counted yet in the -window) and any `resetKind` this build does not recognize both render no reset line — -fail-soft, not a crash or stale copy. - -`AiUsageResetKind` (`packages/app-shell/src/hooks/useAiUsage.ts`) gains the `'weekly'` -member; `resetsAt` was already `string | null` and needed no shape change. diff --git a/.changeset/7397-remove-duplicate-chart-primitives.md b/.changeset/7397-remove-duplicate-chart-primitives.md deleted file mode 100644 index 27dee2d1c6..0000000000 --- a/.changeset/7397-remove-duplicate-chart-primitives.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/components': minor ---- - -BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`, -`ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, -`ChartStyle` and the `ChartConfig` type — are removed. `@object-ui/plugin-charts` -is the single implementation (objectui#7397, maintainer ruling 2026-09-04). - -**Migration: import the chart primitives from `@object-ui/plugin-charts`.** - -(The bump is `minor` by this repo's release model — objectui's major is pinned to -the `@objectstack` family major, and its own breaking changes ship as `minor` with -the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This -paragraph is that spelling-out: the break below is real and consumer-visible.) - -- **What breaks, by specifier**: `import { ChartContainer, ChartTooltip, - ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } from - '@object-ui/components'` and `import type { ChartConfig } from - '@object-ui/components'` no longer resolve — TS2305 at build time, `undefined` at - runtime. They were reachable through two `export *` hops (`src/index.ts` → - `./ui` → `./chart`), so this is a real removal from the published surface, not a - tidy-up of dead code. -- **Not affected**: `ChartSkeleton` — the chart-area loading placeholder in - `src/custom/view-skeleton.tsx` — is a different symbol and stays exported. - `@objectstack/spec/ui` still owns the authored-chart `ChartConfig`; only the - per-series style map published from this package is gone. `@object-ui/plugin-charts` - calls its own map `ChartContainerConfig`, so the two names no longer collide. -- **Why the copy had to go rather than be fixed in place**: it duplicated - `packages/plugin-charts/src/ChartContainerImpl.tsx` and carried the - label-resolution hole objectui#7248 had already fixed there. `ChartLegendContent` - resolves a label as `config[nameKey || item.dataKey || 'value']` while rendering - the colour swatch unconditionally, so a legend entry whose config lookup misses - paints an anonymous coloured dot — on a scatter that reads as a data point drawn - outside the plot area, which is exactly how objectui#7248 was reported. Two copies - of one primitive is how a fixed bug returns; consumers importing from - `@object-ui/components` were getting the unfixed one. -- **Why not re-export the plugin's copy from here instead**: `@object-ui/plugin-charts` - depends on `@object-ui/components` (`workspace:*`), so the dependency direction - forbids it. -- **Consumer census**: zero in-repo importers, measured with a lit control — no file - under `apps/**`, `examples/**` or `packages/**` imported any of these names from - `@object-ui/components`. `plugin-charts` reaches its own copy by relative path. - The `hotcrm` and `cloud` repositories could not be read from the seat that made - this change (HTTP 403), so no claim is made about them. - -`packages/components/shadcn-components.json` records `chart` under -`customComponents` with `movedToPlugin: "@object-ui/plugin-charts"`, which is what -keeps `pnpm shadcn:update-all` from re-fetching the primitive and silently undoing -this. diff --git a/.changeset/7401-chart-family-from-type.md b/.changeset/7401-chart-family-from-type.md deleted file mode 100644 index 5c31009704..0000000000 --- a/.changeset/7401-chart-family-from-type.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/plugin-charts": patch ---- - -fix(plugin-charts): `pie-chart`, `donut-chart`, `radar-chart` and `scatter-chart` render as the family they name - -A schema written as `type: 'pie-chart'` (or `plugin-charts:pie-chart`, and likewise donut / radar / scatter) drew a **bar chart**. The four registrations declared their family as `defaultProps: { chartType: … }`, and nothing on the SDUI path has ever read a registration's `defaultProps` — so `ChartRenderer` resolved no family and `AdvancedChartImpl` fell to its `'bar'` default. Valid data, a confidently wrong picture, and no `data-chart-error` that could fire. - -`ChartRenderer` now derives the family from the schema's own `type`, through `normalizeChartSchema` — the package's single translation point, so the exported `normalizeChartSchema` answers what the runtime actually draws. An explicit `chartType` still wins, so `plugin-charts:chart` with `chartType: 'scatter'` is unchanged. - -The five inert `defaultProps: { chartType: … }` are removed with it rather than left beside a mechanism that works. Registration `defaultProps` remains unread on the SDUI path repo-wide; activating it generally is a separate, wider change and is not this one. - -⚠️ `scatter-chart` now genuinely reaches the scatter arm, so a two-series `scatter-chart` now renders the `scatter-multi-series` refusal it was always supposed to. diff --git a/.changeset/7402-scatter-ignores-compareto.md b/.changeset/7402-scatter-ignores-compareto.md deleted file mode 100644 index b4fe1206b2..0000000000 --- a/.changeset/7402-scatter-ignores-compareto.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/plugin-charts': minor -'@object-ui/plugin-dashboard': minor ---- - -`compareTo` on a `scatter` chart is no longer supported — scatter joins pie / donut / -funnel on the list of chart families that ignore it (objectui#7402, maintainer ruling -2026-09-03). - -**This removes a published capability, deliberately.** Until now a `chartType: 'scatter'` -chart (and the dashboard widget types `scatter` and `bubble`, which both render as one) -with `compareTo` set synthesised a muted "previous period" overlay series. It drew the -wrong picture: a scatter binds ONE measure, and the renderer reads y through the single -`YAxis dataKey={series[0].dataKey}`, so the overlay was plotted on the PRIMARY series' y -— "previous period" painted exactly on top of "current" (objectui#7194). - -Enforce-or-remove: rather than keep drawing that, the capability is removed until it can -be drawn honestly. Drawing a real second measure on a scatter needs the multi-measure -projection recorded as option A of objectui#7194, which is not built (zero authored -callers). **If and when that projection lands, `compareTo` on a scatter returns with -it** — it is the same missing mechanism, one payment. - -What changes for authors: - -- A `compareTo` on a scatter is now IGNORED rather than drawn. The primary series still - renders exactly as before — nothing refuses, nothing goes blank, and no comparison - query is issued on the inline chart path. -- No `__comparison` (inline chart) / `__compare` (dashboard) series is - appended for a scatter, so a compare-to scatter document also never reaches the - two-or-more-series scatter refusal being added under objectui#7194. -- Charts that keep the overlay: line, area, bar, horizontal-bar, combo. Charts that - ignore `compareTo`: pie, donut, funnel and — as of this change — scatter (and the - `bubble` widget type that renders as a scatter). - -Reachability at the time of the change: **0** authored scatter/bubble instances in-repo -across both spellings (control `"type": "bar"` fires at 5 example files); incidence in -deployed tenant metadata is not measurable from this repo. diff --git a/.changeset/7415-action-type-input-rename.md b/.changeset/7415-action-type-input-rename.md deleted file mode 100644 index 9c1923125e..0000000000 --- a/.changeset/7415-action-type-input-rename.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/components': minor ---- - -**Renamed:** the declared input carrying the action's execution type on `action:button` and -`action:icon` is now `actionType`. It used to be `type`, which collides with the SDUI -envelope's component discriminator. **No alias and no transition window** — a declaration -still spelling it `type` sets the discriminator, not the input. - -Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, -verbatim 「同意」). The objectui half; the pinned `sdui.manifest.json` in objectstack follows -as a separate change. - -**Why the old name could not stay.** `action:button` and `action:icon` were the only two of -the manifest's 57 components declaring an input named `type`, and on no tier could an author -actually set it: - -- **html tier** — `parse.ts` composes a node as `{ type: tag, ...props }`, props spread - last, so `type="api"` replaced the component discriminator and the node stopped resolving - to a component at all. `validate.ts` cannot report that: `type` is in `BASE_PROPS`, so it - is skipped before the declared-input check runs. Two mechanisms, one outcome, no - diagnostic. objectstack PR #14274 landed a refusal on this tier whose prescription - ("write the tag you meant") is wrong for exactly these two components. -- **react-page tier** — the wrapper stamps `type: tag` last and parks the author's value - under `specType` (objectui#2880), which neither action renderer reads. -- **JSON / `SchemaRenderer`** — the node's `type` is the component id, and the renderer - forwarded it to `ActionRunner` as the action type. `'action:button'` binds no handler and - no builtin: the click did nothing, with no error and no toast (the objectui#6306 shape). - -`actionType` is not a new vocabulary. It is the spelling this repo already used for this -exact value: `action:bar` renames the declared type as it spreads a member onto its child -(`type: componentType, actionType: action.type`), `ActionRunner.execute` resolves -`action.type || action.actionType || action.name`, and both renderers already read -`schema.actionType` FIRST. The rename makes the one working spelling the declared one. -It follows the resolution `page:tabs.type` got upstream for the same carrier collision -(retired in favour of `tabStyle`, objectstack#6776) rather than inventing a new convention. - -**Census of the authored corpus, measured before the rename, not assumed.** Across 4,797 -files in `examples/`, `apps/console/`, `content/docs/` and `packages/`: **zero** authored -nodes set the input. 517 JSON files (433 of them the schema catalog) contain 2,410 -`type`-bearing nodes and **no** `action:button` / `action:icon` node at all — control: 127 -plain `button` nodes on the same walk. 207 parsed fenced JSON blocks in md/mdx carry **5** -`action:button` nodes, all of them `{ type, label, icon?, action }` — the discriminator plus -the `action` channel, none setting an execution type — control: 28 plain `button` nodes, and -the count independently matches the corpus census already recorded in `SchemaRenderer.tsx` -("`action:button` (5 nodes)"). No docs page documents the input; no catalog entry uses the -components. So the rename breaks no authored document in this repo. - -**What changes for a consumer.** The renderers no longer fall back to `schema.type` when -`actionType` is absent — that fallback is the old spelling, and the ruling forbids an alias. -A node that declares neither now forwards `type: undefined`, and `ActionRunner` falls -through to its own `action.name` leg instead of being handed a component id. Declarations -composed by `action:bar`, `action:group` and `action:menu` are unaffected: they carry the -spec `ActionSchema.type` inside an `actions` array, which is a different surface and is -unchanged. - -Pinned by `action-type-input-html-tier.test.tsx` (the html tier authors the renamed input -end to end; the manifest built from the live registry accepts `actionType` and reports a -bogus prop as the control) and by the rewritten standalone rows in -`action-bar-member-type-resolution.test.tsx`, one of which now fails if the `|| schema.type` -leg is ever added back. diff --git a/.changeset/7421-gantt-tasklistwidth-tombstone.md b/.changeset/7421-gantt-tasklistwidth-tombstone.md deleted file mode 100644 index e772d8ebc2..0000000000 --- a/.changeset/7421-gantt-tasklistwidth-tombstone.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Removes the `taskListWidth_LEGACY_REMOVED` tombstone from `GanttView`, and records the fact -it was carrying at the site that owns it. - -The line was one `const` bound to the literal `null` inside the component body, left behind -by the finished task-list-width refactor. Measured on `origin/main` `a27d153c2`, the -identifier appeared exactly once in the whole repo — its own declaration — and zero times in -a read position; the same probe run returned 20 read sites for the live `taskListWidth` and -360 for `rowHeight`, so the zero is a reading and not a broken grep. Nothing in the platform -repo declared it either. It carried no state, no side effect and no contract surface. - -What it did carry was a false signal. A name spelling `_LEGACY_REMOVED`, sitting inside the -component a few hundred lines below the real width derivation, reads as a seam deliberately -retained for a reason recorded elsewhere, so the next reader goes looking for that reason. - -Its trailing comment held one fact worth keeping — that the width now comes from the -container `useResizeObserver` — and that fact was stated nowhere at the observer call site. -It moves there rather than dying with the line: the observed container width is named as the -source the auto-sized row height, base column width and task-list pane width all derive -from. No behaviour changes, and nothing published moves. diff --git a/.changeset/7429-expand-fls-seven-sites.md b/.changeset/7429-expand-fls-seven-sites.md deleted file mode 100644 index 16b04154fa..0000000000 --- a/.changeset/7429-expand-fls-seven-sites.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-tree': patch -'@object-ui/plugin-view': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-timeline': patch -'@object-ui/app-shell': patch ---- - -FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` -call sites (objectui#7429). - -objectui#7215 / PR #7229 gated the two projection sites in its scope -(`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more -(`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This -closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` -(the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, -`ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. - -**All seven pass no column list at all**, which makes every one of them the -sharp shape: `buildExpandFields` reads an absent column list as "no column -restriction" and falls back to **every declared relation on the object**, -denied ones included. So each of these components asked the server to resolve -the object's full relation set by default, not by configuration — the -ordinary shape of each surface, not a corner of it. - -**`PagePreview` is the one site where the judged principal is not the page's -eventual audience.** It calls the browser's own `fetch` with -`credentials: 'include'` rather than `DataSource.find`, so it runs under -whichever session is loading the Studio preview. Gating on that same session's -`usePermissions()` is still the correct principal: it is exactly the request -the browser is about to make, on its own credentials, regardless of who later -opens the published page. - -**Reproduced before it was fixed**, as a failing test per site (and, for the -two sites — `ObjectView`, `PagePreview` — where the gate was implemented -before its test was run red, a reverse-verification: the gate was reverted, -all four denial-and-set pins on each went red, and the two deferral/positive -control pins stayed green, before the gate was restored). - -**Grading, measured rather than assumed** — the same reading objectui#6898, -#7215 and #7230 recorded: against ObjectStack's own server this is -defence-in-depth, not a live disclosure. `plugin-security`'s -`FieldMasker.maskRecord` deletes every unreadable key from each returned row -and objectql's expand path writes the resolved record back under that same -key, so one statement removes the expanded object and the bare id alike; the -expansion sub-read is itself gated (the referenced object's full CRUD + RLS + -FLS treatment, objectstack#7626). It is load-bearing for any backend that does -not strip, and the client-request side is real regardless. - -**Nothing a permitted view did stops working.** The gate judges each site's -`buildExpandFields` OUTPUT, which contains only the object's declared -reference-bearing fields, so the "`checkField` answers false for an -undeclared key" trap cannot be reached. An unanswered permission policy -filters nothing. `buildExpandFields` itself is unchanged. - -`@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, -`plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth -one objectui#7429's own dependency count missed (it named four); `plugin-list` -and `app-shell` already had it. diff --git a/.changeset/7439-approvals-teardown-race.md b/.changeset/7439-approvals-teardown-race.md deleted file mode 100644 index b0bc5aad13..0000000000 --- a/.changeset/7439-approvals-teardown-race.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Close the network-escape window in -`RecordDetailView.approvalDeclaredActions.test.tsx` (objectui#7439): the `fetch` -double is now installed once for the whole file instead of being torn down by an -unconditional `afterEach`, which used to race the record page's own -`refreshAfter: true` approvals re-read. Test only; no package is released by -this change. diff --git a/.changeset/7443-datetime-compact-style.md b/.changeset/7443-datetime-compact-style.md deleted file mode 100644 index 70900b838d..0000000000 --- a/.changeset/7443-datetime-compact-style.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/fields': minor -'@object-ui/components': patch ---- - -One home for the `datetime` display convention (objectui#7443). - -`formatDateTime` gains a named `'compact'` style, selected through -`options.style` — the dense grid face, `7/4/2024 7:00 am` in `en-US` — which -`DateTimeCellRenderer` used to build from its own inlined `Intl` option bags. -The cell now reads `field.format` (it destructured `value` only, so a -`datetime` field could not reach the style vocabulary a `date` field has) and -renders through the shared function, and `data-table`'s `formatCellValue` -calls `formatDateTime` instead of a third, independently authored option bag. -Every existing cell without an authored `format`, and every cell authoring -`'compact'`, renders byte-identically; `'compact'` is today's face named and -rehoused, not a new one. A `datetime` field that authors any OTHER non-empty -`format` does change: the cell previously ignored `field` altogether and always -painted the compact face, and now anything other than `'compact'` selects the -verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in -`en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An -unrecognised value is neither rejected nor passed through; it silently lands on -that verbose face. No `datetime` field in this repository authors a `format`, so -no cell here moves — a consumer that authored one is the case this sentence is -for. Note that `format` has no declared value vocabulary to check a value -against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec` -carries one free-form `format?: string` on its shared field schema, described -"Format string (e.g. email, phone)" and accepting any string. `'compact'` is -therefore the only value with a defined `datetime` meaning, and every other -value means "the verbose face" by fallthrough rather than by design. - -Additive, no signature change: `formatDateTime(value, options?)` is unchanged -and `formatDateTime(v, { locale })` keeps meaning what it meant. -`DateDisplayOptions` gains an optional `style` key (read by `formatDateTime` -only; `formatDate` still takes its style positionally), and -`formatDateTimeCompactParts` is a new export of `@object-ui/core`, re-exported -by `@object-ui/fields`, returning the compact face as the two halves a grid -cell paints separately. `@object-ui/components` changes no rendered output — -the table's datetime cell is measured identical before and after in `en-US`, -`zh` and `de-DE`. diff --git a/.changeset/7454-calendar-allday-spelling.md b/.changeset/7454-calendar-allday-spelling.md deleted file mode 100644 index 96710753fe..0000000000 --- a/.changeset/7454-calendar-allday-spelling.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -fix(plugin-calendar): the all-day lane header reads the same string with or without an I18nProvider - -`CalendarView`'s `DEFAULT_TRANSLATIONS` table — the `defaults` map behind its -`createSafeTranslation` factory — spelled `calendar.allDay` as `all-day`, while -all ten locale packs carry the key and `en` spells it `All Day`. Since -`createSafeTranslation` serves that table only when no `I18nProvider` is -mounted, the same lane header rendered `all-day` in a standalone embed and -`All Day` inside the console. The table now matches the pack, so both paths -render one string. - -The table entry was the only one of the seven that disagreed with its `en` -value, and the packs predate it by three months — the drift was an oversight, -not a compact spelling chosen for the 56px gutter (both strings are seven -characters wide). - -Also removes the dead `t('calendar.allDay') === 'calendar.allDay'` ternary that -guarded the lane header: the factory's provider-less arm returns `defaults[key]` -before it could ever return the bare key, and with a provider the merged -resources always carry `calendar.allDay`, so its lowercase branch was -unreachable from either side. diff --git a/.changeset/7459-timeline-absent-date-axis-refusal.md b/.changeset/7459-timeline-absent-date-axis-refusal.md deleted file mode 100644 index 6488b748ee..0000000000 --- a/.changeset/7459-timeline-absent-date-axis-refusal.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/plugin-timeline': minor -'@object-ui/i18n': minor ---- - -`ObjectTimeline` refuses an undeclared date axis instead of inventing one -(objectui#7459). - -Steps ① and ② of the three-step sequence the maintainer ruled on objectui#7070 -(2026-09-01, 总监批 #28). House posture, on record with that ruling: -日期轴永不虚构 — a date axis is never fabricated. - -`ObjectTimeline` resolved its date axis through five declared bindings and then -closed the chain with a sixth rung that was a bare literal field name nobody -has ever declared. A name therefore ALWAYS resolved: for a view that declared -no axis, every record read a key its object does not carry, every event landed -in the "No date" bucket, and the screen read as a timeline that had been built -and simply had nothing in it. - -Two changes, shipped together because neither is observable alone: - -- **The renderer now refuses.** An object-bound timeline with no declared date - axis renders a diagnostic naming every binding it accepts — - `timeline.startDateField`, `timeline.dateField`, `mapping.date`, and the two - deprecated flat spellings — instead of a chart. The twin of `ObjectGantt`'s - "Gantt configuration required" screen, in the shape objectui#7070 settled. -- **The invented sixth rung is gone**, which is the only thing that makes the - refusal reachable. Added while the floor stood, it would have been dead code; - retired without the refusal, it would have produced exactly the silent - "No date" outcome the ruling rejects. - -**What changes for an author.** A view that declares a date axis is completely -unaffected — all five declared spellings resolve exactly as before, and a -timeline authored from literal `items` is never refused, since its items carry -their own dates and no field name is read for them. A view that declared no -axis anywhere, and was rendering an empty-looking timeline, now says so. - -⚠️ Both premises were RE-MEASURED on the current tree before anything was -edited, rather than taken from the card: the renderer had no absent-axis -refusal (against a live control term that fires in the same file), and the -floor was still present and still spelled as reported. The pairing itself is -pinned — the refusal cases go red the moment the floor returns, including one -whose records carry a column that happens to be named `date`, where a returned -floor renders a convincing timeline rather than an empty one. - -Refusal is distinguished from "renders an empty timeline" by asserting the -canvas is ABSENT, not merely event-free. The component's success surface is now -named (`data-testid="timeline-canvas"`) so that distinction can be measured; -every other terminal state of the component already named itself. - -Step ③ of the ruling — the `'created_at'` floors on the two plugin faces — -stays on objectui#7070 and is deliberately NOT in this change. diff --git a/.changeset/7466-timeline-colorfieldladder-settlement.md b/.changeset/7466-timeline-colorfieldladder-settlement.md deleted file mode 100644 index d41d8952b6..0000000000 --- a/.changeset/7466-timeline-colorfieldladder-settlement.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only: `colorsFor` in the timeline `colorField` ladder fixture now returns the -`lastItems` array its own readiness predicate accepted, instead of re-reading the -module-level global on the next line. No published behaviour changes — the shared -`createFieldColorResolver` ladder is untouched. diff --git a/.changeset/7477-listview-object-provider-fold.md b/.changeset/7477-listview-object-provider-fold.md deleted file mode 100644 index 39fac8ca2c..0000000000 --- a/.changeset/7477-listview-object-provider-fold.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/core': minor ---- - -ListView: fold `data={{ provider: 'object', object }}` onto `objectName`, and read the -author's view kind from `specType` / `type` (objectui#7477 — step 6 of #2890, released -by the maintainer's ruling B on objectstack#14791, 2026-09-03). - -**What was broken.** A react page bound the way the published `react-blocks` contract -recommends — - -```jsx - -``` - -— validated green against `@objectstack/spec` and then rendered an **empty grid** with no -diagnostic. Both halves of that binding were inert in the renderer: `ListView` read -`data.provider === 'object'` at zero sites (`'value'` and `'api'` are both live there, so -the gap was real and not a dead instrument), and it read `specType` — the slot the react -page tier parks an author's `type` in, because the SDUI envelope claims the `type` key -(ADR-0078) — at zero sites, so an absent `viewType` forced the view to `grid`. - -**What changed.** `normalizeListViewSchema` (`@object-ui/core`) gains two folds. Per -AGENTS.md #0.1 they live in the one documented normalizer — not as a seventh per-block -copy of the six sibling `data.object` reads, and not as a renderer-side `??` dual-read. - -- `data: { provider: 'object', object }` → `objectName`. The `object` provider is a - `strictObject` carrying exactly `{ provider, object }`, so `objectName` captures all of - it. Two deliberate departures from the folds around it, both narrowing: an - already-present `objectName` **wins** (the fold only fills a gap and can never re-point - a binding that already resolves), and `data` is **not** deleted — it has four - providers, `api`/`value` are read live, and the block is forwarded to child views whose - own `getDataConfig` reads `data` before `objectName`. -- the author's view kind is read from `specType`, then from a bare `type` when it names a - kind ListView draws (the component discriminator `'list-view'` never does) — the same - two legs, in the same order, as `normalizeChartSchema`'s chart-family read. An explicit - `viewType` still wins; this only fills the gap that used to resolve to `grid`, and a - kind ListView does not draw is left to that `grid` default rather than written through. - -**Accept behaviour widens.** Metadata that previously had no effect now binds a view: a -list view carrying an `object` data source, or an author `type`, renders differently -after this change than before. Nothing that renders today renders differently. No -authored spelling is removed here — `objectName` / `viewType` remain accepted; their -retirement is objectstack's, after this ships. diff --git a/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md b/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md deleted file mode 100644 index 4074b8a982..0000000000 --- a/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': patch -'@object-ui/components': patch -'@object-ui/layout': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-ai': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-charts': patch -'@object-ui/plugin-chatbot': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-designer': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-editor': patch -'@object-ui/plugin-form': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-markdown': patch -'@object-ui/plugin-report': patch -'@object-ui/plugin-timeline': patch -'@object-ui/plugin-tree': patch -'@object-ui/plugin-view': patch ---- - -**Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and -`ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; -maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). -They are the three keys the manifest serializer does not forward, and nothing read them on any -publication or consumption path. - -No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s -serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, -`type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three -never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type -has no slot for them; the registry's data-source seam reads `name` only; and neither the designer -nor the app-shell inspectors consult registry `inputs` at all. A structural census over every -`inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` -951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on -nearly every registration, read by nothing. - -FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on -the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an -undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 -authored values in silence. The tombstone is what makes the refusal loud and by name. - -- `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. - Migration: delete the key. An input is identified by its `name` on every path that reaches it; - nothing ever rendered a label for it. -- `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the - key. The renderer's own fallback read IS the default; tell the author about it in `description`, - which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, - since nothing reads the value.) -- `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. - No designer surface ever hid an "advanced" input; there is nothing to write instead. - -The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one -is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` -(`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with -`code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message -(one string, both channels). Pinned in -`packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a -tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. - -Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a -`ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses -RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone -`ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule -requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized -`ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` -drops its `label`. The patch entries on the other packages record exactly that: their registrations -stop authoring inert keys, with no runtime or published-manifest change. - -The nine test files that read `defaultValue` off a registration were re-pinned against the -renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the -declaration that went away; two assertions that only restated the shadow default were dropped with -the reason on the line. - -The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys -is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop -into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / -`defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has -ruled on that face; that it now has no reader either is recorded as objectui#7911. diff --git a/.changeset/7507-non-grid-ceiling-correctives.md b/.changeset/7507-non-grid-ceiling-correctives.md deleted file mode 100644 index 1d72d6d1f1..0000000000 --- a/.changeset/7507-non-grid-ceiling-correctives.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507), -all of them inside a change that has not been released yet — so nothing here is -a user-visible fix, and the ceiling's own changeset carries the release note. - -- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from - `patch` to `minor` (they carry the behaviour break), the example footnote is - replaced with the copy that actually renders — no thousands separators, since - the i18next config declares no `format` and the provider-less path - interpolates through `String(v)` — and the export list names - `NonGridCeilingResult`. -- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a - footnote raised by this component's own truncated fetch cannot outlive the - rows it described. Latent today: the only host that passes `data` passes it - from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`. -- The map and calendar ceiling pins now assert the row count handed to the - view, not only `$top` and the footnote; the four pins' reverse-verification - docblocks are rewritten to the mechanism that was measured rather than the - one that was predicted. -- Comment-only: the false "the only package all four already depend on" - rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom" - note in the ten locale packs. diff --git a/.changeset/7509-dashboard-root-title-retired.md b/.changeset/7509-dashboard-root-title-retired.md deleted file mode 100644 index 93994209bf..0000000000 --- a/.changeset/7509-dashboard-root-title-retired.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-dashboard': minor -'@object-ui/plugin-designer': minor ---- - -Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, -maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). - -**What changes for an operator.** A stored dashboard whose header came from a legacy -root `title` now shows its `label`. `label` is the only header source, then the raw -`name`. - -Per surface: - -- Console dashboard page (`DashboardView`) — header falls to `label`, then `name`. -- Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document - with no `label` now shows no header title at all. -- The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to - `label`, then the generic `Dashboard`. -- Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage` - heading) — both fall to `label`, then `name` / the generic heading. - -**Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name** -(`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no -authored dashboard can acquire the key, and what retires is compatibility with documents -stored before that refusal existed. Until now five surfaces read the legacy spelling -independently, which meant a legacy document could show one header in the console and a -different one in the designer. One spelling now answers everywhere. - -**Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard -already carries it and needs no change — it simply starts showing that `label` instead of -the legacy `title`. A document carrying `title` and no `label` was already invalid; give -it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found -9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'` -component examples that declare no `header`, so none of them rendered a header title -either before or after. - -**Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared** -key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings, -the designer's widget-title input and its per-locale write path all behave exactly as -before. Root and widget arms were separated by receiver, and the retirement's pins carry -widget-level controls on every surface for that reason. diff --git a/.changeset/7509-dashboardview-title-comment-corrected.md b/.changeset/7509-dashboardview-title-comment-corrected.md deleted file mode 100644 index 9d9111956e..0000000000 --- a/.changeset/7509-dashboardview-title-comment-corrected.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Comment-only correction in `app-shell`'s `DashboardView` header block — no type, -schema, export or runtime path is touched, so nothing releases. - -The block over the header's title fallback claimed "Per @objectstack/spec, -`DashboardSchema.title` is 'the dashboard title displayed in the header'". -Measured on the installed `@objectstack/spec@17.2.0`, `DashboardSchema` refuses -`title` **by name** — `unrecognized_keys(title)` at the document root, against a -spec-legal control (`{ name, label, widgets }`) that parses — and its 20 keys -spell the display name `label`, not `title`. `header` declares `showTitle` / -`showDescription` / `actions` only, so it toggles a title and never carries one; -`{ header: { title } }` is refused too (`unrecognized_keys(title)@header`). - -The old wording made `title` read as authorable dashboard metadata, which is the -expensive way to learn it is not: the save route refuses the whole body with -`422 INVALID_METADATA` / `unrecognized_keys` before persistence, so an author -who follows the comment gets no header and a rejected document. The replacement -says what `title` actually is — the legacy objectui spelling, the same -legacy-then-canonical pair `DashboardRenderer` reads — and states the 422, which -is the fact the next reader needs. - -It also corrects a second reading the old block invited: `previewSchema` is not -a host-supplied preview channel. `DashboardView` takes no such prop; the value -is the view's own widget-pruned copy of `dashboard`, so both arms of `headerSrc` -read the same stored document. diff --git a/.changeset/7512-ganttview-unread-residues.md b/.changeset/7512-ganttview-unread-residues.md deleted file mode 100644 index 04764dea59..0000000000 --- a/.changeset/7512-ganttview-unread-residues.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Internal cleanup in `@object-ui/plugin-gantt`: `GanttView.tsx` dropped three -declarations (four bindings) that nothing in the tree ever read. - -- `const HEADER_HEIGHT = 50;` and `const COLUMN_WIDTH = 100;` — module-level - constants with zero readers repo-wide. Both were lazy: unread module bindings - cost nothing at runtime, so removing them changes no behaviour at all. -- `const [currentDate, setCurrentDate] = React.useState(() => tzShift.now());` — - a different class of residue. Unlike the two constants this one was **live**: - every mount allocated a state slot and ran the `tzShift.now()` initializer, - even though neither the value nor the setter was ever referenced. Removing it - has a real (if tiny) runtime effect, and no observable one — nothing rendered - from it and nothing could set it, so no output, prop, or timing a consumer can - see changes. - -No published symbol moves: none of the four were exported, `COLUMN_WIDTH` is not -the same binding as the local `const COLUMN_WIDTH = 110` that three sibling test -files declare for themselves, and `tzShift` keeps 27 other readers in the file so -its import stays live. Nothing to release — declared as a no-bump change. diff --git a/.changeset/7515-unreferenced-source-gate.md b/.changeset/7515-unreferenced-source-gate.md deleted file mode 100644 index f9bba023c3..0000000000 --- a/.changeset/7515-unreferenced-source-gate.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@object-ui/components": patch ---- - -Remove `src/ui/toast.tsx`, an unreferenced primitive, and the dependency only it imported - -The file was reachable from nothing: no importer anywhere under -`packages/components/src`, and `ui/index.ts` never carried it, so the barrel's -`export * from './ui'` did not reach it either. It shipped all the same — -`dist/ui/toast.d.ts` was in the published tarball — while contributing nothing to -`dist/index.js` and nothing to the package's export surface. `ui/sonner.tsx` -(`Toaster`) is the live implementation and is unaffected. - -`@radix-ui/react-toast` is dropped from `dependencies` in the same change: the -removed file was its only importer anywhere in the repository, so it would -otherwise have stayed a declared dependency of every install with nothing to -resolve it. - -No exported name changes. A consumer who was resolving `@radix-ui/react-toast` -through this package's dependency was relying on hoisting rather than on a -declaration, and should declare it directly. diff --git a/.changeset/7519-retire-mobile-residue-types.md b/.changeset/7519-retire-mobile-residue-types.md deleted file mode 100644 index 4c5ad7afb2..0000000000 --- a/.changeset/7519-retire-mobile-residue-types.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/mobile': minor ---- - -**Removes two published exports.** Retire the `MobileResponsiveConfig` and -`GestureConfig` types (objectui#7519, ADR-0049 enforce-or-remove). Both names -are deleted from `@object-ui/types` and from `@object-ui/mobile`, which -re-exported them — after this release `import type { MobileResponsiveConfig }` -or `import type { GestureConfig }` from either package is a compile error, not a -deprecation warning. - -Each had exactly one consumer: the `responsive` and `gestures` members of -`MobileComponentConfig`, which objectui#5942 retired. Re-measured on current -`main` before anything was deleted, each was a declaration plus the two barrel -re-exports and nothing else — no type mounted either, nothing extended, -annotated, cast to or imported them outside the barrels, and the example apps -and the `objectstack` sibling checkout had zero authors. A value written against -either could not reach a renderer or a handler by any path. That is the same -declared-surface-with-no-consumption-path shape as `MobileComponentConfig` -itself and `MobileOverrides` (objectui#4919) before it, one level down. - -Removed outright rather than kept as `?: never` tombstones, measured against -this package's two-prong discriminator (a tombstone steers authors to a named -live replacement key, or keeps loud a key the docs taught as working). Prong 1: -neither has a replacement key — the behaviour they named lives in hooks, and -`SpecGestureConfig` is a different contract, not a successor. Prong 2: the only -release-note lines naming either are the objectstack#4115 rename-ledger rows -and, for `GestureConfig`, the objectui#3363 reclaim note; none taught a -renderer or dispatcher reading them, and no member carried a published -`@default` (contrast `triggerIcon`, tombstoned by objectui#7654 on exactly that -evidence). Structurally there is also no silent-strip hazard for a tombstone to -guard: whole interfaces go, nothing ever parsed them, and the mobile module has -never had a `zod/` twin to host a `retirementTombstone()`. The compiler was the -only channel these names ever had, and the refusal now lives there. - -## Upgrading - -**No behaviour changes and there is nothing to migrate at runtime.** An object -authored against either type did nothing before and does nothing now; what -changes is that the contract no longer claims otherwise, so the mistake surfaces -at authoring time instead of silently type-checking. - -- **You imported a type only** (the only thing that was possible — nothing - accepted either as a value): delete the import. If you kept a local object - annotated with it, drop the annotation; it was never passed anywhere that read - it. -- **You wanted per-breakpoint layout:** it exists and is not being retired — - `useResponsive` / `ResponsiveContainer` / `useBreakpoint` in - `@object-ui/mobile`. `ResponsiveValue` and `BreakpointName` stay exported from - both packages. -- **You wanted to bind a gesture to a handler:** `useGesture` in - `@object-ui/mobile` takes `{ type: GestureType, onGesture, threshold?, - longPressDuration?, enabled? }`. `GestureType` and `GestureContext` stay - exported from both packages. -- **You want a declarative mobile config surface:** that re-enters deliberately - as designed product surface on its own card, with the renderer that reads it - landing in the same change as the declaration — not by restoring these - declarations. - -**Do not follow the compiler's suggestion for `GestureConfig`.** Measured against -the built declarations: `import type { GestureConfig }` from either package now -fails as TS2724 with `Did you mean 'SpecGestureConfig'?`. That is a lexical -near-match, not a migration target. `SpecGestureConfig` is the retired -`@objectstack/spec` `ui/touch` **tuning** record (`{ type, label, enabled, -swipe, pinch, longPress }`) that `useSpecGesture` reads; it has no `action` -member and does not bind a gesture to anything. `MobileResponsiveConfig` fails -as a plain TS2305 with no suggestion from either package. - -Marked `minor`, not `major`, per this repo's version-alignment rule (AGENTS.md -版本号策略), which reserves `major` for following `@objectstack` across a major — -the same classification objectui#5942 and objectui#4919 used for identically -breaking type removals. **Breaking for TypeScript consumers of the two names -only.** The in-repo consumer count is zero; consumers outside this repository -that import either name from either package are not visible from here, which is -why this entry is graded on the published-surface change and not on that count. diff --git a/.changeset/7521-timeline-colorfieldladder-race.md b/.changeset/7521-timeline-colorfieldladder-race.md deleted file mode 100644 index 6c3c03a931..0000000000 --- a/.changeset/7521-timeline-colorfieldladder-race.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Removes the shared-`lastItems` race in -`packages/plugin-timeline/src/ObjectTimeline.colorFieldLadder-7243.test.tsx`. No -runtime, type, schema or exported-symbol change — the diff is one test file, so -this declares "not published" rather than a version bump. - -The test helper `colorsFor` left every render mounted and waited on -`lastItems.length` alone. React Testing Library's auto-cleanup runs in -`afterEach`, never between two renders inside one `it`, so the two `it`s that -call `colorsFor` twice had two `ObjectTimeline`s alive writing one module-level -`lastItems` — and a length-only predicate cannot say which of them wrote it. - -The late write was structural rather than incidental: `ObjectTimeline`'s data -effect lists `objectDef` in its dependencies and a separate metadata fetch sets -`objectDef`, so every mount issues two `find()` calls. The second is still in -flight when the predicate goes green, which left each `colorsFor` returning with -a write queued against a component nothing ever unmounted. Under load that write -landed after the next call had reset `lastItems`, and rung 2 read the previous -rung's colour back out. - -Because the failure was load-dependent it could kick any PR that happened to be -in the merge queue, not just changes to `plugin-timeline`. diff --git a/.changeset/7530-predicate-envelope-declared.md b/.changeset/7530-predicate-envelope-declared.md deleted file mode 100644 index 2023934eb2..0000000000 --- a/.changeset/7530-predicate-envelope-declared.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`BaseSchema.visible` / `.hidden` / `.disabled` now declare the CEL envelope object the renderer already evaluates, as one named wire type** (objectui#7530, maintainer ruling 2026-09-04, option A). - -Each of the three keys goes from `boolean | string` to `boolean | ExpressionWire` on both faces, where `ExpressionWire` is `string | { dialect?: string; source: string }` — the exact string-or-envelope union `FormField.visibleWhen` and its `*When` / `*On` siblings already carried, and the exact accept set of `@object-ui/core`'s `toPredicateInput` / `hasDeclaredPredicate`. The Zod mirror's `z.union([z.boolean(), z.string()])` becomes `z.union([z.boolean(), ExpressionWireSchema])` on all three. - -Two names are new on the published surface: - -- `ExpressionWire` (type, main entry) — the TypeScript wire union, in `packages/types/src/expression.ts`. -- `ExpressionWireSchema` (`@object-ui/types/zod`) — its runtime twin, hoisted out of `zod/form.zod.ts` (where it was module-private) into `zod/expression.zod.ts` and imported by both `base.zod.ts` and `form.zod.ts`. One envelope type, reused by reference; no second spelling. - -This is a **widening**, not a replacement, and the renderer's behaviour is untouched: `SchemaRenderer`'s `shouldHide` / `shouldDisable` chains already routed all three keys through core's one definition of "declared" and evaluated the value, and the envelope was already pinned as working on `hidden` and `disabled` — through a `Record` cast, because no key declared it. Measured before this change, `BaseSchema.safeParse({ type, hidden: { dialect: 'cel', source: 'true' } })` returned `success: false` (`invalid_union` at path `hidden`) while the identical envelope on `FormField.visibleWhen` parsed one file over; that is the gap this closes, on all three keys at once. Every boolean and string value keeps parsing and keeps type-checking unchanged. `dialect` is optional and unconstrained on the wire because the runtime reads it that way (only `'cel'` keeps its envelope on the canonical engine; anything else is unwrapped onto the legacy path). - -Not changed: `hasDeclaredPredicate` (no per-key branch — option B was rejected), the `*On` / `visibleWhen` sibling keys, and `ActionSchema.condition`, which already declared the envelope inline. - -Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). diff --git a/.changeset/7540-option-editor-key-loss.md b/.changeset/7540-option-editor-key-loss.md deleted file mode 100644 index 63d7849061..0000000000 --- a/.changeset/7540-option-editor-key-loss.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the object-field options editor no longer drops `default` and `visibleWhen` on save - -Opening a picklist field in the metadata-admin designer, editing any option and -saving used to write the option back without its `default` or `visibleWhen` -key. It was not a validation failure — the payload stayed perfectly valid, just -smaller than what the author wrote — so the loss was silent, and it took a -picklist default with it: `default` is `enforce` on the object-field face, and -the engine seeds the insert path from the option holding it. - -The loss started in the reader, not the writer. `readOptions` projected each -authored option down to `value` / `label` / `color`, so both keys were already -gone before `patchOptions` ran. `readOptions` now carries the keys the editor -has no control for and `patchOptions` writes them back, which also protects any -option key the spec accepts later. No authoring UI changed. diff --git a/.changeset/7544-listview-chart-capability-gate.md b/.changeset/7544-listview-chart-capability-gate.md deleted file mode 100644 index 39cbf0c204..0000000000 --- a/.changeset/7544-listview-chart-capability-gate.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -The list view's capability gate now resolves `chart` (objectui#7544). - -A `grid` list view that declared a complete `chart:` block and whitelisted -`appearance.allowedVisualizations: ['grid', 'chart']` was never offered the Chart -toggle. `availableViews` builds the resolvable set from each visualization's binding -and intersects it with the whitelist (ADR-0047, whitelist ∩ resolvable); seven -visualizations had a capability check there — kanban, gallery, calendar, timeline, -gantt, map, tree — and `chart` had none, so the author's own whitelist was filtered -down to nothing and the view fell back to `['grid']`. `chart` entered the offered set -only through the "always allow switching back to the schema's own viewType" leg, i.e. -only when the view was already `viewType: 'chart'`. - -Both halves were spec-legal and authorable the whole time: `chart` is a -`VisualizationTypeSchema` switcher target and `chart:` is a list-view key -(`ListChartConfigSchema`). Only the gate never asked. This is `map`'s objectui#5042 one -visualization over, and is fixed the same way. - -**What now resolves.** A chart block that binds to names the author wrote: the ADR-0021 -shape (`dataset` plus at least one measure in `values`), or the legacy inline shape (a -declared category — `xAxisField` / `categoryField` — together with a declared measure — -`yAxisFields[0]` / `valueField`), in the view-level `chart` block or the legacy -`options.chart` bag. A block that declares no binding stays unoffered: reaching the -renderer with nothing declared lands on the legacy branch's invented `'name'` / -`'value'` floor, and the switcher must not offer a route into it. That floor itself is -unchanged here (objectui#7547). - -The gate and the `case 'chart'` render branch now read ONE resolver -(`resolveListChartBinding`) rather than two copies of the condition, so they cannot -drift about what a usable chart block is. `schema.chart` also joins the memo's -dependency array, so a block that arrives on a later render is seen. diff --git a/.changeset/7546-chart-series-keys-declared.md b/.changeset/7546-chart-series-keys-declared.md deleted file mode 100644 index 123b480a0f..0000000000 --- a/.changeset/7546-chart-series-keys-declared.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ChartDataSeriesSchema` (and its TS twin `ChartDataSeries`) declares the six series keys the -renderer reads — `label`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis` — which the -non-strict Zod object had been **stripping in silence** while `safeParse` reported success -(objectui#7546, `domain:ui` PM ruling: measure per key, declare what is live, report the rest). - -⚠️ Shipped as `minor`, not `patch`. The declared value domains are the read's own, so the accept -set widens toward what already renders — but one document class that validated before now -**refuses**: a series carrying one of these keys with a value the renderer drops in silence -(`variant: 'bogus'`, `yAxis: 'top'`, `opacity: '0.4'` / `Infinity`, a non-string `stack` / -`dashArray`, a non-string non-map `label`) — and, separately, `variant: 'current'`, which the -renderer does NOT drop: the normalizer keeps that renderer-internal spelling and draws it exactly as -`primary`, but it is not a member of the published pair, so it now refuses at `variant` (below). -Such a document draws a chart today — the normalizer ignores the bad value, or honours `current` -— so this is a narrowing away from something that renders, which is the -distinction objectui#6939's grading language turns on, and it takes the level objectui#6896 and -objectui#7113 set for the same transition in this same file. This repository's `major` is a -cross-repo pin to `@objectstack`'s major, not a severity dial; the change is announced here. - -## The defect, measured - -`ChartDataSeriesSchema` is a non-strict `z.object` — not `.passthrough()` like `BaseSchema` — so an -undeclared key is removed, not kept. Reproduced red on `origin/main` `a472b071` before the change: - -``` -input : { name, label, stack, yAxis, opacity, dashArray, variant } -parse : success = true -output: { name } -``` - -Every one of the six is read by `normalizeSeries` (`@object-ui/plugin-charts`, -`normalizeChartSchema.ts:242-255`) and does real work in `AdvancedChartImpl.tsx` — `label` names -the legend entry, `variant === 'comparison'` selects the muted overlay, `opacity` / `dashArray` set -stroke and fill, `stack` becomes Recharts' `stackId`, `yAxis` binds the secondary axis. Any consumer -of the parse output — `objectui check` / `objectui validate` via `safeValidateSchema`, a JSON -schema derived from the mirror, or any pipeline that keeps `parse()`'s result — lost them outright. - -## Per-key liveness, not a blanket declare - -"The renderer reads it" was ruled insufficient (a read leg can sit on a value nothing produces — -objectui#7642), so each key was measured on producers, real work in the reader, and consumer -surprise, with a lit control on every count. The six are `@objectstack/spec`'s own -`ChartSeriesSchema` members under the same names and value domains; this node's `series` -accepts the spec shape by design; in-repo producers write them onto `type: 'chart'` nodes -(`DashboardRenderer`, `ObjectChart`, `DatasetWidget`, `core/utils/chart-presentation`); and -the `variant` / `yAxis` narrowings are design intent the reader already enforces. - -**`chartType` — the seventh key the review found — is deliberately NOT declared.** It is the first -limb of `str(raw.chartType) ?? str(raw.type)`, but it is the renderer's *internal* spelling of the -declared `type`; the spec's `ChartSeriesSchema` lists it as an alias of `type` and refuses it by -name; and zero documents, fixtures, catalog entries or designer inputs write it on this face -(controls lit). Declaring it would mint a second writable name for one override. It is reported -for its own card; the mirror still strips it, and the pin test holds that gap visible. - -## FROM → TO - -```ts -// ChartDataSeries — all optional, all additive on the TS face -+ label?: string | I18nLabel; // spec I18nLabel: string | inline locale map -+ variant?: 'primary' | 'comparison'; // the spec's own pair -+ opacity?: number; // finite; NaN / Infinity / strings refused (the spec's 0–1 bound is not enforced — the read's domain) -+ dashArray?: string; -+ stack?: string; -+ yAxis?: 'left' | 'right'; -``` - -`variant` is the spec's own pair. The normalizer also tolerates a third spelling, `current`, but that -is the renderer's internal default — written only by the compare-to producers (`ObjectChart`, -`DatasetWidget`) onto internal-shape arrays that never pass this mirror, and by nothing an author -writes (docs, fixtures, designer inputs: 0, controls lit) — so it is not a member here: declaring it -would have fossilised a renderer-side tolerance into a second contract. The normalizer's tolerance -is unchanged; objectui#7682 owns that decision. - -## Unchanged, deliberately - -The object stays non-strict — a truly undeclared key is still stripped, exactly as -`chart-inline-data-retired.test.ts` pins; this change declares what is read, it does not close -the object. The `data` tombstone (objectui#6896) and the at-least-one-binding refinement -(objectui#6939 / #7113) are untouched. No reader changed. - -Pinned in `packages/types/src/__tests__/chart-series-keys-7546.test.ts` — the card's fixture -surviving byte-for-byte, each key on the mirror's own `.shape`, each value domain refusing at its -own path, the TS face in lockstep, and the `chartType` gap. diff --git a/.changeset/7547-forced-path-invented-bindings.md b/.changeset/7547-forced-path-invented-bindings.md deleted file mode 100644 index 81375e7479..0000000000 --- a/.changeset/7547-forced-path-invented-bindings.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-list': minor -'@object-ui/app-shell': minor ---- - -Stop inventing a gallery cover binding, and offer the view switcher only when it -can switch (objectui#7547). - -**The object page no longer floors `gallery.imageField` at `'image'`.** It used -to supply that key for every object view, declared or not. The screen damage was -small — `ObjectGallery` collapses the cover area when no record yields a cover — -but the key fed `ListView`'s ADR-0047 capability gate, which reads -`options.gallery.imageField`, so **Gallery was offered on every object view that -whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now -forwards the view's own declared block (both legacy cover spellings still -cross-fill each other, and `titleField` keeps its `'name'` display floor) and -emits no cover key when the view declared none. Same class and same route as -objectui#7029 (calendar) and objectui#7070 (gantt dates). - -⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery -toggle. That is the ADR-0047 rule working: it was only ever offered because this -relay answered the gate on the author's behalf. - -**The visualization switcher is drawn for the resolved list, not the whitelist.** -`showViewSwitcher` was computed from the LENGTH of -`appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects -it with the capability gate — so a view whitelisting `['grid', 'timeline']` with -no timeline block drew switcher chrome around a single Grid entry. The predicate -now lives at the one site that holds both halves. Views whose whitelisted types -all resolve are unaffected. diff --git a/.changeset/7572-i18n-resources-deep-merge.md b/.changeset/7572-i18n-resources-deep-merge.md deleted file mode 100644 index 624ad4ec10..0000000000 --- a/.changeset/7572-i18n-resources-deep-merge.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -fix(i18n): `createI18n`'s `resources` deep-merges, so overriding one key of a namespace keeps the rest - -A language pack's top-level keys ARE the namespace groups (`common`, `calendar`, -`list`, ...), each a nested object, but `createI18n` merged `resources` over the -built-in packs one level deep. Supplying a partial group therefore **replaced** -it rather than merging into it: - -```ts -createI18n({ defaultLanguage: 'en', resources: { en: { calendar: { today: 'Heute' } } } }); -``` - -left `calendar.today` set and dropped `month`, `week`, `day`, `allDay`, -`newEvent`, `moreEvents` and `unscheduled` from the instance. `t('calendar.allDay')` -then returned the bare key, and `calendar.allDay` reached the DOM as literal -text — silently, with no error and no warning. - -The merge now recurses, so a partial group override touches only the keys it -names. Packs nest up to four levels below the group -(`console.ai.empty.build.title`), so it recurses rather than adding one fixed -extra level. Two in-repo call sites were affected by this and are repaired by -the change: `packages/plugin-gantt/demo` supplied 53 of the 80 `zh.gantt.*` keys -and silently lost the other 27 (its own comment says the demo "is never -half-translated" — it was), and the `skills/objectui/guides/i18n.md` setup -example, the documented way to use this API, dropped 46 of the 48 `common.*` -keys in both `en` and `zh`. - -**Arrays are replaced, not concatenated** — stated because it is a decision, not -a library default. No built-in pack carries an array value today (every leaf in -all ten packs is a string), so nothing observable rides on it; the rule decides -what a future array means. An author who writes an array is naming the whole -list, so replacement is the only rule that lets them shorten or reorder one and -the only one that stays idempotent when the merge runs again. This is -deliberately narrower than i18next's own `deepExtend`, which the provider's -async `addResourceBundle` path uses: that recurses into arrays index-wise and -would leave a longer base array's tail behind, which is the same silent-hybrid -shape this fix removes. - -No caller depended on the old replacement semantics: a census of every -`resources` literal in the repo (47 parsed sites, plus the dynamic ones resolved -by hand) found no site that supplied a partial group in order to clear the rest. diff --git a/.changeset/7578-classname-style-describe.md b/.changeset/7578-classname-style-describe.md deleted file mode 100644 index c939c9470a..0000000000 --- a/.changeset/7578-classname-style-describe.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`ClassNameStylePropsSchema` describes itself by its two keys (objectui#7578). - -The schema's `.describe()` text changes from `Style properties` to -`className and inline style`. **This is published runtime metadata, not a -comment**: on this package `.describe()` is what lands in the generated -JSON-Schema `description` field and in the derived docs, so a consumer that -renders or diffs those will see the new string. The object itself is unchanged — -same two optional keys (`className`, `style`), same accept set, same types, same -export name; nothing validates differently. - -Why it moved. objectui#5928 renamed the const away from `StyleProps`, because -the like-named TypeScript `StyleProps` is the Tailwind-scale vocabulary -(`padding`, `margin`, `gap`, `backgroundColor`, ...) and shares zero keys with -these two. That rename only reached readers who can see the const name; the -description still said what the retired name said, so a reader who meets this -schema through generated JSON-Schema or docs was left hunting `padding` or `gap` -under a label that promised them. Naming the two keys ends that at the one place -that reader actually sees. - -The new text uses the verbatim key spellings, so the label answers "what is in -here" with names the reader can act on. Pinned in -`packages/types/src/__tests__/classname-style-describe-7578.test.ts`, read off -the live schema exported by the published `@object-ui/types/zod` barrel. diff --git a/.changeset/7579-relatedlist-value-gates.md b/.changeset/7579-relatedlist-value-gates.md deleted file mode 100644 index 81d5da2bbb..0000000000 --- a/.changeset/7579-relatedlist-value-gates.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only: settle the fetched value before two RelatedList tests read it (objectui#7579). No published behaviour changes. diff --git a/.changeset/7580-responsive-vocabulary-rehome.md b/.changeset/7580-responsive-vocabulary-rehome.md deleted file mode 100644 index 08626f0ba3..0000000000 --- a/.changeset/7580-responsive-vocabulary-rehome.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/layout': minor -'@object-ui/mobile': minor -'@object-ui/core': minor ---- - -Re-home the breakpoint layout vocabulary and delete the two dead responsive -implementations (objectui#7580, maintainer ruling 2026-09-04, option A). - -**Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole -`ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, -`BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the -stated ground that the four types "had no other authorable carrier". That ground is -measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component -whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by -`resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in -`@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and -when a renderer implements it" — is already met here, so the two types a renderer reads -are re-homed rather than retired. - -What survives, under the same names and the same members: - -- `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead - of re-exported from the spec. **No consumer change**: same name, same six members, same - export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. -- `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), - verbatim from the retired `$strict` schema: six optional column counts, no index - signature. `responsive-grid`'s `columns` input and its resolver are unchanged. - -What is removed: - -- `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had - no read point in the package; it was published only because the retired - `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` - needed the type. With the schema gone there is no order vocabulary for it to be the type - of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. -- `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and - `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with - `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / - `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired - `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). -- `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once - the two implementations above went, dropped rather than re-declared locally, the same - disposition the retired i18n names in that file already carry. - -No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, -`ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and -`responsive-grid` itself — are untouched. - -**Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` -objectui#7580: the retirement is merged upstream and unreleased, so this must land first. diff --git a/.changeset/7581-strict-authoring-face-measurement.md b/.changeset/7581-strict-authoring-face-measurement.md deleted file mode 100644 index 1dd5fa4796..0000000000 --- a/.changeset/7581-strict-authoring-face-measurement.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Add `scripts/measure-strict-authoring-face.mjs`, the measurement throwaway for -objectui#7581 (first deliverable of the #5250 ruling). It derives strict twins -of every node schema in memory and counts undeclared keys per component across -the catalog fixtures, the `content/docs` JSON fences and the authored documents -under `apps/**` and `packages/*/examples/**`. Tooling only; no schema, gate or -`.passthrough()` changes, and no package is released by this change. diff --git a/.changeset/7586-primary-field-consumer-reads.md b/.changeset/7586-primary-field-consumer-reads.md deleted file mode 100644 index 1d72e0ee6b..0000000000 --- a/.changeset/7586-primary-field-consumer-reads.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/plugin-detail': minor ---- - -The last two consumer-side reads of `primaryField` off an OBJECT def are gone (objectui#7586). - -`primaryField` is a `DetailViewSchema` key (`@object-ui/types` `views.ts`) — a **view** key, -which `DetailView.resolveDisplayTitle` reads off `schema` and is welcome to. Read off an -**object** def it is undeclared: `@objectstack/spec`'s object schema is a `strictObject` -answering `unrecognized_keys: ['primaryField']`, and `ObjectSchema.create()` throws. -`primaryField` appears in **zero** files of the shipped `@objectstack/spec@17.2.0` dist, -against 68 for the canonical `nameField`. objectstack#6326 removed the identical read from -two lint rules; objectui#7287 / PR #7585 removed it from `resolveTitleField`. These two -survived it — and three of this repo's own changelogs already called the probe *"not a spec -property — always undefined"* while the code kept honouring it. - -**They are two different repairs, not one patch applied twice.** - -`@object-ui/components` — `PageHeaderRenderer`'s record-chip chain ranked -`objectSchema.primaryField` directly under `schema.title` and **above** the unified ADR-0079 -resolver, on the surface that renders the **actual H1** of a synthesized record page. The -rung is deleted, so the heading now comes from `titleFormat` → the ADR-0079 resolver -(`nameField` → `displayNameField` → type-aware derivation) → the record-key walk, the same -precedence `DetailView`'s own header uses. - -`@object-ui/plugin-detail` — `record:details`' dedupe ladder decides **which row the body -grid hides**, so that the field already shown as the H1 is not repeated underneath it. That -is a different question from "what is the title", and `primaryField` was its *first* -candidate. The rung is deleted; the ladder is the literal display-name walk that mirrors the -tail of the header chip's chain. Its docstring, which had described the chip as resolving -"from objectSchema.primaryField", went stale when PR #7585 landed and now describes the chip -as it is. - -**User-visible, deliberately.** A payload that carries the off-spec key anyway changes in two -ways: the H1 of its record page stops being `primaryField`'s value, and a different row -survives the detail grid. Nothing spec-legal can reach either path. - -`DetailViewSchema.primaryField` is untouched, and so is `ObjectDefLike.primaryField` in -`buildDefaultPageSchema` — a deliberate declaration (not a read) that keeps an external -caller's object literal type-checking. diff --git a/.changeset/7587-quota-reset-promise-pin.md b/.changeset/7587-quota-reset-promise-pin.md deleted file mode 100644 index 481d0b8bf0..0000000000 --- a/.changeset/7587-quota-reset-promise-pin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Pin what a free-plan 429 actually renders after cloud PR #1852 inverted -`error.details.resetsTonight` and added a sibling `resetsAt` (objectui#7587). -Measured: `resetsTonight` has no reader past `parseAiQuotaError`, so the -inversion changes nothing on screen and the banner is the server's own sentence -verbatim — pinned as an exact accounting of the rendered text, and as the flag -being inert. Tests plus one field comment; no package is released by this -change. diff --git a/.changeset/7593-record-alert-success-icon.md b/.changeset/7593-record-alert-success-icon.md deleted file mode 100644 index 9b8e07cd84..0000000000 --- a/.changeset/7593-record-alert-success-icon.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -Fix `record-alert` with `severity: 'success'` rendering a database glyph instead of a -check mark (objectui#7593). - -`SEVERITY_STYLES.success.icon` was the string `'CheckCircle2'`. That literal reaches -``, whose resolver degrades an unresolvable name to the `Database` -icon — deliberately, because server-driven schemas reference icons from other -libraries and it is better to degrade than to throw. The result was an emerald -"success" banner with a database glyph in it, on every such alert, silently. - -The fix is the live spelling `'circle-check'`, and it is **substitution-free**: it was -derived by identity rather than remembered. On `lucide-react@1.31.0`, -`CheckCircle2 === icons.CircleCheck` is `true` — the same glyph object under two names -— so the repair cannot change which glyph is drawn, only whether one is drawn at all. -`CheckCircle2` remains a live named export, so static `import { CheckCircle2 }` -call-sites are unaffected; it is dead only for NAME-BASED lookup, which is the route -this constant takes. - -The load-bearing half is a new pin, because nothing could catch this in either -direction. The icon gate does not judge dynamic-surface resolvers (its own verdict line -says so), and the renderer's suite mocks `LazyIcon`, so its assertion could only echo -the literal back — and it did, pinning the dead spelling. The pin consults the real -resolver and asserts that every `SEVERITY_STYLES` icon RESOLVES, rather than that it is -spelled any particular way, so it fails on the actual failure mode rather than -restating the diff. - -The other three severities (`Info`, `AlertTriangle`, `AlertCircle`) are **not** changed -and were never defective: two of them are absent from lucide's `icons` record but -present on the dynamic surface (1767 keys vs 2025 names), which is the surface that -decides here, so they resolve and render correctly today. diff --git a/.changeset/7603-resource-edit-read-decoration-strip.md b/.changeset/7603-resource-edit-read-decoration-strip.md deleted file mode 100644 index f54483aad9..0000000000 --- a/.changeset/7603-resource-edit-read-decoration-strip.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Strip the framework's read-time decorations off a served draft before the metadata -editor's client Zod gate judges it (objectui#7603). - -**The symptom.** Opening an item that has a **pending draft** in the metadata admin -raised a false "this item is invalid" banner plus inline field errors — on a body the -server accepts, and on exactly the workflow where an author is mid-edit. Every wired -metadata type whose schema is `.strict()` was affected (14 of the 15); `sharing_rule` -was shielded only by accident, because `AUTHOR_SHAPE_ONLY_TYPES` switches its edit gate -off for an unrelated reason. - -**The cause.** `client.getDraft()` serves a **decorated** body: the strict draft branch -returns `item: decorateMetadataItem(type, …)`, which attaches `_diagnostics` whenever -the type has a registered Zod schema, and `_draft` on the preview-draft branch. -`ResourceEditPage` merged that body over the layered baseline and handed the result -straight to `validateMetadataDraft`. The layered half is clean — `getMetaItemLayered` -serves RAW layers — so the misfire required a pending draft to exist, which is why it -stayed invisible: an item with no draft passes today. - -`@objectstack/spec` names both keys a read-time decoration and states that a served body -"is therefore NOT a valid input to the schema that produced it until these are removed". -The server was right and the client was wrong. - -**The fix.** `extractDraftBody` — the one function that turns a served draft envelope -into a body, and the chokepoint all three merge sites go through (the load effect, the -post-save refresh, the post-publish refresh) — now passes the body through the spec's -own exported `stripReadDecorations`, the same helper `MetadataService.saveFields` -already uses on the write side. The strip runs after the presence verdict, so removing -our own decorations can never turn a served draft into "no draft". - -No schema was loosened and no key list is restated in this repo. The list is the spec's, -reached through its helper, because a local copy goes stale the next time the framework -adds a decoration — and a decoration this code does not know to remove is precisely the -defect. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) is deliberately -**not** on that list: those keys are allowlisted by the closed schemas so provenance -survives a re-parse, and this strip leaves them alone. A genuinely undeclared key on the -same draft is still refused, by name. diff --git a/.changeset/7615-field-api-name-follows-label.md b/.changeset/7615-field-api-name-follows-label.md deleted file mode 100644 index a698c21c82..0000000000 --- a/.changeset/7615-field-api-name-follows-label.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio object designer: a new field's API name now follows the Label on **every** -keystroke, not just the first one (objectui#7615). - -Typing a label one character at a time — what a person at a keyboard actually does — -used to leave the field permanently named after its first letter: `Health Score` -produced the API name `h`, and no later keystroke moved it. Pasting or autofilling the -same label produced `health_score`, so the two ways of entering the same label -disagreed. The one-letter name then leaked into the REST API, formulas and exports, -with nothing on screen saying the API name had stopped following the Label. - -Cause: the derivation asked "is this name still auto-generated?" by pattern-matching -the name itself (`field_` / `` / `_`), and its own first rename -destroyed that shape. The inspector now records who owns the API name instead of -re-reading it off a string the feature rewrites. - -Three boundaries, unchanged in intent from objectui#2260: - -- a new field whose API name the author has not touched re-derives on every label - change; -- a field that arrived already named (i.e. saved) is never renamed by a label edit; -- once the author types in the API name box the Label stops moving it — including - when what they typed looks like an auto-generated placeholder, which no - pattern-match on the string alone can tell apart from a real one. diff --git a/.changeset/7620-data-table-date-convention.md b/.changeset/7620-data-table-date-convention.md deleted file mode 100644 index d8815092b6..0000000000 --- a/.changeset/7620-data-table-date-convention.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/components': minor ---- - -One home for the `date` display convention in `data-table` (objectui#7620). - -`data-table`'s fallback cell (`formatCellValue`) sniffs ISO strings and -formats them. Its date-only branch built its own `Intl.DateTimeFormat` bag — -`{ year: 'numeric', month: 'short', day: 'numeric' }` — while the shared -`formatDate` drops the year inside the CURRENT year on purpose (the year -rarely helps on an in-progress record and crowds the cell). So one table -rendered two faces for the same value depending on which path a cell took: -the `date` field cell showed `Jul 4` and the fallback cell showed -`Jul 4, 2026`. The branch now calls `formatDate` (default style). - -**Visible change**: in every `data-table`, a current-year date-only value in a -column that renders through the fallback cell loses its year — `Jul 4, 2026` -becomes `Jul 4` in `en-US` — and now matches the `date` field cell beside it. -Past- and future-year dates are byte-identical (`Jul 4, 2024`), which is why -the split was easy to miss: the two faces only ever diverged on the dates -users look at most. The datetime branch, the non-date passthrough and every -cell with its own renderer are untouched. - -A column that genuinely wants the year on every row is an explicit `format` -style honoured by both paths, not a second option bag — the objectui#7443 / -objectui#4576 lesson, one type over. diff --git a/.changeset/7623-retire-dashboard-root-title-declaration.md b/.changeset/7623-retire-dashboard-root-title-declaration.md deleted file mode 100644 index 8cf4f79fa3..0000000000 --- a/.changeset/7623-retire-dashboard-root-title-declaration.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the -dashboard-root `title` member (objectui#7623). - -Its doc comment said "Dashboard title displayed in the header", and that stopped being -true one release earlier: objectui#7509 retired all five dashboard-root `title` read -arms under ADR-0049, leaving the key declared, documented as rendering, and inert. The -header text is the spec-canonical `label` on `BaseSchema`, resolved through -`pickLocalized`. - -What an author loses is the **type-level suggestion** only, and there is **no runtime -and no validation behaviour change**. `BaseSchema`'s index signature means an existing -`title:` line still compiles; objectui's Zod twin extends `.passthrough()` `BaseSchema`, -so it parsed a root `title` before this release and still does; and a document validated -against `@objectstack/spec`'s strict `DashboardSchema` was already refused there -(`unrecognized_keys: ['title']`) long before it reached a renderer. Nothing that rendered -stops rendering — the read arms were already gone. - -**Unlike `aria` (objectui#5830), no tombstone comes with this.** The spec refuses a root -`title` as an unrecognized key, not with a named removal message, so there is nothing to -inherit by reference and none was invented; the pin -(`packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts`) asserts the -key-set half only — `title` is out of the interface's declared members while `columns`, -`widgets` and `header` stay in. - -**Not affected, despite the shared name:** widget-level `DashboardWidget.title` — the -spec's `I18nLabel` on a different receiver, live, declared and read by -`DashboardRenderer`, `DashboardGridLayout` and `DashboardWithConfig`. The pin carries it -as an explicit control. - -**Migration:** delete a dashboard-root `title` from any authored dashboard and use -`label` (it accepts a string or a per-locale map). Widget titles are unchanged. diff --git a/.changeset/7625-drop-recharts-from-components.md b/.changeset/7625-drop-recharts-from-components.md deleted file mode 100644 index edbc9b70fb..0000000000 --- a/.changeset/7625-drop-recharts-from-components.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`@object-ui/components` no longer declares `recharts`. The dependency became -unused when objectui#7397 deleted `src/ui/chart.tsx`, which was its only -consumer inside this package; `@object-ui/plugin-charts` declares its own -`recharts` and remains the single implementation (objectui#7625). - -**No migration.** Nothing is added to or removed from the published surface, no -type changes, no behaviour changes. Since objectui#7397 this package publishes -no recharts-typed export at all, so there is no supported import that reaches -`recharts` through `@object-ui/components`. - -**What actually changes** is the install graph: every consumer of -`@object-ui/components` was resolving and installing a charting library it could -not reach. AGENTS.md section 3 constrains this package — the Atoms layer — to -"Shadcn primitives, zero heavy 3rd-party deps", and heavy widget dependencies -belong in `@object-ui/plugin-*`; the declaration outlived the reason it existed. - -- **Why `patch` and not `minor`.** This repo ships its own breaking changes as - `minor` with the break spelled out (`scripts/check-changeset-no-major.mjs`), - so the bump has to state which of the two this is. Nothing breaks for a - supported consumer: the built output is byte-identical (this package's Vite - `external` predicate is path-based and never reads `dependencies`, so no - import graph, chunk or `.d.ts` moves), and the package exposes no recharts - surface to import. The one consumer this can reach is someone importing - `recharts` without declaring it and getting it hoisted out of this package's - dependency by a FLAT `node_modules` layout (npm/yarn). That is a phantom - dependency — undeclared, unsupported, and un-typed here since objectui#7397 — - and it is named rather than omitted so the trade is on the record. -- **Not a bundle-size change.** `vendor-charts` stays eagerly reachable through - the plugins, so `check:eager-closure` is unaffected in both directions. The - cost this removes is install-graph weight, not shipped bytes. diff --git a/.changeset/7627-shared-record-source-reader.md b/.changeset/7627-shared-record-source-reader.md deleted file mode 100644 index ac7d4d29df..0000000000 --- a/.changeset/7627-shared-record-source-reader.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/plugin-calendar': minor -'@object-ui/plugin-gantt': minor -'@object-ui/plugin-grid': minor -'@object-ui/plugin-map': minor -'@object-ui/plugin-tree': minor -'@object-ui/react': minor ---- - -`@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which -object is this block bound to" (objectui#7627). - -Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, -`ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three -wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in -dataConfig` test. They now delegate to one function that states the published -objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. - -**No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim -into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its -post-collapse spelling across the whole contract-valid input matrix — both bindings -present, data only, `objectName` only, empty `objectName`, empty `data.object`, the -`api` / `value` / `staticData` / array-shorthand providers, and nothing bound. - -**Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, -ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers -how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. -The new reader answers which object a block RESOLVES, where the `data` block wins — the -order declared on both published faces in `@object-ui/types` and pinned by -`objectql-record-source-refinement-6939.test.ts`. Merging them would silently override -one standing ruling or the other. - -**`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted -order, with an in-place comment saying why: its receiver is a localStorage key -(`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every -saved layout and filter-chip set of a view carrying both bindings. Two more sites the -finding listed are not object-name readers at all and were struck: `ObjectGantt`'s -refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` -type-guard over a widget's `data`. - -`useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four -lines that taught it, so the copies cannot re-seed from the hook that replaced them. diff --git a/.changeset/7632-shared-record-source-config.md b/.changeset/7632-shared-record-source-config.md deleted file mode 100644 index b455fd38be..0000000000 --- a/.changeset/7632-shared-record-source-config.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-tree': patch ---- - -One shared record-source ladder, five plugins delegate (objectui#7632). - -`@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation -of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as -`{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and -`null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader -`resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. - -That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its -zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, -maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — -and it was hand-copied into five plugin components with no gate holding them together. A -change to the ruled order had five edit sites and nothing that noticed a missed one; that -is the AGENTS.md #0.1 drift class. - -**No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the -shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` -shorthand as a documented head above the shared call and are otherwise unchanged. -`record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse -bodies verbatim and asserts the post-collapse spelling agrees with each across the whole -input matrix, so a later edit to the shared reader that moves any site turns red. - -**Two divergences were measured rather than assumed, and both are preserved.** - -`ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is -the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. -That is a TypeScript narrowing device with no runtime effect — an absent property reads -`undefined`, falsy either way — so the guard could never change which rung is taken. The -shared reader's optional-property parameter accepts the union directly, and the -equivalence is pinned on a fixture that really lacks both keys rather than argued. - -`ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; -`ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That -is a real divergence on off-contract input — `ViewData` is a discriminated union over object -variants, so an array under `data` cannot be published. It is NOT unified here: the shared -rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 -left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are -pinned, so neither folding the head in nor deleting it as redundant can happen silently. - -`ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed -parameter. Types are erased at runtime, so nothing it resolves moves. diff --git a/.changeset/7638-navigation-url-follows-record-source.md b/.changeset/7638-navigation-url-follows-record-source.md deleted file mode 100644 index ad2d5dc14d..0000000000 --- a/.changeset/7638-navigation-url-follows-record-source.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-tree': patch -'@object-ui/react': patch ---- - -A record-page URL now names the object the clicked rows actually came from, in -`ObjectTree` and `ObjectCalendar` (objectui#7638). - -`useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed, -and both components handed it the bare top-level `schema.objectName` while resolving -their own rows through the objectui#6939 record-source ladder (`data`, then -`staticData`, then `objectName`). objectui#6939 published `objectName` as that ladder's -THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one -record source — and a row fetched through `data.object` whose click built -`/{schema.objectName}/record/{id}` named a record that the URL's own object does not -contain. - -Two shapes change, both toward the object the rows came from: - -- a block carrying **both** bindings navigated to the top-level key and now navigates to - `data.object`; -- a **data-only** block had no name to build a URL from at all, so the hook took its - `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed - record URL. - -`ObjectCalendar` is where the divergence was plainest: on one click it resolved the -detail drawer through the ladder and the navigation URL through the top-level key. The -URL now reuses the very `schemaObjectName` that already keys the calendar's record query -and its `$expand` derivation, so query, drawer and URL agree by construction. - -**Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName` -tail for the off-contract `data: { provider: 'object' }` that carries no `object` -(`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName` -already carries, and the same conservatism objectui#7627 applied when it published the -shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an -`objectName`, and only what callers hand it has changed. - -The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is -why there were copies to convert at all — component authors copied the divergence out of -the documentation, correctly, as written — so it now points at -`resolveRecordSourceObjectName` and says explicitly that a caller with no data config -has nothing above rung three and should keep passing `schema.objectName`. - -`ObjectKanban` is deliberately **not** converted: it has no data config, no -`getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so -`schema.objectName` already IS its record source and its board, drawer and URL already -agree. diff --git a/.changeset/7645-schema-registry-kanban-honesty.md b/.changeset/7645-schema-registry-kanban-honesty.md deleted file mode 100644 index 8322a99834..0000000000 --- a/.changeset/7645-schema-registry-kanban-honesty.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`SchemaRegistry['kanban']` stops describing a component it cannot name - -`SchemaRegistry` documents itself as "the Single Source of Truth for component -type lookups". Its `'kanban'` entry named `DeclarativeKanbanSchema` — the -authoring/validation face declared in this package — while the renderer -registered for that key is `ObjectKanbanRenderer` in `@object-ui/plugin-kanban` -(`ComponentRegistry.register('kanban', …)`), which consumes that package's own -`KanbanSchema`. The two are distinct dialects, measured member by member: the -plugin face declares 19 members in its own body, the declarative face 7, and -they share three names — `type`, `columns` and `onCardMove`. `onCardMove` has -the same signature on both sides; `columns` is required -`DeclarativeKanbanColumn[]` on one and optional `KanbanColumn[]` on the other, -the two element types sharing five of their six members; the remaining 16 -plugin members and the declarative `draggable` / `onCardClick` have no -counterpart across the gap. For this one key the map's value did not describe -the component the key names. - -Pointing the entry at the honest type is not reachable from this layer, and -that was measured rather than assumed. Importing `@object-ui/plugin-kanban` -here is a phantom dependency — `check:phantom-deps` rejects it by file and -pair, type-only imports included — and declaring the dependency would close the -cycle `@object-ui/types` → `@object-ui/plugin-kanban` → `@object-ui/types`, -because this package is the zero-workspace-dependency bottom layer. -objectui#6172's ruling (2026-08-31) kept the plugin's bare names rather than -relocating that dialect down here — that is why this entry cannot name the -plugin's type at this commit. That half of the ruling has since been reversed: -objectui#7664's ruling (a) (2026-09-05) rewrites this package's `'kanban'` -arm to the plugin's shape, has `@object-ui/plugin-kanban` conform to it, and -retires the `DeclarativeKanban*` trio; `registry.ts` is on its execution list, -so this entry is scheduled to be re-pointed at the declared type. The value -below is the transitional state under that ruling, not the permanent one. - -So the entry now asserts only what this layer can prove, and what BOTH dialects -satisfy: `BaseSchema & { type: 'kanban' }`. - -**What this changes for consumers.** `keyof SchemaRegistry` — and therefore the -published `ComponentType` union — is unchanged; `'kanban'` is still a member, -pinned at compile time so it cannot be narrowed away silently. What changes is -the value side of this one key, in two ways, one loud and one silent: - -- **Breaking, loudly, for consumers who indexed `SchemaRegistry['kanban']` and - flowed the value into the declarative face.** Before this change - `SchemaRegistry['kanban']` *was* `DeclarativeKanbanSchema`, so - `const s: DeclarativeKanbanSchema = registryValue` compiled. It no longer - does: the entry lacks the required `columns` member, and that assignment is - a compile error (TS2741). Nothing in this repository performed such an - indexed access, so no in-repo code moves — the break is for consumers - outside it. -- **Silent, for member reads.** `BaseSchema` carries an index signature - (`[key: string]: any`), so reading the declarative face's own members — - `columns`, `draggable`, `onCardMove`, `onCardClick` — off the new entry is - not an error: the reads resolve through the index signature and type as - `any`. `SchemaRegistry['kanban']['columns']` was `DeclarativeKanbanColumn[]` - and is now `any`; nothing turns red, the read just stops being checked. - (Undeclared keys already read as `any` before, through the same signature; - what changes is those declared members.) - -Migration: name the face you actually mean. `DeclarativeKanbanSchema` is still -exported from `@object-ui/types` unchanged for the authoring face, and -`KanbanSchema` from `@object-ui/plugin-kanban` for the rendered one. - -This is a breaking change shipped as `minor`: this repository's -version-alignment rule keeps objectui's major pinned to `@objectstack`'s and -ships objectui's own breaking changes as `minor` with the break spelled out in -the changeset body, which is what the two bullets above are. diff --git a/.changeset/7654-chatbot-display-mode-tombstone.md b/.changeset/7654-chatbot-display-mode-tombstone.md deleted file mode 100644 index 5c8f73d54d..0000000000 --- a/.changeset/7654-chatbot-display-mode-tombstone.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-chatbot': patch ---- - -Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an -ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's -"Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed -(objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44). - -⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.** -Ships as `minor` per the launch-window convention: objectui's `major` is a cross-repo pin -to `@objectstack`'s so that "same major means compatible" holds across the two repos -(`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes ship as -`minor` with the break named where it lands — this entry is the channel that carries it. - -## What was retired, and why - -The node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one -selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling -of that same choice, and no renderer has ever read it: `chatbot-floating` renders the -trigger and panel unconditionally, and `chatbot` never looked at the key, so -`displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a -`chatbot-floating` node changed nothing. It was nevertheless declared on both faces, -painted as a **Display Mode** control in the designer's property panel, and written as -`'floating'` into every node the designer created — two surfaces teaching a switch that -did not exist. - -Re-measured on this branch's base rather than inherited from the card: a whole-repo -`git grep` census over tracked files, build output excluded, returned the declarations, -the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and -two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same -pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was -not blind. - -FROM → TO: - -- `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an - ADR-0049 retirement tombstone whose comment points at `type` as the replacement. -- `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, - the same tombstone. objectui#7655 declared the key on the floating face with - `ChatbotSchema`'s own lines precisely so this retirement would find it on both faces; - leaving the copy typed would have kept the published face teaching the switch. -- `chatbot-floating` `inputs`: the **Display Mode** control is removed. -- `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into - designer-created nodes. - -A control is restated, never deleted into a vacuum (objectui#7070): the restatement of -the removed control is the tombstone's guidance plus this note. - -**Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema` -or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` — -`'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for -inline. **No JSON document needs editing** — see the next section. - -## Stored documents: runtime validation of this key is unchanged — zero before, zero after - -`displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for -both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is -`.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node -the designer ever created) parses green before this change and parses green after it, -and the value is dropped at render time exactly as it always was. - -That is deliberate, and it is why this tombstone has **no `retirementTombstone()` -half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis -(objectui#6152), a different defect, and a parse outcome the ruling did not ask for. -`packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins' -shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if -objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the -mirror adds the `retirementTombstone()` half at that time, flipping the control rather -than deleting it. - -## Why a tombstone and not a deletion — measured on this carrier - -`ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index -signature, and on such a carrier deleting an optional member is **silent in every value -shape**: the index signature defeats both excess-property checking and the weak-type -check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature -control carrier (`FloatingChatbotConfig`) lit in the same run: - -| route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | -|---|---|---|---| -| declared (before) | clean | `TS2322` | clean | -| deleted | clean | **clean** | clean | -| tombstoned (after) | `TS2322` | `TS2322` | `TS2322` | - -Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned, -**presence with any value** is a compile error — a channel deletion cannot produce on -this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not -louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678). -Prong 2 of that discriminator licenses the tombstone: the key was advertised in the -3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the -presentation switch. The deleted row is pinned in the test file as a live control — an -undeclared key that rides both shapes with no directive — so the contrast cannot rot. - -## Accept-set change, one line per face - -- **TypeScript.** A write of `displayMode` against either chatbot face used to compile - and now does not. -- **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document - carrying the key parses green before and after, and keeps the value. -- **Designer.** The **Display Mode** control disappears from the `chatbot-floating` - property panel, and newly created nodes no longer carry the key. -- **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating` - registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to - 8, so the manifest projected from them no longer lists the prop. Measured on both sides - of this change: `validateTree` on a stored `chatbot-floating` node carrying - `displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity - **`warning`**, message `` ` has no prop "displayMode"` `` — which is - what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs - the props interface `generateDts` derives from those same `inputs` drops from 20 members - to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those - generated intrinsics no longer type-checks the attribute. - - **This is author-time only: no stored document stops parsing and nothing at render - moves.** The value survives compilation — `compile()` returns a tree still carrying - `displayMode: 'floating'`, byte-for-byte the same keys before and after — and a - `warning` never blocks a page, because the page renderer filters the diagnostics to - `severity === 'error'` before deciding whether to fail. Two neighbouring instruments are - untouched and worth naming so the scope is not read wider than it is: `os validate` runs - `safeValidateSchema`, the Zod path, and is silent on this key before and after; and the - build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does - not contain `chatbot-floating` on either side of this change. diff --git a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md deleted file mode 100644 index 9b382d360c..0000000000 --- a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `FloatingChatbotConfig.triggerIcon` (objectui#7654, ADR-0049 enforce-or-remove). - -`triggerIcon` was declared `?: string` with `@default 'MessageCircle'` and read by nothing. -`FloatingChatbot` destructures six of the interface's seven keys — `position`, -`defaultOpen`, `panelWidth`, `panelHeight`, `title`, `triggerSize` — and never this one, -and `FloatingChatbotTrigger` takes no icon prop at all, so the advertised default never -rendered either. Re-measured on this branch's base rather than inherited from the card: a -whole-repo `git grep` census over tracked files, build output excluded, returns the -declaration and one historical CHANGELOG line and nothing else, while the same pass over -`triggerSize` — a key that IS read — returns ten sites across four files, so the instrument -was not blind. - -It is also absent from the `chatbot-floating` registration's `inputs` AND from its -`defaultProps` (`packages/plugin-chatbot/src/renderer.tsx`), both re-confirmed here. No -designer control ever offered it and no designer-created node carries it, so TypeScript was -the only way to reach the key. That is what makes this half of objectui#7654 an ordinary -retirement; the card's other key, `displayMode`, is seeded into `defaultProps` and is NOT -touched here. - -FROM → TO: `triggerIcon?: string` → **tombstoned**, `?: never` on the interface. The FAB -trigger renders a fixed icon and takes no icon prop; there is no authored spelling that -changes it. - -## This tombstone has NO Zod half, deliberately - -Every other tombstone in this package pairs `?: never` with a `retirementTombstone()` -refusal on the Zod twin. There is no twin here to carry one: `FloatingChatbotConfig` has no -Zod mirror at all, and `floatingConfig` sits in the `UnmirroredDeclared` ledger -(`zod-mirror-parity.test.ts`, `complex.zod.ts#ChatbotSchema`). `BaseSchema` is -`.passthrough()`, so the whole `floatingConfig` object rides through unvalidated — before -this change and after it. Minting a mirror to host a refusal would be the -declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and -inert, or unmirrored and live, and fixing one says nothing about the other. This change -does not widen into it. - -**Accept-set change, stated plainly for reviewers:** on the TypeScript face, a write of -`FloatingChatbotConfig.triggerIcon` used to compile and now does not. On the runtime face, -nothing changes at all — the key parsed green before and parses green after. The refusal is -TYPE-LEVEL ONLY, which is narrower than this package's other tombstones and is the reason -this carries a contract-review label rather than being filed as an internal tidy-up. - -## Why a tombstone and not a deletion, when the usual argument does not apply - -The usual case for `?: never` argues from the mirror: an undeclared key is silently -STRIPPED by a non-strict `z.object`, so deleting trades one silent no-op for another. With -no mirror, that argument is unavailable, so the route was measured on the `tsc` channel -alone instead: - -| route | fresh object literal | widened (non-fresh) value | -|---|---|---| -| deleted | `TS2353` excess-property error | **compiles CLEAN** | -| tombstoned | `TS2322` | `TS2322` | - -Excess-property checking only reaches a fresh literal, so deletion would have left the -widened path — `const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig = -raw;` — silently accepting a key nothing reads. The declared `never` makes the assignment -itself ill-typed, so freshness stops mattering. Both rows are pinned in -`packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts`, the "deleted" -row as a live control on a genuinely undeclared key rather than as prose, so the contrast -cannot rot. - -That file also pins the runtime half as a **tripwire**: it asserts that a node carrying -`floatingConfig.triggerIcon` still parses green. If objectui#6152 ever mints a -`FloatingChatbotConfigSchema`, it goes red — the intended signal that whoever lands the -mirror must add the `retirementTombstone()` half at the same time and flip the control -rather than delete it into a vacuum. diff --git a/.changeset/7655-chatbot-registration-authoring-faces.md b/.changeset/7655-chatbot-registration-authoring-faces.md deleted file mode 100644 index c5a8e2fbff..0000000000 --- a/.changeset/7655-chatbot-registration-authoring-faces.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-chatbot': patch ---- - -One named, importable authoring-face type per `plugin-chatbot` registration: -`ChatbotEnhancedSchema` and `ChatbotFloatingSchema` join `ChatbotSchema` -(objectui#7655, under the objectui#6169 / #6172 family ruling — every component -node has exactly one named, importable authoring-face type). - -`packages/plugin-chatbot` registers three components — `chatbot`, -`chatbot-enhanced`, `chatbot-floating` — and `@object-ui/types` published ONE -face for the family with `type` pinned to `'chatbot'`. An author annotating a -`chatbot-enhanced` or `chatbot-floating` node either dropped to untyped JSON or -annotated with `ChatbotSchema` and lied about `type`; the docs' floating example -had to be a `json` fence because no `tsx` fence could compile. The two -registrations' real key sets lived in anonymous `ChatbotSchema & { ... }` -intersections local to the renderer, referenceable by nothing outside that file. - -## The shape, and why not the smaller diff - -One interface per registration, not `ChatbotSchema['type']` widened to the union -of the three keys. The union would give three nodes ONE type and re-open what -#6169 closed — a single interface declaring keys only some of its own `type` -values read — and this card exists because the family's declarations had already -drifted from its reads. Each face declares what ITS registration reads, censused -per key on the PR's base (one `schema.KEY` read per registration body in -`renderer.tsx`, lit by keys that are NOT shared: `processVisibility` 0 / 1 / 0, -`floatingConfig` 0 / 0 / 1), and the twenty keys all three read are picked off -`ChatbotSchema` by name (`ChatbotSharedKey`) so they stay one declaration: - -- **`ChatbotEnhancedSchema`** (`type: 'chatbot-enhanced'`): the shared twenty, - plus `maxHeight` and `processVisibility` (read here, not by the floating - panel), plus `enableMarkdown`, `enableFileUpload`, `surface` (`'card' | - 'plain'`, objectui#6687) and the `onClear` runtime slot — four keys - `ChatbotSchema` never declared. -- **`ChatbotFloatingSchema`** (`type: 'chatbot-floating'`): the shared twenty, - plus `enableMarkdown`, `enableFileUpload`, `onClear`, and the two keys it - declares alongside `ChatbotSchema` — `floatingConfig` (`FloatingChatbotConfig`) - and `displayMode`. No `maxHeight`, `processVisibility` or `surface`: the - floating registration has no named read for any of them. (Its trailing raw - props spread does carry authored keys into the panel today — `processVisibility`, - `surface` and `showAvatars` are live there, measured through the real host; - that accidental channel is tracked as objectui#7708, and this face neither - declares nor promises it.) -- Neither face declares `ChatbotSchema`'s six legacy members (`loading`, - `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown`, `height`) — no - registration reads them by name — and neither redeclares `disabled`, which - stays `BaseSchema`'s `boolean | string` (objectui#7087). - -**`ChatbotSchema` is unchanged.** It keeps `displayMode` and `floatingConfig` -(declarations verbatim), and the floating face declares the same two, so -`ChatbotSchema['displayMode']` and `ChatbotSchema['floatingConfig']` stay the -typed members they were — the objectui#7669 `triggerIcon` tombstone keeps its -reach on `chatbot` nodes, now pinned on the node. `floatingConfig`'s doc comment -is rewritten on both faces: the old text said it was "only used when -`displayMode` is `'floating'`", which was false — it is read by `chatbot-floating` -alone and forwarded to the panel. `displayMode` is RULED RETIRED — objectui#7654, -maintainer ruling B (2026-09-05): `?: never` tombstone, designer control and -`defaultProps` seed removed, in that card's own change. This change carries the -key untouched on both faces (still unmirrored, still read by nothing) so that PR -finds the member exactly as ruled, and a tripwire test pins that any value still -parses green until that PR flips it. - -**New published symbol:** `ChatbotSharedKey`, the string-literal union of the -twenty keys all three registrations read. It is exported from `complex.ts` -because an exported interface may not extend a `Pick` over a private name -(TS4022), so it is emitted into `dist/complex.d.ts` and is reachable through the -published `@object-ui/types/complex` subpath (it is not re-exported from the -package entry). It is a census, not an authoring face. - -## Zod twins, in lockstep - -`@object-ui/types/zod` gains `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` -(and `ComplexSchema` routes the two new discriminants). Every declared key is an -arm except: the three runtime slots (`onError`, `onSend`, `onClear`), refused by -name per objectui#6124; and, on the floating twin only, `floatingConfig` (no -`FloatingChatbotConfig` mirror exists — minting one is objectui#6152's axis) and -`displayMode` (unmirrored on `ChatbotSchema`'s twin too; retired by ruling on -objectui#7654 and executed there). The twins mirror the API body params under the -key the renderer reads, `requestBody`, and inherit `body` as the children slot — -they do not copy `ChatbotSchema`'s `body` naming collision. - -**Accept-set change, stated plainly:** a `chatbot-enhanced` or `chatbot-floating` -document parsed through the family's only twin used to fail on `type`; through -its own twin it now parses, and the keys the twin declares are VALIDATED where -they rode through `.passthrough()` unexamined before (`surface: 'frameless'`, -`enableMarkdown: 'yes'` and `requestBody: 'x'` are refused). A `chatbot` node's -parse outcome is unchanged: `ChatbotSchema`'s twin did not move. - -## `@object-ui/plugin-chatbot` - -The `chatbot-enhanced` and `chatbot-floating` registrations type `schema` as the -published faces and drop the anonymous intersections. One consequence: -`chatbot-floating` used to write `disabled={schema.disabled}` and then spread -`{...props}` AFTER it — and `SchemaRenderer` always includes `disabled: verdict -|| undefined` in those props, so the raw read was overridden on every render. -With `disabled` honestly typed as `boolean | string` the raw union cannot be -forwarded into the panel's `boolean` prop, so the registration now names the -host verdict (`disabled: hostDisabled`) the way its two siblings have since -objectui#4431. No render outcome moves; the pin renders through the real host -both ways. - -This ships as `minor` for `@object-ui/types` because it widens the published -surface with two new node types, two new Zod twins and one new type alias; -`ChatbotSchema`'s own accept set does not move: objectui's major is pinned to `@objectstack`'s -(`scripts/check-changeset-no-major.mjs`), and objectui's own contract changes -ship as `minor` with the semantics spelled out — as above. diff --git a/.changeset/7658-toc-code-span-anchor-parity.md b/.changeset/7658-toc-code-span-anchor-parity.md deleted file mode 100644 index ebc03d3009..0000000000 --- a/.changeset/7658-toc-code-span-anchor-parity.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-markdown': patch ---- - -Fix `extractToc` deleting tag-shaped text that lives INSIDE an inline code span, -so its `#id` links resolve to the heading they name again (objectui#7658). - -`stripInline()` applied its rules in sequence: the inline-code rule unwrapped -`` `objectui add ` `` to `objectui add `, and the raw-HTML -rule that ran next over that same text deleted `` as if it were -markup. The slug became `objectui-add` while `rehype-slug` — which slugs the -RENDERED heading, where a code span's content is a literal text value — put -`objectui-add-component` on the anchor. The TOC entry rendered, was clickable, -and silently went nowhere. The same sequencing let the emphasis rules eat the -underscores out of `` `a_b_c` `` and the link rule rewrite `` `[x](y)` ``. - -Code spans are now lifted out before any other inline rule runs and restored -verbatim at the end, so nothing reaches inside one. The raw-HTML rule is -unchanged and still strips genuine markup — `remark-rehype` runs without -`allowDangerousHtml`, so the renderer likewise drops raw html nodes and keeps the -text they wrapped. Link labels that are code spans still collapse (`` [`getData`](/api) `` -→ `getData`), because the placeholder stays ordinary text to the link rule. - -Seven live headings in this repo's own docs were affected -(`content/docs/utilities/cli.mdx`, `content/docs/utilities/runner.mdx`, -`packages/cli/README.md`). Pinned against the real render pipeline rather than a -second derivation of the slug rules: the new test renders each heading through -`MarkdownImpl` and compares `extractToc`'s id to the `id` attribute -`rehype-slug` actually emitted. diff --git a/.changeset/7664-kanban-arm-plugin-dialect.md b/.changeset/7664-kanban-arm-plugin-dialect.md deleted file mode 100644 index f699bdff08..0000000000 --- a/.changeset/7664-kanban-arm-plugin-dialect.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**BREAKING** — the `'kanban'` validator arm now accepts the shape the registered -renderer reads, and the six `DeclarativeKanban*` exports retire (objectui#7664, -maintainer ruling (a), 2026-09-05). - -For an authored `type: 'kanban'` document two different types were -authoritative depending on who asked. `safeValidateSchema` — what the CLI's -`validate` / `check` commands apply — honoured `DeclarativeKanbanSchema` -(`columns` with `color`, `draggable`, cards with `labels` / `assignees` / -`priority`), while the renderer registered for the key, `ObjectKanbanRenderer` -in `@object-ui/plugin-kanban`, consumed that package's own `KanbanSchema` -(`objectName` / `groupBy` / `cardTitle` / `cardFields`, cards with `badges`). -The two were unrelated dialects, so a board could pass `objectui validate` and -render **empty**. The ruling: the plugin dialect is authoritative. - -**What changes on this package's published surface:** - -- **The `'kanban'` arm's accept set is replaced.** `ComplexSchema` → - `AnyComponentSchema` → `safeValidateSchema` now validate the plugin dialect, - declared here as `KanbanSchema` / `KanbanColumn` / `KanbanCard` / - `CardTemplate` / `ColumnWidthConfig` (TypeScript) and `KanbanSchema` / - `KanbanColumnSchema` / `KanbanCardSchema` / `CardTemplateSchema` / - `ColumnWidthConfigSchema` (`@object-ui/types/zod`). An `objectName` / - `groupBy` board passes. A static `columns[].cards[]` board passes — that - spelling is the same document in both dialects and always rendered. A board - in the retired dialect is **refused by name** at the keys that betray it: a - board-level `draggable` and a column `color` are `?: never` tombstones on the - TypeScript face and named refusal arms on the mirror, each message naming the - retired `DeclarativeKanbanSchema` shape and the spelling to write instead. - Both were measured inert (zero read sites in the plugin). The retired card - keys are deliberately *not* refused: a card is an open record - (`[key: string]: any`), and `priority` or `dueDate` are legitimate record - fields. -- **Every handler key the retired arm refused is still refused, and one more - joins them.** The successor arm carries all five `#6124` refusal arms under - the same `'kanban'` key — `onCardMove`, `onCardClick` and `onQuickAdd` as - RUNTIME SLOTS (callable on the TypeScript face, refused by name on the - mirror: `KanbanRenderer` forwards all three off `schema.*` in one block), - `onColumnAdd` and `onCardAdd` as `?: never` tombstones. `onQuickAdd` is the - one that is newly refused — the plugin dialect declared it, the retired - declarative face did not. ⚠️ `onCardClick` is the key this arm must never - drop rather than refuse: the plugin dialect it is modelled on never declared - the member (the renderer read it undeclared), and because `BaseSchema` is - `.passthrough()`, leaving it out does not refuse it — it stops being judged - and the value is kept. Measured on the built dist, `{ type: 'kanban', - columns: [], onCardClick: { action: 'toast' } }` is REFUSED, beside the same - document at `onCardMove` / `onQuickAdd` / `onColumnAdd` / `draggable`. -- **Six exports retire — the second step of the objectui#6172 rename.** - objectui#6172 (PR #7643, same release line) renamed this package's trio from - the bare names to `DeclarativeKanbanSchema` / `DeclarativeKanbanColumn` / - `DeclarativeKanbanCard` and the three Zod mirrors to `DeclarativeKanban*Schema` - so the bare names could belong to the renderer's dialect. objectui#6172's own - stop condition was "if the renamed copy has no retained value, escalate", and - the retained value it cited was precisely the validator arm. This ruling moves - that arm to the plugin dialect, so the renamed copies have no consumer left - and retire under ADR-0049 (enforce-or-remove): `DeclarativeKanbanSchema`, - `DeclarativeKanbanColumn`, `DeclarativeKanbanCard` from `@object-ui/types` and - `DeclarativeKanbanSchema`, `DeclarativeKanbanColumnSchema`, - `DeclarativeKanbanCardSchema` from `@object-ui/types/zod` are gone. - Importing any of them is a compile error (TS2305). -- **`SchemaRegistry['kanban']` is `KanbanSchema`.** objectui#7645 (PR #7662) - weakened the entry to `BaseSchema & { type: 'kanban' }` because this layer - could not name the plugin's type; it now names the declaration the plugin - itself imports. `keyof SchemaRegistry` — the published `ComponentType` union — - is unchanged. -- **The bare names return to this package with a different shape than they had - before objectui#6172.** `KanbanSchema` here is now the plugin dialect, not the - declarative one the pre-rename `KanbanSchema` was. A consumer that never - migrated off the old bare name and expected `columns` to be required, or - `draggable` to exist, gets a type error rather than a silent change. -- `KanbanConditionalFormattingRuleSchema` is newly exported from - `@object-ui/types/zod`: the rule union the `'object-kanban'` arm already - applied, now shared with the `'kanban'` arm. - -**Migration.** Author boards in the plugin dialect — `objectName` + `groupBy` -for an object-bound board, or `columns[].cards[]` with `badges` for a static -one. Replace `DeclarativeKanbanSchema` imports with `KanbanSchema` (from -`@object-ui/types`, or the Zod `KanbanSchema` from `@object-ui/types/zod`; -`@object-ui/plugin-kanban` re-exports the same `KanbanSchema` type). Delete -`draggable` (drag-and-drop is always on) and column `color` (style a lane -through `className`). `content/docs/api/schema-reference.md`'s kanban section -now documents this dialect. - -This is a breaking change shipped as `minor`: this repository's -version-alignment rule keeps objectui's major pinned to `@objectstack`'s and -ships objectui's own breaking changes as `minor` with the break spelled out in -the changeset body, which is what the bullets above are. diff --git a/.changeset/7664-plugin-kanban-declared-schema.md b/.changeset/7664-plugin-kanban-declared-schema.md deleted file mode 100644 index 3c814de4e1..0000000000 --- a/.changeset/7664-plugin-kanban-declared-schema.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -`KanbanSchema` / `KanbanColumn` / `KanbanCard` / `CardTemplate` / -`ColumnWidthConfig` are now the `@object-ui/types` declarations, re-exported -from this package rather than declared in it (objectui#7664, maintainer ruling -(a)). Nothing this package renders changed and every existing import keeps -resolving; what changed is that `safeValidateSchema` in `@object-ui/types` now -validates an authored `type: 'kanban'` document against this very shape, so a -board that validates is a board these renderers draw. - -**The shape is not member-for-member what this package declared — it is that -shape plus four members**, counted off `origin/main`'s -`plugin-kanban/src/types.ts` (19 members on `KanbanSchema`, 6 on -`KanbanColumn`, 7 on `KanbanCard`) against the `@object-ui/types` -declarations: - -- **`onCardClick` is DECLARED for the first time.** This package's dialect - never had the member, while `KanbanRenderer` has always forwarded - `onCardClick={schema.onCardClick}` — an undeclared read (objectui#7742). It - is declared here as a `#6124` RUNTIME SLOT: callable on the TypeScript face, - refused by name on the mirror, like the `onCardMove` and `onQuickAdd` beside - it in the same forward block. -- **Four `?: never` tombstones** carry the retired declarative face's keys - under the same `'kanban'` key so those spellings keep being refused by name: - `draggable`, `onColumnAdd` and `onCardAdd` on `KanbanSchema`, and `color` on - `KanbanColumn`. None of the four was ever a member of this package's dialect; - each is refused, not silently accepted, because the retired face taught it. - The full accept-set statement is on the sibling `@object-ui/types` entry. diff --git a/.changeset/7667-toc-emphasis-flanking.md b/.changeset/7667-toc-emphasis-flanking.md deleted file mode 100644 index c48aec0a36..0000000000 --- a/.changeset/7667-toc-emphasis-flanking.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/plugin-markdown': patch ---- - -Fix `extractToc` eating the underscores out of a `SCREAMING_SNAKE` heading, so -its `#id` links resolve to the heading they name again (objectui#7667). - -`stripInline()` ended with two hand-rolled emphasis rules that gave `*` and `_` -one shared regex — `(\*\*|__)(.*?)\1` and `(\*|_)(.*?)\1`. Neither knew -CommonMark's flanking rule, under which a `_` run INSIDE a word opens nothing: -it is both left- and right-flanking with no adjacent punctuation, so it may -neither open nor close emphasis. The renderer obeys that and keeps the -underscores; the shared rule paired the first two underscores of -`### NON_GRID_ROW_CEILING` and ate `GRID`, then resumed and ate `ROW`. The TOC -said `nongridrow_ceiling` while `rehype-slug` put `non_grid_row_ceiling` on the -anchor, so the entry rendered, was clickable, and silently went nowhere. - -The underscore form is now its own flanking-aware rule and the asterisk rules -are left alone, because only `_` carries the intraword exemption — giving `*` -the same one would break `a*b*c`, which the renderer really does emphasise. -Underscore runs are matched whole (`(?` -(`plugin-chatbot/src/index.tsx`) — the component the `chatbot` registration renders — -declares `messages`, `placeholder`, `onSendMessage`, `disabled`, `showTimestamp`, -`userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, `assistantAvatarFallback` and -`maxHeight`, and **not one of the six**. - -| key | why not enforce | migration | -| :-- | :-- | :-- | -| `loading` | Chat progress is runtime state the chat runtime owns — the registration derives it from `useObjectChat` as `isLoading`. A static authored boolean would fight the runtime, not configure it | delete the key | -| `showAvatars` | No target on ``. Declaring it on the two faces that DO reach `` would re-open by declaration the channel objectui#7708 closed by fence, one card earlier | delete the key — a `chatbot` node already renders an avatar beside every message | -| `userAvatar` | A second authorable spelling of an image `userAvatarUrl` already carries (AGENTS.md #0.1: one strict contract, not N dialects) | `userAvatarUrl` (+ `userAvatarFallback`) | -| `assistantAvatar` | Same | `assistantAvatarUrl` (+ `assistantAvatarFallback`) | -| `markdown` | `` prints message content as text and has no markdown path; on the two nodes that render markdown, `enableMarkdown` is the live key | `type: 'chatbot-enhanced'` with `enableMarkdown` | -| `height` | `` has no `height` prop, and the live `maxHeight` it forwards is a `string`, not this key's `string \| number` union | `maxHeight`, or `floatingConfig.panelHeight` on a floating node | - -## FROM → TO - -Each member goes to `?: never` on `packages/types/src/complex.ts` and to -`retirementTombstone(...)` on `packages/types/src/zod/complex.zod.ts` — both halves, in -lockstep, the convention `MarkdownSchema.sanitize` (objectui#6972), -`TimelineSchema.timeScale` (objectui#6355) and `ObjectViewSchema.viewTabBar` -(objectui#7779) already carry. Each refusal names the key, says why it is retired, and -points at what to write instead; one string feeds both the parse-time message and the -`.describe()` metadata, so the two cannot drift. - -## Accept-set change, one line per face - -- **TypeScript.** A write of any of the six against `ChatbotSchema` used to compile and - now does not — including through a widened (non-fresh) value, which is the half a - deletion would have missed on a `BaseSchema` carrier. -- **Runtime (Zod / `safeValidateSchema`).** A `chatbot` document authoring one of the six - used to parse **green** and now parses **red**, `invalid_type` at the key's own path - with the guidance as the message. This is the narrowing that carries - `needs:contract-review`. -- **`chatbot-enhanced` / `chatbot-floating`.** Unchanged in both directions. Those faces - never declared the six (objectui#7655 censused them out), their twins have no arm to - refuse one, and `BaseSchema` is `.passthrough()` — so a stored node of either type - carrying a retired key parses exactly as it did. Pinned. - -## Why tombstones and not deletions - -All six **have** a Zod arm, and that is what decides the route here. `BaseSchema` is -`.passthrough()` on the Zod side and carries a `[key: string]: any` index signature on the -TS side, so an UNDECLARED key is not refused — it is KEPT. Deleting the members would hand -the authored spelling exactly the silent no-op this card exists to close, on both faces at -once. The two-prong discriminator (`mobile.ts`, objectui#5941 / #7526 / #7678) leaves that -structural hazard to the carrier: where there is no mirror there is "no silent-strip hazard -for prong 2 to guard". Here there is a mirror to host the refusal, and prong 1 holds by the -letter for four of the six. The "deleted" row is pinned live as a control in -`packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts`, so the contrast -cannot rot into prose. - -## Docs - -`content/docs/plugins/plugin-chatbot.mdx` gains the restatement of the six removed keys -with their replacements (objectui#7070: a control is restated, never deleted into a -vacuum), and its three present-tense claims that the `chatbot-floating` props spread is -still unfiltered are corrected to what PR #8077 actually left behind. diff --git a/.changeset/7704-schema-registry-chatbot-keys.md b/.changeset/7704-schema-registry-chatbot-keys.md deleted file mode 100644 index aaf1ecec86..0000000000 --- a/.changeset/7704-schema-registry-chatbot-keys.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`SchemaRegistry` names all three `plugin-chatbot` registrations, so the published -`ComponentType` union does too (objectui#7704). - -`packages/plugin-chatbot/src/renderer.tsx` registers three components — `chatbot`, -`chatbot-enhanced` and `chatbot-floating` — and `SchemaRegistry` mapped one of them. -Since `ComponentType = keyof SchemaRegistry` is the published union, a consumer -discriminating on it was told two registered keys do not exist: an author narrowing a -node by `ComponentType`, or writing a `Record` table, had no arm for -either. The asymmetry that showed which half was wrong is that -`packages/cli/src/utils/known-schema-types.ts` keeps its own parallel list containing -both keys, precisely because this map did not. - -**Additive.** Two keys join the map under its `// Complex` group; no existing entry -changes, `'chatbot'` still maps to `ChatbotSchema`, and nothing is removed. The union -widens, which cannot break a consumer that produces `ComponentType` values and can only -help one that consumes them — except an exhaustive `Record` or -`switch`, which now needs the two new arms. Nothing in this repo has one: the only -consumer of `ComponentType` outside its own declaration is a pin test. - -**Why the entries can be honest now.** This map's value has to be the type the -registered renderer honours, and until objectui#7655 there was none to point at — -`ChatbotSchema` pins `type` to `'chatbot'`, and each registration's real key set lived -in an anonymous `ChatbotSchema & { … }` intersection local to the renderer file. -objectui#7655 published `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` from this -package, and both registrations already take them as their `schema` parameter, so the -map's value and the renderer's prop type are one declaration — the same property the -`'kanban'` arm gained in objectui#7664. Being declared here also makes them reachable: -`@object-ui/types` has zero workspace dependencies, which is exactly what blocked the -`kanban` case objectui#7645 measured, where the honoured type lived in a plugin. - -Pinned in `src/__tests__/schema-registry-chatbot-keys-7704.test.ts` in two channels — -compile-time (`tsc -p tsconfig.test.json`: the keys survive in `keyof`, each value is -the face its renderer honours, and each value's own `type` literal is its key) and -runtime (a TypeScript-AST census of the interface source, plus each key selecting its -own arm through `safeValidateSchema`). - -Scope: these two keys, whose authoring faces now exist — not a sweep of the map's other -entries, which objectui#7665 holds. diff --git a/.changeset/7708-chatbot-floating-spread-fence.md b/.changeset/7708-chatbot-floating-spread-fence.md deleted file mode 100644 index 3ad24f5747..0000000000 --- a/.changeset/7708-chatbot-floating-spread-fence.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-chatbot': minor ---- - -`chatbot-floating` now fences its `` spread the same way its -two sibling registrations (`chatbot`, `chatbot-enhanced`) already do — -`{...toDomProps(props)}`, at the head of the element, instead of a raw -`{...props}` spread at the end (objectui#7708). This is a deliberate, -user-visible behavior change, not a refactor: - -- **A message sent through a floating chatbot now actually renders.** - Previously the authored `messages` seed (whatever array was on the node - when it was authored) silently overrode the live runtime messages on every - render, because the raw spread landed AFTER `messages={runtimeMessages}`. - Neither the user's own message nor an `autoResponse` reply ever appeared — - the identical send on `chatbot-enhanced` worked correctly. Fixed. -- **`displayMode`, `systemPrompt` and `model` stop leaking as DOM attributes** - on the panel's root element (`systemPrompt` / `model` are still read - normally, by name, for the request they configure — only the second, - unfiltered forward is gone). Closes objectui#4425's leak class on the one - `plugin-chatbot` registration that had not closed it yet. -- **Three undeclared keys go dark on `chatbot-floating` nodes:** - `processVisibility`, `surface` and `showAvatars` reached the panel's - `ChatbotEnhanced` through the raw spread even though `ChatbotFloatingSchema` - never declared them. `ChatbotFloatingSchema` documents this explicitly and - always has — the face never promised these keys — so this closes an - accidental channel rather than removing declared behavior. A document that - relied on any of the three to affect a floating node loses that effect; - author them on a `chatbot-enhanced` node instead, where they are part of - the declared, tested contract. - -`@object-ui/types`: `ChatbotFloatingSchema`'s doc comment is updated to match -— no type-shape change, so nothing that imports the type needs to change. diff --git a/.changeset/7709-edit-mode-chip-sample-data.md b/.changeset/7709-edit-mode-chip-sample-data.md deleted file mode 100644 index f9a759722d..0000000000 --- a/.changeset/7709-edit-mode-chip-sample-data.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/i18n': minor -'@object-ui/app-shell': minor ---- - -**The maker's edit-mode starter offers sample data, not an automation v1 cannot -build (objectui#7709).** Bound to an existing app (`?package=`), the maker's -empty state offered four starters: add a field, add an object, add a dashboard, -and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and -notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the -measured behaviour on the sibling chips was not a refusal but a silent degrade -into a view — so the product recommended an automation and would have handed -back a page. - -Rewording it was not available: asking for a field, a view or a dashboard -duplicates one of the three chips beside it. The fourth chip is now -`addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all -ten packs and in the call-site `defaultValue` fallback, which is a second copy -of the same string. The three surviving chips all add STRUCTURE; what an app -that already has objects most often lacks is DATA, and `seed` is on v1's -authoring whitelist. A note beside the keys in every pack and at the call site -says this chip's automation wording comes back when ADR-0112 v2 re-adds flows -and actions, and the retired sentence for each pack is kept in the guard suite -so v2 has it verbatim. diff --git a/.changeset/7711-calendar-filter-is-not-a-config-slot.md b/.changeset/7711-calendar-filter-is-not-a-config-slot.md deleted file mode 100644 index 080975d209..0000000000 --- a/.changeset/7711-calendar-filter-is-not-a-config-slot.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-calendar': minor ---- - -Retire the `filter.calendar` configuration spelling on `object-calendar` (objectui#7711). - -**Breaking, deliberately.** `getCalendarConfig` no longer probes `schema.filter` for a -`calendar` key. A calendar whose configuration was written as -`filter: { calendar: { startDateField: … } }` no longer resolves a configuration at all -and now renders the component's existing "Calendar configuration required. Please -specify startDateField and titleField." refusal screen. Write the configuration under -the declared `calendar` container instead — the read for it already existed, directly -below the retired arm. - -`filter` is the query filter and nothing else. `@objectstack/spec`'s -`ComponentPropsMap['object-calendar']` declares `calendar` as the configuration -container and `filter` as the base query filter, and admits no `filter.calendar` -spelling; the renderer nevertheless read the config out of the filter FIRST, and the -comment on the canonical read below called *that* one the "backward compatibility" -branch — the contract inverted in a source comment. The arm and the comment are both -gone. No compatibility rung and no deprecation window, per AGENTS.md #0.1: a tolerant -fallback fossilizes the wrong convention into a second de-facto contract. - -**The bug this closes is not only the retired spelling.** One authored key was being -read twice with two incompatible meanings. `filter: { calendar: 'team' }` — a -legitimate condition on a field literally named `calendar` — was returned as the -`CalendarConfig` while the same object still went to `$filter` on the wire. `'team'` is -truthy, so the refusal screen did not fire either: `startDateField` destructured off a -string is `undefined`, every record fell into "unscheduled", and the grid rendered empty -while the author's declared `calendar` block sat unread. Such a filter now reaches -`$filter` untouched and the declared container is what configures the calendar. - -The calendar twin of the `filter.map` retirement in objectui#4034, with one measured -difference: there is no `Array.prototype.calendar`, so `'calendar' in schema.filter` was -never true for an array-shaped filter the way `'map' in schema.filter` was true for -every one of them. Only an object-shaped filter with an own `calendar` key ever reached -the retired arm, and the calendar answers the loss of a configuration with a named -refusal on screen rather than the map's silent fallback to default field names — so this -retirement carries no dev-time diagnostic rider. - -**Migration.** A census of this repo found **zero** authored `filter: { calendar: … }` -sites (both the object-literal and JSON spellings, with positive controls and a planted -fixture proving the search fires). - -Also in this change: the `calendarConfig` memo's dependency list drops `schema.filter`, -which the function no longer reads, and gains the three flat keys it does read but the -list never named — `startDateField`, `endDateField` and `allDayField`. Dropping `filter` -removed an accidental co-trigger that used to pick those up whenever a filter changed -alongside them. diff --git a/.changeset/7712-kanban-calendar-filter-input.md b/.changeset/7712-kanban-calendar-filter-input.md deleted file mode 100644 index 4a375fe6bd..0000000000 --- a/.changeset/7712-kanban-calendar-filter-input.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-calendar': minor -'@object-ui/plugin-kanban': minor ---- - -Declare the `filter` input on every `object-kanban` / `object-calendar` -registration (objectui#7712) — the html tier stops reporting `unknown-prop` on a -key the spec declares and both renderers read. - -`ObjectKanban.tsx` sends the authored key to the query as `$filter: schema.filter` -and `ObjectCalendar.tsx` does the same, and `@objectstack/spec`'s -`ComponentPropsMap` declares `filter` on both blocks (measured: `safeParse` -accepts it, and refuses an undeclared key by name on the same call). But none of -the four registrations that publish those two renderers listed `filter` in -`inputs`, and `sdui-parser`'s `validateTree` reports `unknown-prop` for every key -no `inputs` entry claims. So an author writing the one spelling that WORKS was -told it was unknown — objectui#6678's shape, where a correct write draws the same -diagnostic as a write that does nothing. That is worse than an inert key: it -actively punishes the correct behaviour, and the honest response to it is to -delete working metadata. - -ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has -live readers on both ends, so the registrations were the side that was wrong. The -declaration is `type: 'array'` on all four, matching the `filter` that -`object-grid`'s `GRID_QUERY_INPUTS` and `object-metric` already publish, and it is -writable as one shape only because objectui#7711 landed first and retired the -object-shaped `filter.calendar` spelling — `filter` is the query filter and -nothing else. - -Declared per key against the spec rather than derived from the -`ElementDataSourceMapping` sitting beside these registrations, even though that -mapping already asserts `filter` is a live query key. Measured, that derivation -would be wrong: the kanban mapping also carries `limit`, and the spec's strict -`ComponentPropsMap['object-kanban']` **rejects** `limit` by name, so emitting it -would publish a key the save gate refuses. - -⚠️ Note for whoever meets this class next: `check:react-blocks-declaration-parity` -runs manifest → spec, one direction. A key the SPEC declares and the manifest -omits is structurally outside what that ratchet measures, so fixing these four -registrations does **not** make the next omission loud. Making that ratchet -bidirectional is its own card. diff --git a/.changeset/7714-lookup-draft-stays-client-side.md b/.changeset/7714-lookup-draft-stays-client-side.md deleted file mode 100644 index 7672973a99..0000000000 --- a/.changeset/7714-lookup-draft-stays-client-side.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-designer': minor ---- - -A half-filled relationship field stays in the client and is never PUT (objectui#7714, -maintainer ruling on objectui#7122 item 4, 2026-09-05). - -**Breaking, deliberately, and stated rather than implied.** Both metadata writers — -`MetadataService.saveFields` / `saveObject` in `@object-ui/app-shell`, and -`MetadataFieldsPage` in `@object-ui/plugin-designer` — now REFUSE a `lookup` or -`master_detail` field whose `reference` is missing, empty, blank or not a string. A -caller that previously got a PUT now gets a thrown error and **no request at all**. The -refusal is raised while the wire `fields` map is being built, so nothing is sent and -nothing is partially applied. - -**Why the draft may not leave the client.** `@objectstack/spec` 17.3.0 turned -`reference` from prose into a hard requirement on relationship types (a `custom` -refinement at path `reference`). Driven against a real 17.3.0 backend in a running -designer: creating a `lookup` and leaving its target empty PUT the whole object, -came back `422 INVALID_METADATA` at `fields..reference` — and then the NEXT -edit, to a different and already-saved field, was refused identically, because the -half-filled draft rides along inside the same document. The author sees that later -edit rendered as applied while the server has none of it, and the only escape that -does not require noticing the lookup is a reload, which discards the work. An editing -session's half-finished state belongs to the client, not to the metadata store. - -⛔ **Not** "strip the incomplete field from the body and report a successful save". -That shows the author a field the server never received — the silent-drop shape -objectstack#4001 closed. - -⚠️ **A declared divergence: objectui is stricter than the contract here.** The -predicate is `typeof reference === 'string' && reference.trim() !== ''`. Measured on -the 17.3.0 artifact, at field level and again through `ObjectSchema`, the spec -ACCEPTS a whitespace-only `reference` while refusing an absent or empty one — so -`reference: ' '` is refused by these writers on their own authority, not the -platform's. Kept deliberately: whitespace names no object (the spec's own -`ObjectSchema.fields` key grammar `/^[a-z_][a-z0-9_]*$/` admits no whitespace-bearing -name), so admitting it only moves the identical failure past the PUT and into a stored -document, where it surfaces with no field named. Filed upstream as objectstack#16126; -if the spec trims, behaviour here is unchanged and only the declaration retires. - -The refusal message diagnoses which of the four states it found — absent, empty, -non-string (`invalid_type`, a value of the wrong kind rather than a missing target), -or blank — because the repair and the consequence differ per state. - -`minor` rather than `major` per `AGENTS.md`: objectui's major tracks `@objectstack`'s, -so objectui's own breaking changes ship as `minor` with the breaking semantics stated. diff --git a/.changeset/7721-actions-reader-census.md b/.changeset/7721-actions-reader-census.md deleted file mode 100644 index 538069fd48..0000000000 --- a/.changeset/7721-actions-reader-census.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Correct a measured-false sentence in the pending objectui#7344 changeset, and widen the -reader census that produced it (objectui#7721). - -`.changeset/7344-handler-string-any-mirrors.md` said `AppAction.onClick` retires because -"nothing reads `AppComponentSchema.actions[]`". The array IS read — `@object-ui/runner`'s -`LayoutRenderer` renders its `'button'` arm as toolbar buttons and its `'user'` arm as an -avatar dropdown. The retirement itself is correct and unchanged: what no reader touches is -`onClick`, on the action or on `items[]`. That sentence was future published -`@object-ui/types` CHANGELOG copy, so it is corrected before the next release rather than -after, when the CHANGELOG is history and no longer editable. - -The census in `handler-keys-string-any-mirrors-7344.test.ts` was the origin: it scoped to -`@object-ui/layout`, `@object-ui/app-shell` and the console, which made it literally true -and its conclusion false. It is now an assertion whose population is read off the tree, so -a reader appearing in a package nobody listed fails the pin instead of narrowing the claim -in silence. - -**Nothing published changes**: no accept set moves, no exported symbol changes, no -TypeScript face moves. The edits are one parenthetical in an unreleased changeset and one -pin's comment plus its new census — which is why this declares no bump. The `minor` the -objectui#7344 changeset already declares for `@object-ui/types` is the release this copy -belongs to. diff --git a/.changeset/7722-wrapper-class-five-more.md b/.changeset/7722-wrapper-class-five-more.md deleted file mode 100644 index c5d11dcb0d..0000000000 --- a/.changeset/7722-wrapper-class-five-more.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Declare `wrapperClass` on `SwitchSchema`, `TextareaSchema`, `DatePickerSchema`, -`SelectSchema` and `ListSchema`, on both faces (objectui#7722 — the read-driven -residue outside objectui#6938's batch, one key over five more types). - -Each of `renderers/form/switch.tsx`, `textarea.tsx`, `date-picker.tsx`, -`select.tsx` and `renderers/data-display/list.tsx` reads `schema.wrapperClass` -onto its wrapper element, and neither the TypeScript interface (`form.ts`, -`data-display.ts`) nor the zod mirror (`zod/form.zod.ts`, -`zod/data-display.zod.ts`) declared the key. The reads compiled through -`BaseSchema`'s index signature (objectui#5155) and the values parsed through -`.passthrough()`, admitted unexamined. The same key, on the same class of read, -is declared on `CheckboxSchema` (objectui#6938), `FileUploadSchema` and -`FilterBuilderSchema` (objectui#6150); these five were left out only because -their doc pages never listed it. - -**minor, not patch — the published face gains five members.** objectui#6938 and -objectui#7295 graded a one- or two-key residue `patch` because "the accept set -only widens toward what already renders"; that reasoning still describes the -VALUE dimension here, but this change is the batch shape of objectui#6150 -(`minor`), and it is dispatched under the contract-review tier precisely because -it widens the PUBLISHED surface: five schemas each gain a member of the shipped -`.d.ts` and of the mirror's `.shape` that an editor completes, an annotation -checks and a validator enforces. Two verdicts move, in opposite directions: - -- **Nothing well-typed stops validating or compiling.** Key membership was never - narrow: `[key: string]: any` admitted the key on the TS face and - `.passthrough()` admitted it on the zod face, so every document that carried a - string `wrapperClass` parsed green before and parses green now, with the value - surviving the parse exactly as before. -- **A non-string `wrapperClass` is now REFUSED at the key** on these five mirrors - (`{ type: 'switch', wrapperClass: 42 }` parsed green before; it is refused now, - at `wrapperClass`). That is enforcement of the declared type, not a new - capability, but it is a behaviour change for a document that carried a - wrong-typed value under one of these five names — a value the renderer would - have interpolated into the class string as text. - -Keys outside the five are untouched: an undeclared key of any type is still -admitted unexamined on all five mirrors, pinned per mirror with a control key -the renderer does not read. `InputSchema.wrapperClass`, declared on the TS face -only, is a recorded row of the parity ledger (`UnmirroredDeclared`) and stays -there; the new sweep pin carries it as a self-expiring exemption. diff --git a/.changeset/7735-zod-mirrors-stop-authoring-defaults.md b/.changeset/7735-zod-mirrors-stop-authoring-defaults.md deleted file mode 100644 index 8944d97e34..0000000000 --- a/.changeset/7735-zod-mirrors-stop-authoring-defaults.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': patch ---- - -The zod mirrors stop authoring defaults; the renderer's fallback is the one -authoritative default (objectui#7735, director ruling, decision batch #69, -2026-09-07 — maintainer reply 「其他同意」). - -**A validator validates; it does not write values into an author's document.** -`.default(v)` on a mirror member is not documentation — `parse` SUBSTITUTES `v` -into the output when the key is absent. One authored document therefore had two -shapes depending on whether it had been through `safeValidateSchema`, and where -the mirror and the renderer disagreed the mirror silently won: - -| key | mirror wrote | the renderer applies | -|---|---|---| -| `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | -| `FlexSchema.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | - -A `container` omitting `maxWidth` rendered `max-w-xl` as authored and `max-w-lg` -after a round-trip through the mirror; `'center'` is a value neither `flex` nor -`stack` applies at all. `StackSchema` already declared no defaults, so the file -was not even self-consistent. - -**What changed.** All 41 `.default()` call sites under `packages/types/src/zod/` -are removed — `layout.zod.ts` 22, `crud.zod.ts` 11, `form.zod.ts` 5, -`views.zod.ts` 2, `app.zod.ts` 1. `@object-ui/components` reconciles the third -face objectui#8229 found: `flex`'s registration `defaultProps.align` seeded -`'center'`, the value its own renderer never applies, so a designer-made node -laid out differently from a hand-authored one; it now seeds `'start'`. - -**Accept set unchanged.** Every one of the 41 was spelled -`.optional().default(v)`, so none was carrying optionality: nothing that -parsed before is refused now, and nothing refused before is accepted. This is a -narrowing of `parse` OUTPUT only. - -**⚠️ Breaking for consumers that read `result.data`.** `validateSchema` / -`safeValidateSchema` no longer substitute: for a document that OMITS one of these -keys the key is now ABSENT from the parsed output instead of carrying a value the -document never authored. Every one of the 41 keys changes output this way. If you -read a default off the parsed document rather than applying your own fallback, -apply the fallback yourself. - -**And rendering changes too, wherever the mirror's value differed from the -renderer's.** An earlier draft of this note said rendering does not change on the -ground that "the two keys that disagreed were the bug"; that was the triage -sample of 7 keys, not a measurement of the population. Auditing all 22 layout -keys against their renderers finds **six** disagreements, not two: - -| key | the mirror wrote | what the renderer applies when the key is absent | -|---|---|---| -| `container.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | -| `flex.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | -| `grid.columns` | `3` | `grid.tsx`: `let baseCols = 2` | -| `text.variant` | `'body'` | `text.tsx`: none — a bare node, no typography class (objectui#6942) | -| `resizable.withHandle` | `true` | forwarded bare; `undefined` draws NO grip | -| `page.template` | `'default'` | `page.tsx`: a null template falls through to the `pageType` dispatch | - -So a consumer that renders `result.data` sees a different result on all six. What -does NOT change is rendering inside this repository: the render path never ran -this validator at all — `SchemaRenderer` calls the STRUCTURAL `validateSchema` -from `@object-ui/core`, which returns `{valid, errors}` and hands the renderer -the author's own object. The four modules that do import `@object-ui/types/zod` -were censused: two read only `result.success`, one discards `result.data`, and -`os validate` reads only `type` / `id` / `label` / `title` / `children`, none of -which ever carried a default. The four JSDoc `@default` tags that described the -mirror rather than the renderer (`grid.columns`, `text.variant`, -`resizable.withHandle`, `page.template`) are corrected in the same change. - -Note that `safeValidateSchema` still substitutes through subschemas imported by -reference from `@objectstack/spec` (`app.active`, `object-view.navigation.*`, -`list-view.sharing.type`, and others). Those values are written in that package, -not this one, and are unaffected by this change. diff --git a/.changeset/7741-list-import-mappings-discriminate.md b/.changeset/7741-list-import-mappings-discriminate.md deleted file mode 100644 index 18530a9e29..0000000000 --- a/.changeset/7741-list-import-mappings-discriminate.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -`listImportMappings` no longer renders a refused door as "no mapping is registered" -(objectui#7741). - -`ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the -import wizard hides its saved-mapping selector on an empty list. So "the server served -zero mappings" and "the server refused, or broke" produced the identical UI on every -deployment — the feature simply absent — with a `console.warn` as the only -discriminator, in the browser console, with nothing pointing at it. That silence did not -merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter -(objectstack#14026 was filed, routed and worked by two seats against a wizard that had -been correct since `@object-ui/data-objectstack@17.1.0`). - -**The empty-list return is unchanged.** `listImportMappings` still answers -`Promise` and still never throws, on every arm including the loud ones — this is -a channel added ALONGSIDE that contract, not a change to it. - -- **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe - channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a - metadata read failed in a way that is NOT the supported "this deployment does not - serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the - object, whether the server `refused` this caller or the answer was `unreadable`, and - the server's own ADR-0112 code, HTTP status and message. -- **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported - so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code` - first, the status only where no code was declared — and never "is the result an empty - array", which is what both conditions produce and so can never tell them apart. -- **The older-server case stays quiet.** A deployment that does not serve the `mapping` - kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list - door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no - event. That is a real, supported deployment shape and it must not become a visible - fault. -- **The console now says so.** `AdapterProvider` subscribes to the new channel and - renders a warning toast naming the object, the remedy and the server's own words, so a - user without devtools open can tell "there are none" from "we could not find out". - Three new `console.importMappings*` keys ship in all ten locale packs. - -This applies framework #13906 decision 1 option A — *a thing that could not be READ is -not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not -a new principle. diff --git a/.changeset/7744-chart-type-internal-only.md b/.changeset/7744-chart-type-internal-only.md deleted file mode 100644 index 3d78e64162..0000000000 --- a/.changeset/7744-chart-type-internal-only.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -`@object-ui/plugin-charts`: documentation and a pin only — no published behaviour changes. -`normalizeSeries` returns the identical output for the identical input (measured: 336 input -cases, byte-identical JSON before and after; the probe was itself shown sensitive — a reader -reorder moves 49 of those 336). - -`normalizeSeries` resolves the per-series family as `str(raw.chartType) ?? str(raw.type)` — -the internal spelling FIRST. `chartType` is the carrier of the renderer's internal `dataKey` -series shape (`ChartRenderer`'s `series?` union declares it on that arm and on no other); -the AUTHORING face refuses it by name via `ChartDataSeriesSchema` (objectui#7694 / PR #7737). -Because the one function normalizes both shapes, an author who skips validation still gets -`chartType` honoured at runtime while the validator refuses that same key — the two faces -disagree. - -objectui#7744 ruled that the split is annotated and pinned, NOT retired at the reader: -retiring the limb would change what the internal producers (`DashboardRenderer`, `ObjectView`, -the dataset path) render, which is a reader-side decision of its own. So the limb keeps its -behaviour and gains a docblock that delegates the refusal's rule to where it lives, plus -`chartType-internal-only-7744.test.ts`, which pins the RELATIONSHIP the two files each held -half of: the key the validator refuses is the key the reader prefers. Bringing the faces into -agreement from either side now goes red and names the side that moved. diff --git a/.changeset/7745-formatdate-options-style.md b/.changeset/7745-formatdate-options-style.md deleted file mode 100644 index 4a9296ffbe..0000000000 --- a/.changeset/7745-formatdate-options-style.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`formatDate` reads `options.style` (objectui#7745). - -`DateDisplayOptions` is the one bag `formatDate` / `formatRelativeDate` / -`formatDateTime` share. `style` was added to it for `formatDateTime`'s `'compact'` -grid face (objectui#7443, PR #7621) and only `formatDateTime` read it, so on -`formatDate` the key was inert — and inert beside a POSITIONAL parameter of the same -name. `formatDate(v, undefined, { style: 'short', locale: 'en-US' })` rendered -`Jul 4, 2024`, the default face, with no diagnostic; it now renders `Jul 4, '24`. -This is the additive half of the maintainer's long-run ruling on objectui#7443: -both functions accepting `options.style`. - -**The precedence is pinned: the positional argument wins.** `options.style` is -consulted only when the positional slot is `undefined` (`??`, not `||`, so `''` -still counts as given). That is the only direction that is purely additive — it -fires exactly on the input that is a silent no-op today, so no call that renders a -face today renders a different one after. The reverse would let a key aimed at a -SIBLING function outrank an argument written for this call: the bag is shared, and -carrying `{ style: 'compact', locale }` built for `formatDateTime` into -`formatDate(v, 'short', bag)` must not cost that call its short face. - -**What changes for you.** Only `formatDate(value, undefined, { style: 'short' | 'relative' })` — -a call that silently rendered the default face before. Every call that passes the -style positionally, and every `formatDateTime` / `formatRelativeDate` call, renders -byte-identically to before. - -`formatRelativeDate` still does NOT read `style`; the ruling names `formatDate` -only. Its out-of-window fallback to `formatDate` strips the key so that the new read -cannot leak in through the delegation — which also keeps -`formatRelativeDate(v, { style: 'relative' })` from recursing. diff --git a/.changeset/7747-datetime-format-cast-narrowing.md b/.changeset/7747-datetime-format-cast-narrowing.md deleted file mode 100644 index b9f09b92e6..0000000000 --- a/.changeset/7747-datetime-format-cast-narrowing.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Narrow the `DateTimeCellRenderer` style cast from `as any` to the subtype that -actually declares the property (objectui#7747). - -`const style = (field as any)?.format || 'compact'` becomes -`(field as DateTimeFieldMetadata | undefined)?.format`. Some cast is -load-bearing — `FieldMetadata` is a 37-member union and `BaseFieldMetadata` -carries no `format`, so the bare read is `TS2339` — but `as any` was wider than -the job: `DateTimeFieldMetadata` is exported from `@object-ui/types`, which this -file already imports from, so the narrow cast was available at zero cost. The -difference that buys: `as any` would also have silenced a typo in the property -name, and the narrow cast will not. - -Type-level only; no package is released by this change. The emitted expression -is identical, so every cell renders exactly what it rendered before — -`'compact'` and an absent or empty `format` keep the compact face, and any -other value keeps selecting the verbose `formatDateTime` default. diff --git a/.changeset/7752-analytics-where-lowering.md b/.changeset/7752-analytics-where-lowering.md deleted file mode 100644 index 2ec9ef7aea..0000000000 --- a/.changeset/7752-analytics-where-lowering.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -Array filters on analytics aggregates were posted un-lowered and refused by the runtime with 400; they are now lowered to the canonical `FilterCondition` before the wire. - -An `element:number` or an `object-metric` whose filter is authored as an array (`[{ field, operator, value }, ...]`, a comparison tuple, or an `and`/`or` group) reached `POST /analytics/query` as an array. That route parses the body with `AnalyticsQueryRequestSchema` first, and its `where` is a `FilterCondition`, so the widget answered `400 Invalid AnalyticsQuery body: where: ...` instead of its number — leaving the MongoDB-style record as the only authoring form that still worked. - -`aggregate()` now lowers the array through `parseFilterAST`, the single sink `@objectstack/spec` names for turning a `FilterArray` into a `FilterCondition`, so the posted `where` is the shape the wire declares. An empty array posts no `where` at all, a record-shaped filter is unchanged, and an array the sink cannot lower — an infix join such as `[condA, 'or', condB]` — is refused with this adapter's `INVALID_FILTER` / 400 error rather than posted or silently dropped into an unfiltered aggregate. diff --git a/.changeset/7767-collapsible-trigger-union.md b/.changeset/7767-collapsible-trigger-union.md deleted file mode 100644 index 508d6c78c8..0000000000 --- a/.changeset/7767-collapsible-trigger-union.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`CollapsibleSchema.trigger` now declares the node array its Zod mirror, its runtime and its own shipped default already accept** (objectui#7767). - -`trigger` on `CollapsibleSchema` widens from `string | SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face, and stays required. The dropped `string |` half was redundant rather than an extra: `SchemaNode` is `BaseSchema | string | number | boolean | null | undefined`, so `string | SchemaNode` already denoted exactly `SchemaNode` — a bare string trigger type-checks before and after. `SchemaNode` itself is unchanged. - -This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/disclosure.zod.ts` already spelled this key `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: `renderers/disclosure/collapsible.tsx` hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and that same registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying the renderer's own default into a typed document is no longer a type error against the type that shipped it. The docs page's `trigger` row follows the declaration. - -This is the eighth member of the change objectui#7081 made to the overlay family, carried out under the same ruling (2026-09-03 on that card): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). diff --git a/.changeset/7772-page-block-kanban-group-by-control.md b/.changeset/7772-page-block-kanban-group-by-control.md deleted file mode 100644 index 1427679fc8..0000000000 --- a/.changeset/7772-page-block-kanban-group-by-control.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -**Breaking for the page designer's output:** the page-block inspector's `object-kanban` -panel now authors `groupBy` (the key the board reads) instead of `groupField` (the key it -never read), and offers the `limit` control the schema has declared since objectui#7322. - -**What was measured, on this branch's base (`0c8dbc49`).** `BLOCK_CONFIG['object-kanban']` -offered exactly four controls — `objectName` / `groupField` / `titleField` / `cardFields`. -`ObjectKanbanSchema.groupBy` is REQUIRED on both published faces (`objectql.ts:2765 -groupBy: string`, no `?`; `objectql.zod.ts:1001 groupBy: z.string()`, no `.optional()`) and -had **no control at all**, while `groupField` — the only control able to set grouping — has -been a `retirementTombstone()` on the Zod face and `?: never` on the TS face since -objectui#7322. `ObjectKanban.tsx` reads `schema.groupBy` at thirteen sites and `groupField` -at zero (control: those same thirteen hits in the one query, so the zero is a reading). - -Run against the node those four controls produced, `ObjectKanbanSchema.safeParse` reported -**two** issues, not one: `groupBy` "expected string, received undefined", and `groupField` -carrying the retirement guidance verbatim. So this panel could not author a valid -`object-kanban` node however it was filled in, and the board it produced grouped nothing -with no diagnostic on any face. Both halves are now pinned against the schema itself rather -than against a spelling, in `previews/__tests__/block-config.test.ts` — -`scripts/check-designer-field-key-parity.mjs` judges `PAYLOAD_SHAPES` and does not read -`BLOCK_CONFIG`, so nothing mechanical was watching this table. - -`limit` is not polish: `ObjectKanban.tsx` sends it as a real `$top` -(`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and renders every fetched record into a lane -with no pagination, so a board over 100 records was silently truncated with no way to widen -it. The new number box states `100` — `DEFAULT_KANBAN_LIMIT`, read from the renderer's own -source by its pin — as the value that applies while it is empty. - -**Stored pages.** Documents saved by every released build since -`@object-ui/app-shell@17.5.0` can carry `properties.groupField` on an `object-kanban` block. -The inspector's read door now strips it (`stripRetiredBlockProps`, keyed to -`RETIRED_BLOCK_PROP_KEYS`), so an edit-and-save round-trip comes out without the retired -key — no migration pass, and no blanket unknown-key purge: every other key the designer -does not render still survives. It is a strip and **not** a migration into `groupBy` on -purpose: reading the old spelling into the new box would be a second de-facto alias -(AGENTS.md #0.1) for a value no board ever acted on. - -The **view-level** `kanban.groupField` alias (`core/src/utils/normalize-list-view.ts`) is -live and untouched — node-level retirement and the view-level alias are different things. - -The two locale entries move with the control (`engine.inspector.pageBlock.field.object-kanban.groupBy`, -plus a new `.limit`); the English wording is unchanged, so the panel reads the same as it -always did. diff --git a/.changeset/7773-kanban-adapter-groupfield-write.md b/.changeset/7773-kanban-adapter-groupfield-write.md deleted file mode 100644 index 3e50cbb8b0..0000000000 --- a/.changeset/7773-kanban-adapter-groupfield-write.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/plugin-view': minor -'@object-ui/plugin-list': minor ---- - -The two kanban adapters stop writing the retired `groupField` onto the -`object-kanban` node they generate (objectui#7773). `groupBy` — the key the -renderer actually reads — is unchanged and is now the only lane key emitted. - -**What was measured, on this branch's base (`a915064e`).** Both adapters emitted -the key twice: - -``` -packages/plugin-view/src/ObjectView.tsx:1362 groupField: groupBy, -packages/plugin-list/src/ListView.tsx:2500 groupField: laneField, -``` - -The `object-kanban` renderer never read it: `groupField` has ZERO hits anywhere -under `packages/plugin-kanban/`, against a control of thirteen `schema.groupBy` -read sites in `ObjectKanban.tsx` from the same query — so the zero is a reading, -not a blind grep. The write was inert; the board grouped by `groupBy` and -`groupField` rode along unread. - -**Why it is removed rather than tolerated.** objectui#7322 RETIRED -`groupField` on this node on both published faces — `groupField?: never` on the -TypeScript interface and a `retirementTombstone()` in the Zod mirror, which -refuses an authored value BY NAME. So the adapters were producers emitting a -node their own published contract rejects. That was harmless only because a -generated node never reaches the mirror at runtime (`SchemaRenderer` runs the -structural `validateSchema`, not `safeValidateSchema`) — but the CLI's -`os check` / `os validate` DO run the mirror, so the identical node was already -refused when authored by hand and admitted when generated. This closes that -split. - -**What changes for a consumer.** Nothing on any documented path: the renderer's -behaviour is byte-identical, because it never read the key. A host that -registers its own `object-kanban` component and reads `props.schema.groupField` -off the generated node now reads `undefined` — read `groupBy` instead, which -carries the same value and always did. Graded `minor` rather than `patch` for -exactly that narrowing, following the repo convention that objectui's own -breaking changes ship as `minor` (AGENTS.md 版本号策略, mechanically enforced by -`scripts/check-changeset-no-major.mjs`). - -**Who is NOT affected — the boundary is node-local.** Every VIEW-LEVEL -`groupField` read is untouched and still live: it is a legacy alias of the -spec's `groupByField` on the kanban *view config*, mapped by -`normalize-list-view.ts`, and both adapters still resolve lanes through it -(`ObjectView.tsx`'s `kanbanCfg.groupField ||`, `ListView.tsx`'s -`groupByField || groupField`). Authoring `options.kanban.groupField` on a -`list-view` or `object-view` keeps working exactly as documented in -`packages/plugin-list/README.md`. `groupField` is dead only on the generated -`object-kanban` NODE. - -The two tests that pinned the duplicate write are TURNED, not deleted — they now -assert the key is absent, so restoring the write reddens them instead of being -silently re-blessed by a missing assertion. diff --git a/.changeset/7786-storage-spy-counter-probes.md b/.changeset/7786-storage-spy-counter-probes.md deleted file mode 100644 index 2d931d93f7..0000000000 --- a/.changeset/7786-storage-spy-counter-probes.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Test-only repair of two `Storage.prototype` spy instruments that graded green -without measuring what their names claimed (objectui#7786). No package is -released by this change and no runtime source is touched. - -- `apps/console/src/__tests__/bootSplash.test.ts` — `it('survives a browser - with no localStorage access')` injected its fault with - `vi.spyOn(Storage.prototype, 'getItem')`, which never reached the script - under test: happy-dom hands `localStorage` out through a proxy that has - already bound `getItem`, and `vitest.setup.base.ts` may swap the store for a - plain object that never inherited from `Storage.prototype` at all. Measured - on Node 22.22.2, the version CI pins, a direct `localStorage.getItem(...)` in - that test did not throw — so the case asserted that an ordinary boot does not - throw, under a name promising a blocked browser. The fault is now injected - where the script resolves it (the property-replacement shape - `packages/i18n/src/__tests__/provider-locale-persistence.test.tsx` already - uses against the same hazard), a counter-probe asserts the throw is - observable before anything is graded on its absence, and a stored `dark` - choice makes the expected `light` outcome reachable only when storage is - genuinely unreadable. -- `packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx` — the - load-bearing `expect(setItem).toHaveBeenCalledTimes(0)` is a negative - assertion, which a blind spy satisfies for the wrong reason. It now proves - the spy observes two writes the test makes itself, in both `sessionStorage` - and `localStorage`, before asserting anything about writes the component did - not make. diff --git a/.changeset/7787-navigation-overlay-doc-example.md b/.changeset/7787-navigation-overlay-doc-example.md deleted file mode 100644 index 40b0f34ba5..0000000000 --- a/.changeset/7787-navigation-overlay-doc-example.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Point `navigation-overlay.tsx`'s file-header `@example` at the shared record-source -reader — `resolveRecordSourceObjectName(schema, dataConfig)` from `@object-ui/core` — -instead of the `objectName: schema.objectName` spelling PR #7648 retired from -`useNavigationOverlay`'s own doc block (objectui#7787). That fix never reached this -file: different file, different package, a block documenting a different symbol. The -example follows the hook's doc block rather than restating its rule, because a ruling -written out twice is a ruling one of whose copies rots — which is what happened here. - -`KNOWN_HAND_SPELLINGS` in `scripts/check-doc-example-shared-reader.mjs` carried this -file as its one row and is drained in the same change, as that ledger requires: a row -naming a defect the gate no longer finds fails the gate rather than sitting there as a -waiver for nothing. - -Doc comment and tooling only; no published behaviour changes and no package is -released by this change. diff --git a/.changeset/7791-specparity-cwd-root.md b/.changeset/7791-specparity-cwd-root.md deleted file mode 100644 index d6dd46ff09..0000000000 --- a/.changeset/7791-specparity-cwd-root.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Test-only: the `CapabilityMultiSelectField` spec-parity pin -(`packages/fields/src/widgets/CapabilityMultiSelectField.specParity-6285.test.tsx`) -resolved the source it reads against `process.cwd()`, so it failed under the -package-level `test` script (objectui#7791). Nothing ships — no runtime source -changed, and no package is released by this change. - -The root is now derived from the test file's own `import.meta.url` instead of -from the cwd, so the pin reaches the same verdict under every invocation. Both -readings are 133 files / 2178 tests green after the change; the package-level -one was 1 file / 2 tests red before it. diff --git a/.changeset/7798-getdataconfig-doc-example.md b/.changeset/7798-getdataconfig-doc-example.md deleted file mode 100644 index c6676c92f3..0000000000 --- a/.changeset/7798-getdataconfig-doc-example.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Point the two `@example` blocks that still taught `getDataConfig(schema)` at the -shared record-source producer `resolveRecordSourceConfig(schema)` from -`@object-ui/core` (objectui#7798): the `useNavigationOverlay` block in -`packages/react/src/hooks/useNavigationOverlay.ts` and the -`resolveRecordSourceObjectName` block in `packages/core/src/utils/record-source.ts`. - -`getDataConfig` is exported by nothing. It survives only as a file-local wrapper in -`plugin-map` and `plugin-grid` — and both of those already delegate their ladder to -`resolveRecordSourceConfig`, keeping only the array shorthand above it — so a reader -outside those two files could not write the line either example taught: there was no -module to import it from. The examples now spell `dataConfig` the way the live call -sites spell it (`plugin-calendar`, `plugin-tree`), which is the same judgement PR #7797 -applied to `navigation-overlay.tsx`: follow the live call sites, not another doc block. -The `useNavigationOverlay` example also names the import, so the line it teaches is -copyable as written. - -Doc comments only; no published behaviour changes and no package is released by this -change, so the frontmatter is empty. diff --git a/.changeset/7799-cwd-rooted-package-tests.md b/.changeset/7799-cwd-rooted-package-tests.md deleted file mode 100644 index 3bb385375d..0000000000 --- a/.changeset/7799-cwd-rooted-package-tests.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Test-only: 13 test files under `packages/*/src` built a repo path out of the -process cwd, so they reached a different verdict under their package's own -`test` script (`vitest run --root ../.. packages/PKG/`, which is what -`pnpm --filter PKG test` and `turbo run test` run) than under the repo-root form -CI runs — `--root` moves vitest's root and leaves `process.cwd()` in the package -directory (objectui#7799). - -Nothing ships: no runtime source changed, and no package is released by this -change. The root is now derived from each test file's own `import.meta.url`, -copying the precedent landed for objectui#7791, so every one of them reaches the -same verdict under both invocations. - -Readings for the whole class, one tree, cwd the only variable — 17 files / -373 tests: - -- repo root: 373 passed / 0 failed before, 373 passed / 0 failed after -- package dir: 325 passed / 48 failed before, 373 passed / 0 failed after - -`pnpm --filter @object-ui/components test`, the acceptance invocation, goes from -1 file / 6 tests red to 233 files / 2166 tests green. diff --git a/.changeset/7821-manage-refresh-failure-is-not-deletion.md b/.changeset/7821-manage-refresh-failure-is-not-deletion.md deleted file mode 100644 index e67ce38cba..0000000000 --- a/.changeset/7821-manage-refresh-failure-is-not-deletion.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A failed package-list refresh no longer reads as "the package was deleted" and no -longer evicts the author from the Studio (objectui#7821). - -`onManageChanged` — the callback the Studio's `PackageDetailSheet` fires after every -package lifecycle action (disable / duplicate / delete / publish / manifest edit) — -refreshed the list into a local `list` initialised to `[]` and swallowed the rejection -under a comment reading "keep the stale list". That is true of the `pkgs` state, which -is simply not written, and false of the local, which stayed `[]`. So after a failed -`GET /api/v1/packages` the `!list.some(...)` check three lines down was -unconditionally true, the code took the branch labelled `// Deleted`, and — when the -managed package was the one under the editor — navigated away with `list[0]` -undefined, i.e. to `/home`. One transient 503, network blip or auth expiry threw the -author out of the editor with no toast and no confirmation, while the package was -still there. - -The local now starts as `null` — "the refresh told us nothing" — and only a list that -actually came back, without the managed package in it, is read as a deletion. A -failure draws no inference at all: no navigation. It is reported instead, through the -posture this surface already has (objectui#7368): `formatMetadataError` on the shared -`studio-package-list` sonner id, and recorded so the switcher reads `failed` rather -than presenting a now-stale list as current. A real deletion navigates exactly as -before — to the first surviving package, or `/home` when none is left. - -Still a `.catch` and still no retry: one 503 must not take the Studio down, and no -retry policy has been ruled on. diff --git a/.changeset/7823-objectview-chart-relay-whole-block.md b/.changeset/7823-objectview-chart-relay-whole-block.md deleted file mode 100644 index 028d672f43..0000000000 --- a/.changeset/7823-objectview-chart-relay-whole-block.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Forward the authored `chart:` block WHOLE from the object-view relay to `ListView` -(objectui#7823). - -`ObjectView`'s `renderListView` builds the list schema by spreading the object's list -schema and then relaying the active view's visualization blocks under `options`. Every -sibling block is forwarded whole; `chart` was a hand-listed projection of exactly six -keys — `chartType`, `xAxisField`, `yAxisFields`, `aggregation`, `series`, `config` — the -pre-ADR-0021 key set, frozen. The whole ADR-0021 (objectui#1890) authoring shape -(`dataset` / `dimensions` / `values`) and the legacy `categoryField` / `valueField` -spelling had no rung, so a view that declared them reached `ListView` with its binding -stripped, and `ListView` could not tell that from a view that had declared nothing. - -**What changes for authors.** On the object-view route, a list view that declares an -ADR-0021 chart block and whitelists `chart` in `appearance.allowedVisualizations` is now -offered the Chart toggle and renders from the dataset it named. Before this fix the -capability gate added by objectui#7544 was handed six `undefined` keys, correctly -answered "nothing declared" about a view whose author had declared everything, and -ADR-0047 filtered the author's own whitelist down to `['grid']` — no toggle, no -diagnostic. The legacy `xAxisField` / `yAxisFields` spelling did survive the projection -and did resolve, so the two authoring shapes behaved differently on this route for -reasons that lived entirely in that one object literal. - -**Not a wider whitelist.** The projection is replaced by a pointer, not extended from -six keys to nine: a hand-listed key set is a copy, and copies rot silently — three more -keys would buy ADR-0021's correctness while re-arming the identical trap for the next -block key, and nothing would fire then either, because the relay's view definition is -`Record` and a missing rung is invisible to `tsc` (objectui#7559 owns that -mechanism). Forwarding whole is safe because `ListView` reads this block by name at both -of its readers — the capability gate's `resolveListChartBinding` and the `chart` render -branch — and never spreads it. - -A view that declares no chart block, an empty block, or a block with no binding at all -is still offered no Chart toggle: the relay now forwards `undefined` instead of the old -permanently-truthy husk of six `undefined` keys. diff --git a/.changeset/7828-calendar-gantt-ladder-harness.md b/.changeset/7828-calendar-gantt-ladder-harness.md deleted file mode 100644 index 594f3d76b9..0000000000 --- a/.changeset/7828-calendar-gantt-ladder-harness.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Test-only change; no published behaviour changes. - -objectui#7828 ports the three harness guards from objectui#7527 (`c2fc261f5`), -plus PR #7826's bound capture, from the timeline `colorField` ladder fixture to -its two siblings — `ObjectCalendar.colorFieldLadder-7243.test.tsx` and -`ObjectGantt.colorFieldLadder-7243.test.tsx`. Their readiness predicate now -identifies WHICH render wrote instead of counting arity, each helper returns the -array that predicate accepted rather than re-reading a module global, every -render is unmounted in a `finally`, and no renderer may be alive when one is -mounted. - -No runtime code is touched, and neither component's behaviour was changed or -found wanting: the two-paint window the guards exist for was measured for and is -absent on both (both gate their record query on the settled object schema), so -this is a fixture that can no longer be broken by the ordinary next edit, not a -fix for a failure. diff --git a/.changeset/7837-vscode-export-react-side-effect-import.md b/.changeset/7837-vscode-export-react-side-effect-import.md deleted file mode 100644 index a5e719c67c..0000000000 --- a/.changeset/7837-vscode-export-react-side-effect-import.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'object-ui': patch ---- - -Fix the phantom import the VS Code extension's **Export to React** command wrote into -every file it generated (objectui#7837). - -`generateReactComponent()` emitted a preamble that imported `registerDefaultRenderers` -from `@object-ui/components` and then called it. That symbol is on **no export** of -that package: its built `dist/index.d.ts` carries exactly one `register*` name, -`registerPlaceholders`, and `registerDefaultRenderers` appears **0 times** in either -`dist/index.d.ts` or `dist/index.js`. So every file the command produced failed to -compile with `TS2305` naming a symbol the user never typed. - -`@object-ui/components` registers its renderers as an **import side effect** — -`sideEffects: true` in its manifest, `import './renderers'` in its barrel under the -comment `Register all ObjectUI renderers (side-effects)`, and **114 `register(` call -sites** at module scope in the built `dist/index.js`. There is no registration function -to call, so the generated preamble now imports the package for the side effect and says -why. Same spelling the root README landed for objectui#7417. - -`packages/vscode-extension/DESIGN.md`, which documented the identical two lines, is -corrected in the same commit so the design record does not freeze the defect. - -No public surface moved: no export added, no signature changed. diff --git a/.changeset/7839-tree-arm-own-reference.md b/.changeset/7839-tree-arm-own-reference.md deleted file mode 100644 index c91d9b08c5..0000000000 --- a/.changeset/7839-tree-arm-own-reference.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-tree': patch ---- - -`ObjectTree`'s parent-pointer auto-detection accepts a `tree` field only when it is -this object's own (objectui#7839, objectstack#14892 follow-up). - -A hierarchy is parent/child **within one object**, so `@objectstack/spec` refuses a -`tree` field whose `reference` names any other object (`refuseForeignTreeReference`); -`reference` stays optional on a `tree`, where it is a redundant self-annotation. -`detectParentField` returned the first `type: 'tree'` field whatever its `reference` -said, so a foreign-shaped one was silently picked as the parent pointer and the forest -was grouped on a pointer into a table it does not point at. It now mirrors the spec's -own kernel predicate `hasDetectableParentField` term for term — accept when `reference` -is absent, or when it equals the bound `objectName` — and the `objectName` guard comes -with it: an object whose name we do not know cannot be self-referenced. - -Skipping rather than returning also removes a masking bug the old early-return hid: a -foreign `tree` declared before a self-referencing `lookup` used to win by position and -the lookup never got its turn. The `lookup` / `master_detail` arm is otherwise -unchanged. - -Reachability is narrow and stated rather than oversold: the refused shape does not -survive `ObjectSchema.parse` on a spec carrying that rule, so it is unreachable from -parsed metadata and reachable from a hand-built one — `getObjectSchema` is a required -member of the published `DataSource` interface, so a third-party implementation reaches -this reader raw. Note also that the renderer tightens **ahead** of the spec copy this -repo installs: `@objectstack/spec@17.2.0` still accepts the foreign shape at parse -(measured), so until that pin moves this function is the only door. diff --git a/.changeset/7842-objectview-datasource-contract.md b/.changeset/7842-objectview-datasource-contract.md deleted file mode 100644 index df6b32db5e..0000000000 --- a/.changeset/7842-objectview-datasource-contract.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in `@object-ui/plugin-view`, plus its pin. -`ObjectViewProps.dataSource` promised "If not provided, falls back to -SchemaRendererProvider context" one line above a declaration that has always -been required, so one of the two had been wrong for the prop's whole life -(objectui#7842). Measured: the TYPE is right. `ObjectView` holds no context read -at all — no `useContext`, no `SchemaRendererContext`, and not -`useElementDataSource`, the hook `@object-ui/react` publishes for exactly this -fallback; the prop's value travels into `useSettledSchema` (no context read -either) and into two effects that GUARD on it rather than resolve one. - -The sentence was misfiled rather than invented: `ObjectViewRenderer`, the -renderer registered for the `object-view` and `view` schema tags, is what really -reads `useContext(SchemaRendererContext)` and hands `ctx?.dataSource` down to -this prop. So a schema-driven host does get the provider's adapter; a caller -writing `` in TSX does not, and tsc refuses the omission at the -call site. The JSDoc now says that, says what happens when the prop is absent -anyway (every read site returns early, the value is forwarded verbatim to a -child whose own `dataSource` is optional, and the view renders empty rather than -throwing), and records that making the prop optional would widen a published -accept set. - -The new pin keeps both halves: `ObjectView` under a real `SchemaRendererProvider` -never touches the provider's adapter (behind a live control that proves the same -probe DOES observe an adapter passed as the prop), and the registered renderer -still resolves one from context. No published behaviour changes and no -declaration moved. diff --git a/.changeset/7849-unevaluated-diagnostic-lists-properties.md b/.changeset/7849-unevaluated-diagnostic-lists-properties.md deleted file mode 100644 index c2e0864b36..0000000000 --- a/.changeset/7849-unevaluated-diagnostic-lists-properties.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@object-ui/react": patch ---- - -The unevaluated-expression diagnostic now lists `properties` among the channels -that evaluate and read back, matching the sibling `props`-bag diagnostic that -tells authors to write their keys there. - -Both messages are dev-build diagnostics in `SchemaRenderer`, and one node can -trip both. They disagreed: `propsBagDiagnostic` said *"`props` is NOT hoisted -onto the node — only `properties.*` is … Write them under `properties` -instead"*, while `unevaluatedExpression` enumerated *"channels that do evaluate -and read back today"* as `content` or host-side resolution — omitting -`properties`. An author who hit both was told to use a channel the other message -said did not work. - -The enumeration was the wrong half, established by measurement rather than by -reading the `COMPAT` label on the hoist. `properties` has no retirement on -record: `@objectstack/spec@17.2.0` calls the vocabulary carried there *"ALIVE — -this is not dead surface to retire under ADR-0049"*, keeps -`PageComponentSchema.properties` as the open carrier on purpose, gates it at the -authoring door, and tombstones no part of it via `retiredKey()`. In this repo -`props`, not `properties`, is the spelling annotated as the legacy alias. - -Diagnostic text only — no evaluation, hoist or schema behaviour changed. diff --git a/.changeset/7862-vscode-export-react-unused-import.md b/.changeset/7862-vscode-export-react-unused-import.md deleted file mode 100644 index e75f1449ea..0000000000 --- a/.changeset/7862-vscode-export-react-unused-import.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'object-ui': patch ---- - -Drop the unused `import React from 'react'` the VS Code extension's **Export to React** -command wrote into every file it generated (objectui#7862). - -The generated file's only JSX is a single SchemaRenderer element, so under the automatic -JSX runtime — `"jsx": "react-jsx"`, what a new Vite or Next project is configured with — -the `React` identifier was never read. Measured on this branch against the built -`dist/index.d.ts` of `@object-ui/react` and `@object-ui/components`, TypeScript 6.0.3: -the emitted file compiled clean under `react-jsx` + `strict` (exit 0), and under the same -config plus `noUnusedLocals: true` it failed with -`TS6133: 'React' is declared but its value is never read` — so a consumer with that -option on could not compile the file the command had just handed them. - -The preamble now says in a comment that it assumes the automatic runtime and that the -import goes back only on the classic `"jsx": "react"` transform, which is the one -configuration this costs: measured, that file reports one diagnostic about `React` being -out of scope. Nothing in the extension emits or promises a `jsx` setting — the string -does not occur anywhere in the package — and the published docs page for the command -already showed the output without the import. - -A new pin, `src/__tests__/export-to-react-compiles.test.ts`, now extracts the template's -PRODUCT and compiles it under `noUnusedLocals`, rather than matching substrings in the -generator. The sibling objectui#7837 pin was green for the whole life of this line -because it never named it; a compile closes the class instead of one member of it. Its -positive control runs on every invocation: re-adding the import must report TS6133, so -the harness cannot go quietly, permanently green. - -`packages/vscode-extension/DESIGN.md`, whose section 4 hand-mirrors this preamble, is -corrected in the same commit so the design record does not freeze the defect — the -spelling objectui#7837 landed for the same file. What binds the two copies together is -still nothing, and objectui#7976 holds that question. - -No public surface moved: no export added, no signature changed. diff --git a/.changeset/7874-timeline-relative-defaults-retired.md b/.changeset/7874-timeline-relative-defaults-retired.md deleted file mode 100644 index c817d5fbe8..0000000000 --- a/.changeset/7874-timeline-relative-defaults-retired.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Internal residue removal, no published behaviour: the five `timeline.relative.*` -rows are deleted from `TIMELINE_DEFAULT_TRANSLATIONS` -(`packages/plugin-timeline/src/useTimelineTranslation.ts`), and a retirement pin -replaces them. - -The rows, transcribed verbatim because that — not "it is in git history -somewhere" — is what makes the deletion reversible: - -```ts -'timeline.relative.today': 'Today', -'timeline.relative.tomorrow': 'Tomorrow', -'timeline.relative.yesterday': 'Yesterday', -'timeline.relative.inDays': 'In {{n}} days', -'timeline.relative.daysAgo': '{{n}} days ago', -``` - -Nothing releases because nothing could reach them. The `en` pack defines no -`timeline.relative.*` leaf, so the provider path never served one; no call site -anywhere in the tree — sources, tests, JSON, ignored build outputs — asks for the -prefix `timeline.relative`, so the provider-less `fallbackT` never reached a row -either; and neither `TIMELINE_DEFAULT_TRANSLATIONS` nor `translateTimelineDefault` -is re-exported from the package entry, so no consumer of the tarball can name -them. Day-granularity relative phrases are produced by `formatRelativeDate` in -`@object-ui/core` through `Intl.RelativeTimeFormat`, which needs no copy row — -that is what left these five behind (objectui#7874, found by objectui#7567's -factory-default census printing its abstention count). diff --git a/.changeset/7876-activity-map-docs-transcription-pin.md b/.changeset/7876-activity-map-docs-transcription-pin.md deleted file mode 100644 index bbbfa8586d..0000000000 --- a/.changeset/7876-activity-map-docs-transcription-pin.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Pin the `sys_activity` mapping paragraph on `content/docs/plugins/plugin-detail.mdx` -against `ACTIVITY_TYPE_TO_FEED_TYPE`, derived from the map in both directions -(objectui#7876, ruling A). Test only; no package is released by this change, and the -published behaviour of `@object-ui/plugin-detail` is unchanged. diff --git a/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md b/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md deleted file mode 100644 index c8806fadcd..0000000000 --- a/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio: a failed package lookup no longer opens the management sheet on nothing - -`fetchFullPackage` — the helper behind the switcher's "Package info & settings" — -fetched `/api/v1/packages` and went straight to `res.json()`, never reading -`res.ok`. The platform answers a failed read in the ADR-0112 envelope -(`{ success: false, error: { code, message } }`), which parses cleanly through -that reader: the error object is neither an array nor carries `packages`, so the -list fell to `[]` and the lookup returned `null` without throwing. `openManage`'s -`catch` therefore never ran and the two lines after it still fired, opening the -management sheet over a null package — which renders nothing. During an outage -the author clicked the menu item and got silence: no sheet, no toast, no -explanation. - -The read now refuses a non-2xx, carrying the server's own `error.message` and -`error.code` (in the 5xx band the platform withholds the producer's prose, so the -code is the discriminating word) and naming the status when the body is not JSON -at all. The failure is reported through this surface's existing posture — -`formatMetadataError` on the shared `studio-package-list` sonner id, so one outage -across this surface's four callers of that endpoint is one toast, not four. - -And the sheet no longer opens on a `null` package at all: a successful list that -simply does not contain the package — deleted or uninstalled elsewhere — now says -so instead of opening over nothing. diff --git a/.changeset/7895-timeline-settled-schema-gate.md b/.changeset/7895-timeline-settled-schema-gate.md deleted file mode 100644 index 9250f5c4b5..0000000000 --- a/.changeset/7895-timeline-settled-schema-gate.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/plugin-timeline': patch ---- - -`ObjectTimeline` waits for the object definition instead of querying twice -(objectui#7895). - -It was the last member of the set objectui#6482 converged on the shared -settled-schema gate — `ObjectKanban`, `ObjectView`, `ObjectCalendar` and -`ObjectTree` were named there, `ObjectGantt` was ask 2 of objectui#7225 — and -nothing marked it a deliberate exclusion. It still held the object definition in -a local `useState` fed by its own metadata effect, and listed that definition in -the record-fetch effect's dependency array. - -**User-visible.** Every object-bound timeline load issued **two** `find` calls -instead of one: the first before the definition landed, with `buildExpandFields` -seeing no fields and therefore carrying no `$expand` at all, and a second one -after. Whenever the metadata read is the slower of the two — the common case on -a cold metadata cache — the second call is not merely a wasted round trip but a -**three-step paint**: raw foreign-key ids, back to the loading skeleton (the -effect's re-run calls `setLoading(true)` and `loading` is an early return), then -the expanded rows. After this change the timeline paints once, from a query that -already carries its expansion. - -Measured on the component with an instrumented renderer, one mount per hold, -`getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with -`ObjectCalendar` and `ObjectGantt` as positive controls in the same run: before, -2 `find` calls with expand sets `[null, ['owner']]`, 1 paint at the readiness -predicate and 3 late writes after it at every hold from +3 ms up; after, 1 `find` -carrying `['owner']`, 1 paint, 0 late writes, and a first-paint time that tracks -the hold (8 ms at +3, 30 ms at +25, 105 ms at +100) where before it was a flat -3-7 ms at every hold. Both controls read 1 paint / 0 late writes before and -after. - -The resolution half is now `useSettledSchema` from `@object-ui/react`, which -settles on **every** exit — no source, no `getObjectSchema`, no object name, and -a read that threw alike. That is what makes the gate safe: the replaced effect -returned without settling on all four, which cost nothing while nothing waited on -it and would have held a gated query open forever. Pinned by -`ObjectTimeline.fetchGate-7895.test.tsx`, including a timeline whose adapter -exposes no `getObjectSchema` and one whose definition read rejects — both still -query, unexpanded. - -Unlike the two sibling conversions, the metadata read is **not** disabled for a -timeline whose items were authored inline: this component also reads the -definition's fields for option colours and field labels on that path, where no -record query is issued at all. diff --git a/.changeset/7903-gallery-settled-schema-gate.md b/.changeset/7903-gallery-settled-schema-gate.md deleted file mode 100644 index 89eae6fc51..0000000000 --- a/.changeset/7903-gallery-settled-schema-gate.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`ObjectGallery` waits for the object definition instead of querying twice -(objectui#7903). - -It sat outside the set objectui#6482 converged on the shared settled-schema gate -— `ObjectKanban`, `ObjectView`, `ObjectCalendar` and `ObjectTree` were named -there, `ObjectGantt` was ask 2 of objectui#7225 and `ObjectTimeline` was -objectui#7895 — and nothing marked it a deliberate exclusion. It still held the -object definition in a local `useState` fed by its own metadata effect, and -listed that definition in the record-fetch effect's dependency array. - -**User-visible.** Every object-bound gallery load issued **two** `find` calls -instead of one: the first before the definition landed, with `buildExpandFields` -seeing no fields and therefore carrying no `$expand` at all, and a second one -after. The first paint was therefore a grid of cards rendered from raw -foreign-key ids. After this change the gallery paints once, from a query that -already carries its expansion. - -Measured on the component with an instrumented renderer, one mount per hold, -`getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with -`ObjectCalendar` as a positive control in the same run: before, 2 `find` calls -with expand sets `[null, ['owner']]` at every hold, the issue order always -`schema:issued, find(unexpanded), schema:settled, find(expanded)`, two distinct -painted states, 3 late writes into the card grid after the first paint, and a -first-paint time flat at 3-7 ms across the whole sweep; after, 1 `find` carrying -`['owner']`, one painted state, 0 late writes, and a first paint that tracks the -hold (9 ms at +3, 15 ms at +10, 35 ms at +25, 106 ms at +100). The control read -1 `find` carrying `['owner']` and a hold-tracking first paint both before and -after. - -The cost this component was paying is a **two**-step paint, not the three-step -one `ObjectCalendar` and `ObjectTimeline` each measured: those make `loading` an -unconditional early return, so their re-run drops back to a placeholder in -between, while this component's early return is `loading && !items.length` — the -raw ids were replaced in place. Measured here rather than inherited from the -matching shape, which is objectui#6482's own per-component standard. - -The resolution half is now `useSettledSchema` from `@object-ui/react`, which -settles on **every** exit — no source, no `getObjectSchema`, no object name, and -a read that threw alike. That is what makes the gate safe: the replaced effect -returned without settling on all four, which cost nothing while nothing waited on -it and would have held a gated query open forever. Pinned by -`ObjectGallery.fetchGate-7903.test.tsx`, including a gallery whose adapter -exposes no `getObjectSchema` and one whose definition read rejects — both still -query, unexpanded. - -Two departures, each judged for this component rather than copied. Like -`ObjectTimeline` and unlike `ObjectCalendar` / `ObjectGantt`, the metadata read -is **not** disabled for a gallery whose records were authored inline: this -component reads the definition on every path, for cell semantics and for ADR-0079 -card titles, not only to expand a query. And the gate window now holds the -loading placeholder rather than showing "No items to display", which the two -siblings get from their initial `loading` state and this one did not. diff --git a/.changeset/7907-manage-snapshot-refresh-reports.md b/.changeset/7907-manage-snapshot-refresh-reports.md deleted file mode 100644 index ec71213f7f..0000000000 --- a/.changeset/7907-manage-snapshot-refresh-reports.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio: a failed managed-snapshot refresh no longer leaves the package sheet showing -the pre-action record as current (objectui#7907). - -`onManageChanged` runs after every package lifecycle action fired from the detail sheet -(disable / enable / duplicate / publish / publish-drafts / manifest edit). Its tail -re-read the managed record so the change would show immediately, and swallowed any -failure under a bare `catch {}` commented "keep the current snapshot" — a snapshot the -action itself had just made stale. The author disabled a package, was told nothing, and -went on reading `Status: Enabled`. - -The sheet derives its lifecycle verb from that record (`enabled` picks both the button's -label and the endpoint it POSTs), so a stale snapshot did not merely display a stale -badge — it re-armed the author with the verb they had just fired. The failure is now -reported through this surface's existing posture (`formatMetadataError` on the shared -`studio-package-list` sonner id, so one outage across the surface is still one toast) -and the sheet closes rather than stay open on a record known to be pre-action. The -Studio, the top bar and the package list are untouched: still a degradation, never a -throw, and no navigation is inferred from a refresh that could not happen. - -The same tail dropped `fresh === null` — a successful read whose list no longer contains -the package — just as quietly; it now reports that with the same sentence `openManage` -already uses for it. - -Pre-existing, and objectui#7881 made it much easier to hit rather than causing it: -before that fix `fetchFullPackage` never read `res.ok`, so this `catch` could only ever -see a non-JSON body; now that the helper refuses a non-2xx, the same `catch` was also -swallowing every 401 / 403 / 503 / 500. diff --git a/.changeset/7918-zod-lazy-getter-identity.md b/.changeset/7918-zod-lazy-getter-identity.md deleted file mode 100644 index aaba556a64..0000000000 --- a/.changeset/7918-zod-lazy-getter-identity.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Two of the ten `z.lazy` exports in the zod node face now memoise their getter, so their -public `.unwrap()` compares by identity (objectui#7918). The other eight are **deliberately -unchanged** — measured, memoising them is a module-load `ReferenceError`. - -The card that found this did not claim the ten were wrong. It asked whether the spelling was -buying a temporal-dead-zone dodge, and that check is what shipped. Each of the ten was -rewritten in place to `const inner = ; z.lazy(() => inner)`, the package rebuilt, and -the built barrel imported in a fresh process. **Eight refuse to load.** Seven name the very -const being declared (`children: z.array(TreeNodeSchema)` sits inside `TreeNodeSchema`'s own -initialiser); `SchemaNodeSchema` names `BaseSchemaCore`, which `base.zod.ts` declares below -it. For those eight the `z.lazy` is load-bearing, so `ActionSchema`, `AppMenuItemSchema`, -`FilterGroupSchema`, `MenuItemSchema`, `NavLinkSchema`, `NavigationMenuItemSchema`, -`SchemaNodeSchema` and `TreeNodeSchema` keep the spelling they have. The two that loaded -clean are memoised: `FilterBuilderConditionSchema` is not recursive at all, and -`NavigationItemSchema` already defers its self-reference through an inner -`z.lazy(() => NavigationItemSchema)` on `children`. - -⚠️ Two corrections to the finding, both measured, both worth more than the edit: - -**The recursion point was already identity-comparable, through the right handle.** -`zod@4.4.3` caches a lazy's resolved inner type on `def._cachedInner` — its own comment says -this preserves "identity for cycle detection on recursive schemas" — and `S._zod.innerType` -reads that cache. It is stable for all ten, including the eight, and survives `.describe()` -clones. What is *not* stable is `S.unwrap()`, because `ZodLazy` defines it as -`() => inst._zod.def.getter()`, going around the cache (`ZodPromise` spells its own as a -stored field). So a schema walker can recognise the recursion point today by reading -`_zod.innerType`; the objectui#7581 false negative — `ActionSchema` reported "not exported by -name" when it plainly is — was the wrong handle, not an unrecognisable schema. Memoising is -still worth doing where it is free, because it makes the public `.unwrap()` honest. - -**The "rebuilt on every parse" cost does not exist.** The finding recorded, explicitly -unmeasured, that a document with N nodes reconstructs the recursive sub-schema N times. It -does not: the getter runs **once per lazy for the life of the process**, via the same -`_cachedInner` — measured at one call during the first parse of a 13-node document and zero -during the second. Wall clock agrees. `NavigationItemSchema` over a 73-node document, -memoised versus not, medians of nine trials of 200 parses: 149,973 ns versus 140,293 ns per -parse — ratio 0.94x, with the ranges overlapping. Those are shared-box seconds, so the -absolutes are not idle-machine figures; the ratio is the reading, and the reading is "no -difference". There is no parse-time win here, and anyone pricing the strict face -(objectui#7935 / objectstack#5250) should strike this from the input list. - -No accept/reject behaviour moves — a memoised getter changes schema *identity*, not what is -declared or admitted. The measurement, the eight `ReferenceError` messages, the identity -matrix and an executable reproduction of both the TDZ mechanism and the once-per-process -getter are pinned in `packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts`. - -⚠️ Also settled while locating the ten: `AppMenuItemSchema` has no declaration of its own — -it is the barrel alias of `app.zod.ts`'s `MenuItemSchema`, while the barrel's own -`MenuItemSchema` is `overlay.zod.ts`'s. Two different schemas, so the list really is ten -entries and not nine. diff --git a/.changeset/7923-schema-reference-named-list-view-keys.md b/.changeset/7923-schema-reference-named-list-view-keys.md deleted file mode 100644 index 7fe632e9a5..0000000000 --- a/.changeset/7923-schema-reference-named-list-view-keys.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Documentation and test only. `content/docs/api/schema-reference.md`'s `object-view` -example no longer authors `listViews["my-deals"].default` — a key `NamedListView` -never declared and `ObjectView` never read — and now spells the node-level -`defaultListView` the renderer actually reads. The new pin -`packages/types/src/__tests__/schema-reference-named-list-view-keys-7923.test.ts` -holds that example's `listViews` keys to `NamedListView`'s declared members. -No published behaviour changes (objectui#7923). diff --git a/.changeset/7947-named-list-view-figures.md b/.changeset/7947-named-list-view-figures.md deleted file mode 100644 index 09da676eea..0000000000 --- a/.changeset/7947-named-list-view-figures.md +++ /dev/null @@ -1,37 +0,0 @@ ---- ---- - -Corrective, comment-and-figures only: no published behaviour changes, so this -declares no bump. - -`NamedListView`'s member figures were quoted wrong in four places. The -declaration has **47** top-level members, counted with the -`namedListViewMemberCount()` regex that -`packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts` uses for -its own assertion — not "about 52", which was a hand figure sitting between two -instruments (a looser count that also matches nested object-literal lines gives -59 on the same declaration). The derived "unread" figure is **41**, not 45: the -renderer reads seven keys off a named view, but only six of them (`label`, -`type`, `columns`, `filter`, `sort`, `options`) are declared members. The -seventh, `data`, is not declared on `NamedListView` at all — it reaches the -renderer through an `as any` cast on the named-view config in -`packages/plugin-view/src/ObjectView.tsx` — so the arithmetic is 47 − 6, not -52 − 7. - -Corrected at all four sites: this changeset's sibling -`.changeset/object-view-unmirrored-keys-7779.md` (still unconsumed, corrected in -place so the wrong figure never reaches a published CHANGELOG), the -`ObjectViewSchema` docblock in `packages/types/src/zod/objectql.zod.ts`, the -`UnmirroredDeclared` note in -`packages/types/src/__tests__/zod-mirror-parity.test.ts`, and the header of the -pin test file. - -The pin that should have caught the drift did not bind: it asserted -`toBeGreaterThanOrEqual(40)` against a true 47, which permitted the declaration -to shed seven members — including a shrink toward the read set, the exact -condition that re-opens the `listViews` value-type decision — and could not -catch growth at all. It is now an exact `toBe(47)` whose failure message names -the three sibling files whose figures must move with it, plus a comment -recording how the count is taken and where the retired "about 52" came from. - -No schema, no type, and no assertion about what is accepted or refused changed. diff --git a/.changeset/7952-dashboard-widgets-component-arm.md b/.changeset/7952-dashboard-widgets-component-arm.md deleted file mode 100644 index 42b36e9a91..0000000000 --- a/.changeset/7952-dashboard-widgets-component-arm.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/types": minor ---- - -`DashboardComponentSchema.widgets` gains the component-node arm on the TypeScript face (objectui#7952) - -`widgets` was `DashboardWidgetSchema[]` in TypeScript while the zod schema it mirrors has been a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the widget slot (`type: 'metric-card'`, body validated as passthrough `BaseSchema`) or a spec-family widget. So the shape `@object-ui/plugin-dashboard`'s README teaches in every `metric-card` example — and the shape the shipped `DashboardRenderer` renders — parsed green under `safeParse` and was refused by `tsc --strict` (`TS2561: 'value' does not exist in type 'DashboardWidgetSchema'`, six occurrences across the README's dashboard blocks at `fc32921`). There was no annotation an author could write for a document the platform accepts. - -**Accept-set change (Clause ②, TypeScript face only).** `widgets` is now `Array`, and `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` — is a new export of `@object-ui/types`. The zod schema is unchanged; `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue` (those are `MetricCard`'s registry inputs, not widget keys — the compiler's `Did you mean to write 'values'?` points at the repair both declarations forbid). - -**What still refuses.** A widget that names a spec-family `type` and carries an undeclared key (`{ type: 'bar', bogus: 1 }`) is still a `tsc` error: the literal is discriminated by `type`, so the passthrough arm never applies to it. A `type` outside both vocabularies is refused as before. The one corner the TypeScript union cannot discriminate — a legacy `component` envelope with NO `type` plus an undeclared key — compiles on the TypeScript face and is refused by name at validation, as every `BaseSchema` slot already behaves. - -**Consumers.** The new arm is assignable to `DashboardWidgetSchema`, so code that annotates a widget callback `(w: DashboardWidgetSchema)` keeps compiling unchanged. Code that reads a property off an unannotated element of `schema.widgets` now sees the union, and through `BaseSchema`'s index signature that read is `any` rather than the widget's declared type — annotate the parameter to keep the narrower type. diff --git a/.changeset/7979-package-form-dialog-envelope-reader.md b/.changeset/7979-package-form-dialog-envelope-reader.md deleted file mode 100644 index 87c38686d3..0000000000 --- a/.changeset/7979-package-form-dialog-envelope-reader.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`PackageFormDialog` reads the producer-marked `error.userMessage` (objectui#7979). - -The create / edit / view package dialog POSTs and PATCHes `/api/v1/packages` through its -own `apiJson`, which held a fourth copy of the ADR-0112 failure-envelope ladder — -character for character the one `PackagesPage` had before objectui#7959. It read the -diagnostic `error.message` and stopped, so two things a refusal carries never reached the -author: the producer's marked `error.userMessage` (present since objectstack#9934, emitted -by both doors that serve these routes) and `error.code`. - -The read now comes from the one shared rule, `readEnvelopeFailureText` -(`utils/apiErrorEnvelope.ts`), which prefers the mark over the diagnostic and appends the -declared code behind whichever prose won. Create and edit are exactly where an author meets -a refusal that names what to fix — a namespace rule, a version already published — and in -the 5xx band the door substitutes the generic `Internal server error` into `message` while -the mark rides through untouched, so on a marked 500/503 the dialog used to show the generic -sentence and discard the specific one. - -The two rungs below the shared read stay: a bare-string `error` and a top-level `message` -are older runtimes' shapes, not this envelope, and they are live for this call site alone. -The dialog's two status-driven arms (409 → "already exists", 403 → the localized capability -copy, objectstack#8270) are unchanged. diff --git a/.changeset/8067-component-input-member-kind.md b/.changeset/8067-component-input-member-kind.md deleted file mode 100644 index ddd9c49df8..0000000000 --- a/.changeset/8067-component-input-member-kind.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/sdui-parser': minor -'@object-ui/components': minor -'@object-ui/plugin-detail': minor -'@object-ui/plugin-grid': minor ---- - -`ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one -(objectui#8067). - -A registration's `type: 'array'` said a value was a list and stopped there, so a member -that drifted from `@objectstack/spec` was invisible to every layer that reads a -declaration. `page:header.actions` is the measured cost: the contract declares -`z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` -objects, and the repo-wide parity gate in -`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the -whole life of the drift because both sides carried the key and neither could say what -was inside it. What settled it was a maintainer ruling, not a test — and even after the -fix, "these are ids" survived only as English in the registration's `description`. - -**What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind -vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an -`object` used as a map. One kind, or an array of them for a member contract that is a -union, with `type`'s semantics — a member passes when any declared arm accepts it. The -manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per -input instead of six. - -**Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 -is the precedent: five `ComponentInput` keys declared and read by nothing). The -repo-wide parity gate compares every declared `of` against the member kind -`ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; -`sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new -`member-type-mismatch` diagnostic naming the offending positions; and the generated -`sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is -`string[]` where it used to be `unknown[]`. - -**Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: -every container key's member position was probed with one value of each coarse kind and -a declaration written only where exactly one kind was accepted. A member contract that -admits several kinds — `record:highlights.fields` takes a field name or an inline field -object — is deliberately left undeclared and pinned with its reason, because picking one -arm there is a narrowing this repo leaves un-gated and picking all of them would -advertise shapes only a per-block pin can vouch for. - -**The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer -ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` -is the publication face's expression ceiling, and spec is the sole judge of values — -stands exactly as written. `of: 'object'` says the members are objects; which keys they -carry is still `description`'s job and `os validate`'s. - -**Nothing published before this changes.** An input that declares no `of` validates, -serializes and types byte-identically: `validateTree` checks no member, the serializer -emits no key, and the codegen emits the same `unknown[]`. diff --git a/.changeset/8068-mandatory-per-block-member-pin.md b/.changeset/8068-mandatory-per-block-member-pin.md deleted file mode 100644 index 11684e5252..0000000000 --- a/.changeset/8068-mandatory-per-block-member-pin.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Make a per-block member pin MANDATORY for every `array`/`object` typed -registered input on a spec-carried block (objectui#8068). - -`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` compares -top-level key names and delegates member shapes to per-block tests beside the -renderers — but nothing required one. `page:header.actions` is the key that -proves it: array-typed, member shape in `description` prose only, spec -`z.array(z.string())` while the renderer read the members as objects, and every -layer green for a full contract cycle until a human filed it. - -The new direction judges the same `covered` set as the rest of that file: every -array/object-armed input must name a pin whose file exists, is collected by the -runner, and names both the block and the key — or carry an explicit, -issue-backed exemption. Measured first, as the card required: 77 such inputs, 19 -already pinned, 58 not, so the route is the transition this repo already uses — -all 58 listed by name, self-deleting once a key acquires a pin, under a ceiling -that may only ratchet down. `objectui#8071` owns converting them. - -Test only; no package is released by this change. diff --git a/.changeset/8137-record-activity-calibration-repoint.md b/.changeset/8137-record-activity-calibration-repoint.md deleted file mode 100644 index 48401d08a9..0000000000 --- a/.changeset/8137-record-activity-calibration-repoint.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Re-point the `record:activity.types` member calibration control in -`registry-inputs-spec-parity` from `'Account'` to `''` (objectui#8137). -`@objectstack/spec` 17.3.0 made that vocabulary open — -`z.array(z.union([FeedItemType, z.string().min(1)]))`, objectstack#11658 -executing the maintainer's 2026-08-24 ruling on objectstack#11507 — so the old -probe now accepts, while `''` is still refused for its CONTENT via `.min(1)`. -Test only; no package is released by this change. diff --git a/.changeset/8165-translatefn-one-authority.md b/.changeset/8165-translatefn-one-authority.md deleted file mode 100644 index a172590962..0000000000 --- a/.changeset/8165-translatefn-one-authority.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Re-point `saveAdvisoryToast`'s `TranslateFn` at its one authority, -`writeWarningToast` (objectui#8165), and shrink the `KNOWN_COLLISIONS` baseline -in `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` by that -site. Type-level only: the declaration was byte-identical to the one it now -re-exports, the name is on no package's published face, and `export type { X } -from '…'` erases at build — so no package is released by this change. diff --git a/.changeset/8168-objectchart-absent-category-axis-refusal.md b/.changeset/8168-objectchart-absent-category-axis-refusal.md deleted file mode 100644 index ccc98a7e6f..0000000000 --- a/.changeset/8168-objectchart-absent-category-axis-refusal.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/plugin-charts': minor -'@object-ui/i18n': minor ---- - -`ObjectChart` refuses an object-bound chart that declares no category axis (objectui#8168). - -An object-bound chart that named no field to group by used to be composed anyway. -`runAggregate` passed `schema.aggregate` to `ds.aggregate(objectName, { field, -function, groupBy, filter })` with no guard on `groupBy`, and the `ds.find` leg -handed the same bag to `aggregateRecords`, which buckets every record on -`record[groupBy] ?? 'Unknown'`. So one path asked a driver to group by `undefined` -and the other collapsed the whole object into a single `'Unknown'` bar — and which -of those a reader saw was decided by the data source, not by the renderer. The only -loud states this component had were a fetch `error` (`chart-error`) and a generic -"No data yet"; neither is a statement about an absent binding. - -`ObjectCalendar`, `ObjectGantt` and `ObjectTimeline` each already refuse a view that -declares no axis. This is the fourth, following `ObjectTimeline`'s shape -(objectui#7459): a `role="alert"` box, `data-testid="chart-missing-category-axis"`, -naming the bindings the author can declare — `aggregate.groupBy`, `xAxisKey`, -`xAxis.field` — rendered from the resolver's own vocabulary so the message cannot -drift from what the resolver reads. - -**Breaking, deliberately — and this repo ships breaking as `minor`.** A chart that -previously rendered an `'Unknown'`-bucketed bar (or whatever the driver did with -`groupBy: undefined`) now renders the refusal instead. That is the intent: the -picture it drew was not a picture of the data. - -It keys on the CATEGORY alone. A measure may legitimately be absent — `count` takes -no field — so refusing on an absent measure would refuse `count` grouped by a -declared category, a chart that renders correctly. Four shapes are deliberately -untouched: an ADR-0021 `dataset` chart (which may declare no dimension), a chart -carrying authored `data` or a `bind` scope (no field name is read to fetch those -rows), a spec-shape `xAxis: { field }` with no `xAxisKey` (resolved through -`normalizeChartSchema`, this package's one translation of the author-facing shape), -and every schema the five in-repo producers compose today — all five floor their own -category, so none of them can reach the refusal. - -This does **not** retire the six `'name'` / `'value'` floors at the three relay faces; -that is the remainder of objectui#7547 and is mechanical only once this screen exists. - -New key `chart.unconfigured.noCategoryAxis` in all ten locale packs. diff --git a/.changeset/8171-calendar-sort-input.md b/.changeset/8171-calendar-sort-input.md deleted file mode 100644 index 48b7be711c..0000000000 --- a/.changeset/8171-calendar-sort-input.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -'@object-ui/plugin-calendar': minor ---- - -Declare the `sort` input on both `object-calendar` registrations -(objectui#8171) — the html tier stops reporting `unknown-prop` on a key the spec -declares and `ObjectCalendar` already reads. - -objectui#7712's defect, one key over. `ObjectCalendar.tsx` lowers the authored -key onto its own query as `$orderby: convertSortToQueryParams(schema.sort)`, and -`@objectstack/spec`'s `ComponentPropsMap['object-calendar']` declares `sort` -(measured on 17.2.0: `safeParse({ objectName, sort })` returns `success: true`, -while the same strict schema on the same call refuses `bogusProp` by name — that -control is what makes the acceptance a verdict). But neither of the two -registrations that publish this renderer — `plugin-calendar:object-calendar` and -`view:calendar` — listed `sort` in `inputs`, and `sdui-parser`'s `validateTree` -reports `unknown-prop` for every key no `inputs` entry claims. So an author -writing the one spelling that WORKS was told it was unknown — objectui#6678's -shape, where a correct write draws the same diagnostic as a write that does -nothing. - -ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has -live readers on both ends, so the registrations were the side that was wrong. -Declared `type: 'array'`, matching the `filter` entry objectui#7712 put beside -it and the `sort` that `object-grid`'s `GRID_QUERY_INPUTS` publishes — measured, -that arm is the repo-wide convention for this key, all seven existing `sort` -declarations write it. - -⛔ Not derived from the `ElementDataSourceMapping` sitting six lines above, -which already asserts `sort: true`. That structure maps query keys for -`ElementDataSourceGate`; it is not an authoring declaration, and objectui#7712 -measured why a mechanical derivation would be wrong — kanban's mapping also -carries `limit`, which the spec refuses by name. - -⚠️ Carried forward from objectui#7712, because it stays true here: -`check:react-blocks-declaration-parity` runs manifest → spec, one direction. A -key the SPEC declares and the manifest omits is structurally outside what that -ratchet measures, so declaring this key does **not** make the next omission -loud. Making that ratchet bidirectional is objectui#8176. - -Rider carried by the same change, because objectui#8212 landed first: the -console's `registry-inputs-spec-parity` ledger is updated in step with the -declaration — the `object-calendar.sort` unpublished-key exemption is deleted -(its cover expires the moment the key is declared), the shrink-only objectui#8176 -backlog ceiling drops 16 to 15, and `sort` gets a `MEMBER_PINS` entry rather than -a member-pin exemption. `MEMBER_PIN_EXEMPTION_CEILING` is untouched at 62. - -⚠️ That member pin is deliberately NOT the identity-forwarding shape the two -`filter` pins use. `ObjectCalendar.tsx` writes -`$orderby: convertSortToQueryParams(schema.sort)`, which builds a new -`field -> direction` map, so this key is lowered rather than passed through and -`toBe` on `$orderby` is false about it. The pin asserts what is READ inside a -member instead — `field`, `order`, an omitted `order` meaning ascending, and a -member with no usable `field` dropped rather than invented. - -No version bump is declared for `@object-ui/console`: its only edit here is that -test file's ledger data, which publishes nothing. diff --git a/.changeset/8181-draft-envelope-read-decoration-strip.md b/.changeset/8181-draft-envelope-read-decoration-strip.md deleted file mode 100644 index 42968d7ac0..0000000000 --- a/.changeset/8181-draft-envelope-read-decoration-strip.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/data-objectstack': patch -'@object-ui/app-shell': patch ---- - -fix(studio): one draft-envelope reader, and it strips the framework's read decorations - -`client.getDraft()` serves a DECORATED body — the draft branch stamps -`_draft: true` and then `decorateMetadataItem` attaches `_diagnostics` for any -type with a registered Zod schema. The spec names both READ-TIME decorations -precisely because a served body "is NOT a valid input to the schema that -produced it until these are removed" (`METADATA_READ_DECORATIONS`). - -objectui#7603 taught `ResourceEditPage` to strip them. It could only teach one -site, because `extractDraftBody` existed **four times** — three verbatim copies -plus a hand-rolled one in `ObjectHooksPanel` — and six more consumers unwrapped -the envelope inline. Ten readers, one of which knew the rule. - -**The user-visible half.** The pending-changes sheet's per-entry diff compares -the published body against the draft body key by key. Those two reads are -decorated ASYMMETRICALLY — only the draft branch stamps `_draft` — so the sheet -listed `_draft` under "Also changed:" on every entry that has a published -counterpart, and `_diagnostics` alongside it whenever the two read-time verdicts -differed. Framework-internal keys were being presented to the author as their -own edits, on the screen where they decide whether to publish. - -**The rest.** Six sites merged a decorated body into a document they then wrote -back through `save(..., { mode: 'draft' })` — the Studio app / page / object / -flow surfaces, the package OWD panel, the object hooks panel, and the -adapter's `updateView`. Today's server absorbs that (it strips read decorations -on ingress, before its own schema gate), so nothing 400s; this is still a client -emitting a body its own spec calls invalid, and the fix belongs at the producer. - -The cure is one function rather than ten strips: `extractDraftBody` is now -exported from `@object-ui/data-objectstack`, beside the `getDraft` whose -envelope it decodes. The key list is the spec's exported -`stripReadDecorations` — never a second hand-maintained copy in this repo. The -presence verdict still runs BEFORE the strip, so removing our own annotations -can never turn a served draft into "nothing pending", and the ADR-0010 -protection envelope (`_lock`, `_provenance`, `_packageId`, `_packageVersion`) -is deliberately untouched: those keys are declared by the closed schemas. - -No schema was loosened, and no gate was taught to tolerate `_diagnostics`. diff --git a/.changeset/8193-objectview-canonical-groupbyfield.md b/.changeset/8193-objectview-canonical-groupbyfield.md deleted file mode 100644 index 02972a93b4..0000000000 --- a/.changeset/8193-objectview-canonical-groupbyfield.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-list': minor ---- - -Emit the spec-canonical kanban lane key from the object page, and teach the -capability gate to recognize it (objectui#8193). - -`ObjectView` built the view-level kanban config it hands to `list-view` and wrote -the deprecated alias `groupField` — never `groupByField`, the key -`@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it -already READ the canonical key first. Its sibling producer in the same package, -`defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and -left the reasoning next to itself ("that read-site now prefers the spec key, so -one key is enough"); the twin was not carried along, so one producer surface -spoke two vocabularies for one concept depending on which entry point you -arrived through. `ObjectView` writes into `options.kanban`, which -`normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing -corrected it downstream and the legacy spelling was what drove the lanes. - -The expression is now the exported `kanbanViewOptions`, the fifth member of the -`timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` / -`galleryViewOptions` family it had been the odd one out of. - -**`ListView`'s kanban capability gate changed with it, and had to.** The gate -consulted the `options.kanban` bag for the LEGACY spelling only, so a producer -writing the spec key into the bag was invisible to it while still rendering -correctly — the render branch merges the bag and resolves -`groupByField || groupField`, so the gate recognized strictly less than what -renders. Measured before and after: a bag of `{groupBy, groupField}` offered -Kanban and `{groupBy, groupByField}` did not, so shipping the producer change -alone would have removed the Kanban toggle from every object view. The gate now -reads both spellings out of the bag — the same one-question-two-sites repair -objectui#5042 made for `map` and objectui#7544 for `chart`. - -**No alias READ was removed, and the alias is not retired.** Stored metadata -still authors `groupField`, and every read site still resolves it; `ListView` -already preferring the canonical key is precisely why the sibling producer could -drop the alias write. What changed is only what this one face WRITES, plus one -added rung on the gate. - -**Migration.** Nothing authored has to change. If you read -`options.kanban.groupField` off the schema `ObjectView` produces, read -`groupByField` (or both) instead — that bag now carries the spec spelling. - -The view-level `groupBy` in the same bag is untouched. It is not a spec key -either — measured against the strict `KanbanConfigSchema`, which refuses it by -name — but `ListView`'s projection collectors read it, so retiring it needs its -own producer census and is filed as objectui#8213. diff --git a/.changeset/8194-fields-date-widget-convention.md b/.changeset/8194-fields-date-widget-convention.md deleted file mode 100644 index 8fe0942574..0000000000 --- a/.changeset/8194-fields-date-widget-convention.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/fields': minor ---- - -One home for the `date` display convention in the readonly field widgets -(objectui#8194). - -Four readonly `date` faces in `@object-ui/fields` called -`toLocaleDateString(locale)` with **no options bag at all** — `Intl`'s numeric -default — so they never implemented the year-dropping decision the shared -`formatDate` documents and every `date` CELL already follows. They now call -`formatDate` (default style): - -- the readonly `DateField` (the form / detail face, and what `FieldEditWidget` - renders in the grid and detail inline editors), -- the sub-grid `GridField`'s readonly `date` column, -- a `FormulaField` declaring `return_type: 'date'`, -- the lookup picker's plain-text `$date` fallback (`lookupColumnDisplay`), - which sits in the same function as the descriptor path that already rendered - through `formatDate`. - -**Visible change**: every one of those faces changes shape in every locale, in -every year — not only the year token. In `en-US` a date renders `Jul 4` this -year and `Jul 4, 2024` for a past year, where it used to render `7/4/2026` and -`7/4/2024`; in `de` `4. Juli` / `4. Juli 2024` for `4.7.2026` / `4.7.2024`; in -`zh` and `ja` `7月4日` / `2024年7月4日` for `2026/7/4` / `2024/7/4`; in `ar` -`4 يوليو` / `4 يوليو 2024`. Each now matches the `date` cell beside it. This is -a larger move than the sibling change in `@object-ui/components` -(objectui#7620), whose former face already asked for a short month and so only -lost its year token — these four passed no bag whatsoever. - -A value the formatter cannot parse now reads `—` at three of the four sites -instead of the literal `Invalid Date`. The sub-grid keeps showing the raw -stored string for an unreadable value, unchanged (objectui#3569). - -Untouched: the `datetime` readonly faces (`DateTimeField`, the sub-grid's -`datetime`/`time` branch). They are the same omission one type over, but their -home is `formatDateTime`, whose named faces are a separate display-convention -question; they are recorded on their own card rather than picked here. - -A surface that genuinely wants the year on every row is an explicit `format` -style honoured by both paths, not a second option bag — the objectui#7620 / -objectui#7443 / objectui#4576 lesson, one surface over. diff --git a/.changeset/8198-unused-runtime-declarations-removed.md b/.changeset/8198-unused-runtime-declarations-removed.md deleted file mode 100644 index 0749e35e3a..0000000000 --- a/.changeset/8198-unused-runtime-declarations-removed.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@object-ui/auth': minor -'@object-ui/console': minor -'@object-ui/core': minor -'@object-ui/fields': minor -'@object-ui/layout': minor -'@object-ui/plugin-ai': minor -'@object-ui/plugin-calendar': minor -'@object-ui/plugin-chatbot': minor -'@object-ui/plugin-dashboard': minor -'@object-ui/plugin-designer': minor -'@object-ui/plugin-editor': minor -'@object-ui/plugin-map': minor -'@object-ui/plugin-markdown': minor -'@object-ui/plugin-report': minor -'@object-ui/react': minor -'@object-ui/runner': minor ---- - -Remove 37 runtime dependencies that no file in the declaring package consumes, and gate -the direction so the next one cannot land (objectui#8198). - -`check:phantom-deps` judges imports that are not declared; nothing judged the reverse, -so a declaration could outlive its last consumer indefinitely. That is what happened to -`recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it -was removed by hand on objectui#7625, and nothing would have reported the next one. The -new `pnpm check:unused-deps` asks the reverse question over `dependencies` and -`optionalDependencies` of every released package. - -**Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship -changes: their Vite `external` predicates are path-based and never read `dependencies`, -so no built artifact moves. What changes is the install graph — a project that imports -one of the removed packages while depending only on the ObjectUI package that used to -drag it in will no longer resolve it. Declare it directly; that is the correct -dependency edge in either case. The removals, by package: - -- `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` -- `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) -- `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` -- `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` -- `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` -- `@object-ui/core`: `lodash`, `zod` -- `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply -- `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect -- `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` -- `@object-ui/fields`: `clsx`, `tailwind-merge` -- `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` -- `@object-ui/auth`: `@object-ui/types` -- `@object-ui/plugin-calendar`: `@object-ui/fields` -- `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` -- `@object-ui/react`: `react-hook-form` - -Every one was verified by a whole-package grep before removal — the name appeared nowhere -under the package but its own manifest and CHANGELOG — and the whole workspace builds, -type-checks and tests green afterwards. diff --git a/.changeset/8201-kanban-calendar-scalar-authoring-keys.md b/.changeset/8201-kanban-calendar-scalar-authoring-keys.md deleted file mode 100644 index e98c998ec1..0000000000 --- a/.changeset/8201-kanban-calendar-scalar-authoring-keys.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-calendar': patch ---- - -`object-kanban` / `view:kanban` and `object-calendar` / `view:calendar` now DECLARE seven -spec-carried keys their renderers already honoured, so the html tier stops reporting working -metadata as `unknown-prop` (objectui#8201, the backlog objectui#8176 exposed). - -Board: `groupBy`, `cardTitle`, `titleField`, `swimlaneField`, `coverImageField`. -Calendar: `defaultView` (the spec's three-member enum) and `locale`. - -No renderer behaviour changes — every one of these keys was already read and already acted on; -what changes is that authoring tools can now discover them and the save gate agrees with the -validator. Both tags of each block now spread ONE shared `inputs` list, so the two published -surfaces cannot drift apart by hand-copy. diff --git a/.changeset/8204-refuseskind-invalid-type-decisive.md b/.changeset/8204-refuseskind-invalid-type-decisive.md deleted file mode 100644 index 215d941eff..0000000000 --- a/.changeset/8204-refuseskind-invalid-type-decisive.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Test-only change: repair the `registry-inputs-spec-parity` kind classifier so a member -whose contract is a `z.union` is judged by what the union actually refused. No published -behaviour changes; nothing outside `apps/console/src/__tests__/` is touched. - -objectui#8204. `refusesKind` asked "is EVERY complaint at this node a kind complaint", -and Zod keeps running a schema's checks after its type check has failed: for -`z.string().min(1)` the probe `[]` comes back as both `invalid_type` AND `too_small`. -Read by `every`, that pair is a CONTENT refusal — so the array probe that -`@objectstack/spec` 17.3.0's `RecordActivityProps.types` -(`z.array(z.union([FeedItemType, z.string().min(1)]))`) measurably refuses put `array` -into that key's accepted kind set, and the derivation gate reported -`record:activity.types → contract accepts {string,array}` for a key whose array members -the contract rejects. - -`invalid_type` at a node is now decisive: a value cannot be simultaneously the right kind -and the wrong one, so whatever else the node says about a value it has already refused -for its kind is downstream of that refusal, not a second opinion. Measured against the -installed 17.3.0 artifact, `record:activity.types` goes `{string,array}` to `{string}` -and no other judged key moves; `record:highlights.fields`, a genuine member union, is -still reported as `{string,object}`. - -The suspected mechanism "`refusesKind` does not recurse into `z.union`" was measured and -is FALSE — the recursion runs and is what carries branch verdicts up. The per-branch -reading underneath it was the defect. diff --git a/.changeset/8218-studio-property-panel-de-developerize.md b/.changeset/8218-studio-property-panel-de-developerize.md deleted file mode 100644 index a805a8b99d..0000000000 --- a/.changeset/8218-studio-property-panel-de-developerize.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -De-developerize the metadata-admin property form now that Studio's 「界面」 panel -grafts it in front of an AI-build maker (objectui#8218). - -`SchemaForm` + `widgets.tsx` were written for an administrator editing metadata. -Studio's interface panel renders the very same form for someone who has never -seen a JSON Schema, so four developer habits landed inside an otherwise fully -Chinese surface. Same cause, one pass: - -- **Machine-name tooltip.** The `title="Machine name"` on the identifier chip - went through the engine string table. Note the chip's own predicate degrades - under localization: it shows when the visible label does not spell the machine - name, which a translated label never does — so in a Chinese panel it renders - for *every* field. Reported, not changed here; whether a maker should see the - identifier at all is a product call, not a rendering one. - -- **Master-detail column headers.** A column whose item schema carries no - `title` printed the raw JSON Schema key (`actionUrl`, `actionType`) as if it - were a column name — in *every* locale, English included. It now humanises the - key, the same convention this tree already applies wherever a title is absent - (`json-schema-to-fields.ts`'s `prop.title || humanizeKey`, and `SchemaForm`'s - own grid repeater: `s.label || prettify(s.field)`). `title` is an OPTIONAL - annotation, so its absence is not off-spec metadata and the renderer has to - render something; this is not a lenient contract fallback. The LOCALIZED - column name needs an upstream channel that does not exist yet — filed there. - -- **Numeric fields.** Both numeric renderers (`SchemaForm`'s field control and - the master-detail cell) now gray the schema's `default` in as a placeholder, - so an empty box reads "using the default" instead of "unknown", and forward - `minimum` / `maximum` / `multipleOf` onto the control. Previously only - `fieldSpec.min` / `.max` were read — which a spec-derived authoring form never - declares — so a panel accepted a negative column count its own contract had - already ruled out. The default stays a placeholder, never a written value, so - "left on the default" remains distinguishable from "pinned to today's default" - in the saved metadata. - -- **Untranslated strings.** A full sweep of both files: 45 user-visible literals - (aria-labels, placeholders, empty-state prose, segmented-control labels, - secret-field copy, the code editor's chrome) now resolve through the engine - table in `en` and `zh`. diff --git a/.changeset/8266-dashboard-count-aggregate-series-key.md b/.changeset/8266-dashboard-count-aggregate-series-key.md deleted file mode 100644 index c4fc28fefb..0000000000 --- a/.changeset/8266-dashboard-count-aggregate-series-key.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-charts': patch ---- - -Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing -(objectui#8266). - -A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` -and no `field` — the normal way to author "how many records per status" — rendered an -empty chart. No error, no empty state: a plot frame with the category ticks drawn and -not one mark in it, which reads exactly like "this object has no rows yet". - -**Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each -built the series binding as `aggregate?.field || (options.yField || 'value')`, which for -a fieldless count resolves to `'value'`. The rows an object-bound fieldless count -returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned -since framework#3701. A `dataKey` naming a column no row carries plots nothing, and -neither of the renderer's two guards fires on it: the rows DO carry the category key, -and the series array is not empty. - -**Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to -`chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of -"the value column an object-bound aggregate produces". Both relays now consult it, and -the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed -through the same function, so the two halves of the question cannot drift again. - -**What moves on screen.** A chart that was blank now draws. Charts that already drew are -unaffected: a field-bearing aggregate resolves to its raw field under both the old and -the new reading, and a chart with no `aggregate` at all keeps the author's `yField`. -One authored key changes meaning: a `yField` written on an object-bound chart that -ALSO declares an aggregate no longer wins over the aggregate's own column — it named a -record column that a grouped aggregate never returns, so it plotted nothing before. - -**Not fixed here, and out of scope.** The same widget with no `options.xField` is -refused by the category-axis guard naming `name`, a key the author never wrote (they -wrote `aggregate.groupBy`). That is the category half of the same relay gap and is -filed separately. diff --git a/.changeset/8269-dashboard-category-axis-groupby.md b/.changeset/8269-dashboard-category-axis-groupby.md deleted file mode 100644 index d6e554dd06..0000000000 --- a/.changeset/8269-dashboard-category-axis-groupby.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-charts': patch ---- - -Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being -refused for lacking a `name` column (objectui#8269). - -A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` -and no `options.xField` rendered a refusal instead of a chart: - -> This chart cannot plot its category axis: no row has a `name` field. - -The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that -had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent -them to debug the wrong layer. - -**Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each -floored the category binding on a literal — `options.xField || 'name'` — and handed it to -the `object-chart` node without ever consulting the aggregate that decides it. An -object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no -row carried `name` and the category-axis guard (framework#4033) fired correctly on a -binding that was already wrong when it arrived. - -**Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to -`chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of -"the category column an object-bound aggregate produces", and the published sibling of the -`chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now -consult it for the object-provider branch. - -**What moves on screen.** A widget that rendered a refusal now draws. Measured through -`ChartRenderer` at 480x320 over the rows a fieldless count returns -(`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from -`xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`, -1 series and 2 marks with the category ticks drawn. - -**Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows -are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it -returns a single row with no category column), and the authored-literal-rows branch — the -`chart` node composed after the object-provider check fails — keeps its floor unchanged. -One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField` -written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the -aggregate's own column — it named a record column a grouped aggregate never returns, so it -produced the same refusal before. diff --git a/.changeset/action-callback-retired-7068.md b/.changeset/action-callback-retired-7068.md deleted file mode 100644 index 0ed6b08f8d..0000000000 --- a/.changeset/action-callback-retired-7068.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Breaking for authored metadata:** the legacy `ActionSchema`'s Phase-2 callback -pair — `onSuccess` / `onFailure`, each carrying an `ActionCallback` object — is -RETIRED, and the `ActionCallback` type and its Zod mirror `ActionCallbackSchema` -(with the inferred `ActionCallbackSchemaType`) are DELETED from `@object-ui/types` -and `@object-ui/types/zod` (objectui#7068; maintainer ruling option 1 of -2026-09-05, immediate, no deprecation window; ADR-0049 enforce-or-remove). - -**What an author who wrote the shape sees now.** A `{ type: 'action', … }` -document authoring `onSuccess: { type: 'toast', message: '…' }` (or any -`onFailure` callback) no longer validates: the parse fails loudly on the -`onSuccess` / `onFailure` path (`invalid_type`, expected `never`) with the -explanation and the migration in the message, and the TypeScript members are -`?: never` tombstones so the same document is a `tsc` error at the authoring -site. `import type { ActionCallback } from '@object-ui/types'` and -`import { ActionCallbackSchema } from '@object-ui/types/zod'` fail to resolve. - -**What was measured, on this branch's base (`900f8d99`).** `ActionCallback` -(`{ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | -'dialog', message?, url?, api?, method?, dialog?, handler? }`) was declared in -`crud.ts`, mirrored in `zod/crud.zod.ts`, re-exported by both barrels, and -carried on the legacy `ActionSchema` as `onSuccess?` / `onFailure?`. Producers: -the package's own `phase2-schemas.test.ts` fixture and three `ts` fences in -`content/docs/core/enhanced-actions.mdx` — nothing else (`git grep -l -ActionCallback` over `packages content skills` hit the five `packages/types` -files; positive control `SchemaNodeSchema` hit 22). Runtime readers: none — -`ActionRunner` imports `UIActionSchema`, never this interface, and its own -`ActionDef.onFailure` is a different (runner-native) meaning. It was the THIRD -meaning of one key: objectui#5934 had already retired the runner's callback -meaning of `onSuccess` and converged it on the spec's block. - -**Why authored JSON that passed publish is unaffected.** `@objectstack/spec`'s -`ActionSchema` (installed pin 17.2.0) already refused the callback shape at -publish — `invalid_type` at `onSuccess.navigate` plus `unrecognized_keys` on the -`onSuccess` block, and `onFailure` refused as an unrecognized key on the action — -so no published or saved metadata could carry it. Only TypeScript code that -typed a callback against the legacy interface, or JSON validated solely through -`@object-ui/types/zod`, meets the new refusal. - -**Where the live meaning lives.** Post-success navigation is the spec's -`onSuccess` block, `{ navigate, openIn }`, declared on `UIActionSchema` -(`ui-action.ts`) and forwarded to the runner (objectui#5934). A success or -failure notice is `successMessage` / `errorMessage` — adjacent keys on the same -legacy `ActionSchema`, NOT retired, and still accepted on both faces. - -**Two published faces, one retirement — tombstone on the keys, deletion of the -type.** `BaseSchema` is `.passthrough()` on the mirror and carries an index -signature on the interface, so DELETING the two keys would have ADMITTED an -authored callback unchecked on both faces; they stay declared as `?: never` / -`retirementTombstone()` (the PR #7761 / #7769 shape) and the base-vs-extended -contrast is pinned. The standalone `ActionCallback` / `ActionCallbackSchema` have -no such escape hatch and are deleted outright, the route objectui#7664 / PR #7743 -took for the `DeclarativeKanban*` trio; the parity ledger drops the pair -(`EXPECTED_MIRROR_PAIRS` 159 → 158) and the absence is pinned in -`action-callback-retired-7068.test.ts`. - -**Docs, same change.** `content/docs/core/enhanced-actions.mdx` — the three -`onSuccess` / `onFailure` fences author `successMessage` / `errorMessage` -instead, and the "Callbacks" section is a "Post-success behaviour" note pointing -at the spec block (no fence: the legacy type carries no spec-derived block). -`content/docs/guide/schema-overview.md` — the fragment line, the feature bullet -and the checklist row are rewritten to the truth (the ✅ claim is now a -retirement note). - -**Migration:** delete `onSuccess` / `onFailure` from any legacy `ActionSchema` -document or fixture; write `successMessage` / `errorMessage` for notices, put -follow-up work in `chain`, and author post-success navigation as the spec's -`onSuccess: { navigate, openIn }` block on `UIActionSchema`. - -Graded `minor`, not `patch`: this narrows the accepted input set on both faces -and removes two exports, which is breaking for any consumer who wrote the shape. -It is not `major` per this repo's fixed-group convention (objectui's own breaking -changes ship as `minor`; the group's major tracks `@objectstack` — AGENTS.md -版本号策略, mechanically enforced by `scripts/check-changeset-no-major.mjs`). diff --git a/.changeset/action-designer-param-vocabulary.md b/.changeset/action-designer-param-vocabulary.md deleted file mode 100644 index 9b82c424a0..0000000000 --- a/.changeset/action-designer-param-vocabulary.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -fix(app-shell): the Action designer's preview draws what the runtime dialog will draw - -`ActionDefaultInspector` offers eight param `type` spellings; `ActionPreview`'s -dialog mock switched on five of them over a private table, so three of the eight -previewed as a control `ActionParamDialog` does not render — `datetime` and a -targeted `lookup` as plain text boxes, and a `select` whose options were not -authored yet as a text box as well. A `text` param that happened to carry -`options` previewed as a select the runtime never draws, for the same reason in -the other direction. - -The mock now resolves each param through `paramToField`'s -`resolveParamWidgetType` / `paramDegradesWithoutTarget` — the same adapter the -dialog itself renders through — so the two panes cannot disagree about a -spelling again. `datetime` draws a date/time control, a `lookup` with a declared -`reference` draws a record picker, a targetless one draws the record-id text box -the dialog degrades to and says why, and a `select` always draws a picker. - -The per-param editor also gains an `options` control for `select` params. It had -none, and `params` is hidden from the collapsed "More fields" form, so the panel -that offered the type had nowhere to author the choices the type needs. diff --git a/.changeset/action-keys-warning-text-5642.md b/.changeset/action-keys-warning-text-5642.md deleted file mode 100644 index d4fdb94a12..0000000000 --- a/.changeset/action-keys-warning-text-5642.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/core': patch ---- - -The dev-mode unknown-key warning stops stating a fact that was retired, and -sends the author to the file that actually declares the interface -(objectui#5642). - -Both halves of the message's tail had outlived the change they described. It -told the author the key was warned about rather than rejected because -`ActionDef` "still carries `[key: string]: any`" — objectstack#4075 step 3 -deleted that index signature, and `actionKeys.pin.test.ts` pins the deletion in -the opposite direction (`{ ActionDef: false, ActionContext: true }`), while -`actionDef-closed-surface.test.ts` pins that `tsc` now rejects exactly such a -key at the construction site. And it prescribed promoting the key to an explicit -field on `ActionDef` "(packages/core/src/actions/actionKeys.ts)" — that file -holds the INVENTORY (`ACTION_DEF_KEYS`); the interface is in `ActionRunner.ts`. -The wrong pointer had teeth: an author who followed it edited the inventory -alone, which is precisely the half-change the pin test reddens on, since it -re-derives the inventory from the interface's AST. - -The tail now carries the reason the module's own header already gives for why -this warning survived step 3 — the two mechanisms cover disjoint populations. -`tsc` sees action literals authored in code; the warning sees actions that -arrive as data, from stored rows that are rehydrated unparsed and that no -compiler ever looked at (objectstack#3903). The prescription names -`ActionRunner.ts` for the field and `ACTION_DEF_KEYS` as the same-commit second -edit, saying why. - -No behaviour change: the classification logic, the key inventory and its -derivation are untouched, and the warning fires on exactly the same actions as -before. Two comments in the same file and two in the pin test that described the -pre-step-3 world were refreshed in the same pass, and the message text is now -pinned — the printed interface path is resolved off the message and read, so a -move or rename reddens by name instead of shipping a second dead prescription. diff --git a/.changeset/action-onsuccess-forward-5493.md b/.changeset/action-onsuccess-forward-5493.md deleted file mode 100644 index b74a69cef5..0000000000 --- a/.changeset/action-onsuccess-forward-5493.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`action:button`, `action:icon`, `action:group` and `action:menu` now forward the -declared `onSuccess` block, so the post-success navigation an author writes in metadata -actually happens on those four surfaces (objectui#5493). - -`onSuccess` — spec's closed strict `{ navigate, openIn }` object — became authorable on -`ActionSchema` with the `@objectstack/spec` 17.1.0 pin bump (objectui#5328), and the -runner has read it off the **forwarded** def since objectui#5221: -`ActionRunner.handlePostExecution` → `readOnSuccessNavigation` → `navigateOnSuccess`, -which hops through the app's own `navigationHandler` (a real SPA route change, immune to -popup blocking). Between those two halves sat these four forward whitelists, which never -carried the key. The action succeeded, the toast said so, and the declared hop silently -did not happen — the same "shipped green while dropped one hop before the runner" class -as `bodyExtra` (objectstack#6837), `bodyShape` (objectstack#6938) and `resultDialog` -(objectui#3646). - -Reachability before this change was a function of which host rendered the action: the -full-def-spread hosts (`DeclaredActionsBar`, `ObjectGrid.onActionDef`, -`RelatedRecordActionsBridge`, `useNavActionDispatch`) already carried it through, so the -same declaration hopped there and did nothing here — and on an `action:bar`, whether an -action lands inline (`action:button`) or in the overflow (`action:menu`) is decided by -`maxVisible`, 3 desktop / 1 mobile. The declared navigation therefore depended on -viewport width. - -Pinned end to end in -`packages/components/src/renderers/action/__tests__/action-onSuccess-forward.test.tsx`: -one row per surface drives the real renderer through the real runner and asserts the -`${result.*}`-interpolated url reaches `onNavigate`, with `openIn` exercised on both -branches. Each row asserts the action executed first, so a zero-navigation reading -cannot be a harness that did nothing. - -The four matching `KNOWN_GAPS` entries in `scripts/check-action-forward-parity.mjs` are -deleted — that ledger is ratcheted, so a stale entry excusing a key that is now -forwarded fails the gate. `element:button` is untouched and stays correct: `onSuccess` -is not on spec's `InlineActionSchema` pick list, so that surface never owed it. diff --git a/.changeset/action-param-degrade-predicate-5654.md b/.changeset/action-param-degrade-predicate-5654.md deleted file mode 100644 index 2fc5978fe4..0000000000 --- a/.changeset/action-param-degrade-predicate-5654.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -**Behaviour change:** a `master_detail` action param with no reference target now -receives the same "paste a record id" placeholder and help text a targetless -`lookup` param has had since objectui#3405. Previously it degraded to a plain -text input exactly like `lookup` did, but silently — the user was shown an -unexplained empty box that wanted a bare record id. - -The cause was two hand-maintained answers to one question. `paramToField()` -performs the degradation over RESOLVED widget keys (`lookup`, `master_detail`), -while `ActionParamDialog` decided who gets the hints with its own literal over -RAW param spellings (`'lookup' || 'reference'`). Neither set contained the -other: `master_detail` degraded with no hints, and `reference` was a copy of an -alias-table row the adapter folds to `lookup` before it tests membership. - -`paramToField` now exports `paramDegradesWithoutTarget(param)` and both the -adapter's own fallback branch and the dialog's two hint readers ask it, so "who -degrades" and "who gets hints after degrading" are one member set answered once -(objectui#5654; same convergence shape as objectui#4770 / #4790 / #4815). - -Params that already had the hints keep them, including the `reference` spelling; -a param that renders as text without having degraded (an unknown type) still -gets no picker hints. diff --git a/.changeset/action-runtime-same-origin-only-5702.md b/.changeset/action-runtime-same-origin-only-5702.md deleted file mode 100644 index 1340b71436..0000000000 --- a/.changeset/action-runtime-same-origin-only-5702.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -**Behaviour change:** the console action runtime (`useConsoleActionRuntime`) now -builds its authenticated fetch with `sameOriginOnly: true`, matching the -`provider: 'api'` data-source lane (`ConsoleShell`). A metadata `type: 'api'` -action whose resolved target is a different origin than the page is fetched -through the bare global fetch: the platform Bearer token, `X-Tenant-ID`, and -`Accept-Language` are no longer attached (objectui#5702, maintainer ruling -2026-08-22). Previously the Bearer rode to any off-origin target whose URL -contained `/api/`, and `X-Tenant-ID` rode to every off-origin target -unconditionally. - -Same-origin actions — including every relative target in a same-origin -deployment — are unchanged, and off-origin requests still execute (pass-through, -not a refusal). An off-origin integration that legitimately needs the platform -bearer declares itself explicitly or proxies same-origin. - -Note for split-host setups: `apiHandler` prefixes relative action targets with -`VITE_SERVER_URL`. When that is set to an origin different from the page's, -those action requests are off-origin and no longer carry credentials. The -committed dev and starter configurations are unaffected: since objectui#5745 -they ship `VITE_SERVER_URL` empty and reach a split-host backend through the -Vite `/api` dev proxy, so their action requests are same-origin. diff --git a/.changeset/active-org-clear-removal-failure-5731.md b/.changeset/active-org-clear-removal-failure-5731.md deleted file mode 100644 index aacb03bc76..0000000000 --- a/.changeset/active-org-clear-removal-failure-5731.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually -gone and, when it is not, both reports the failure and stops `get()` answering from -the surviving value — instead of swallowing the failed removal (objectui#5731). - -`clear()` nulled `_memoryValue` and then removed the persisted key inside a -`try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a -NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two -halves of `clear()` were not equally strong: nulling memory always sticks, while a -removal that did not stick left the key readable and the read order preferred it. -Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does -not stick", and it was silent — the cleared organization went back on the wire as -`X-Tenant-ID` on every subsequent request. - -The removal is now judged by a READ-BACK rather than by catching the throw, which is -both narrower and wider in the right directions. Wider: a wrapped or proxied -`localStorage` whose `removeItem` is a silent no-op never throws and leaves identical -residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where -every operation throws have nothing readable to resurrect, were already safe, and are -not reported as failures. - -A key whose removal could not be verified is quarantined in memory for the rest of the -page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`, -which `clear()` has just nulled and which a later `set()` refills with the value that -write was meant to persist. The quarantine is released as soon as a removal on that key -sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract -(objectui#5279); a re-stamped signed-out organization is not. - -The failure is not thrown and not returned. All five call sites — sign-out's -`purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`, -`leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive -after the transition they follow up on has already happened, and none can act on a -storage failure; a `boolean` every caller ignores would read as handled when it is not. -So the invariant is restored inside `clear()` and the failure is reported to the -console. - -A working `localStorage` behaves exactly as before: the removal sticks, nothing is -quarantined, nothing is reported, and a non-null persisted read is still authoritative. -`set()`'s swallowed write failure is deliberately untouched — that swallow is -objectui#5703's memory fallback, and it is the correct kind, because the memory copy -upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s. diff --git a/.changeset/active-org-storage-memory-fallback-5703.md b/.changeset/active-org-storage-memory-fallback-5703.md deleted file mode 100644 index eef9655d93..0000000000 --- a/.changeset/active-org-storage-memory-fallback-5703.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls -back to the in-memory value otherwise, instead of returning the `localStorage` read -unconditionally — so the fallback is reachable in the browser state it was written for -(objectui#5703). - -`set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but -`get()` only consulted `_memoryValue` when the READ itself threw. There is a real -browser state where the read does not throw and the fallback is nonetheless the only -copy: `localStorage` present and readable but rejecting writes — Safari private -browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`. -In that state the active org was stored and could not be read back, measured as -`_memoryValue = org-42` alongside `get() = null`. - -The cost was silent and lasted the whole session rather than the documented first-boot -window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on -any request. Per the edge contract documented on objectui#5279 that header is a routing -hint a reader falls through on — the framework scopes from the session — so no row -visibility rode on this; what was missing is the tenant-routing input, on every request. -`switchOrganization` also appeared to succeed while the client-side stamp never -followed. - -Sign-out is unaffected, and that is the half worth stating: the new fallback fires -exactly when the `localStorage` read is null, which is the state `clear()` leaves -behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That -property is now pinned by test rather than relied upon, so a future `clear()` that only -removed the persisted key fails a test instead of quietly re-stamping a cleared org. - -A non-null persisted read still wins over the memory value, so a working `localStorage` -behaves exactly as before. diff --git a/.changeset/activity-map-single-reading-5878.md b/.changeset/activity-map-single-reading-5878.md deleted file mode 100644 index b04790a791..0000000000 --- a/.changeset/activity-map-single-reading-5878.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-detail': minor ---- - -The console record page and the `record:activity` block read ONE `sys_activity` -type table, so a scheduled meeting no longer appears on one and vanishes on the -other. - -`RecordDetailView`'s `sys_activity` merge carried a hand-written copy of the -table that `record:activity` exports as `ACTIVITY_TYPE_TO_FEED_TYPE`. Neither -file imported the other and nothing compared them, so the two could drift -silently — and they had. objectui#5840 added `scheduled` -> `event` to the -exported table, because a shipped producer (HotCRM's `schedule_meeting`) writes -that value and the row was being dropped before any filter ran. The copy here -was left untouched, so the same row rendered on a hand-authored record page and -was dropped on the console record page: same record, same row, two answers. - -`RecordDetailView` now imports the exported table, and the copy is gone. The -table is re-exported from `@object-ui/plugin-detail`'s entry point, which is -what makes a single reading possible at all — it was previously reachable only -from inside the plugin. No module enters the eager closure: the module holding -it was already pulled in by the renderer beside it. - -Adding an activity type is now one edit, in one place, that both surfaces see. -A re-fork is caught rather than merely discouraged: the new pins spy on the -shared object and inject a member into it at runtime, so a private copy holding -today's members exactly — the failure this change removes — fails, where a -value comparison would pass on the defect. - -Only the table converges. The row-to-`FeedItem` construction around it is still -written twice, so an unmapped activity type is still dropped silently on the -console surface where the block warns once; that mirror is filed separately. diff --git a/.changeset/adapter-advisory-sink-pin.md b/.changeset/adapter-advisory-sink-pin.md deleted file mode 100644 index 7a9a2c668e..0000000000 --- a/.changeset/adapter-advisory-sink-pin.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: pins that a metadata-save advisory emitted by the `ObjectStackAdapter` actually reaches the toast sink `AdapterProvider` wires to it. No published behaviour changes. diff --git a/.changeset/adapter-meta-type-singular-4940.md b/.changeset/adapter-meta-type-singular-4940.md deleted file mode 100644 index a96ab64517..0000000000 --- a/.changeset/adapter-meta-type-singular-4940.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata -types in the singular, matching the other twelve `client.meta.*` call sites in this -file (objectui#4940). - -`getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed -the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued -its singular spelling was deliberate — the plural site was silent. Both plural sites -resolved today only because the server folds plural → singular -(`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`, -confirmed by reading both the mapping and the by-name route handler that calls it), so -this is consistency restoration rather than a behavior change — nothing a user hits was -broken, and nothing a user hits changes. - -`appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended -with two new cases asserting `getApp`/`getPage` pass the singular type to -`client.meta.getItem`, so a future revert to the plural spelling fails a test instead of -depending on the server-side fold staying in place. diff --git a/.changeset/adr-0057-d10-citation-attribution-5202.md b/.changeset/adr-0057-d10-citation-attribution-5202.md deleted file mode 100644 index e30834a070..0000000000 --- a/.changeset/adr-0057-d10-citation-attribution-5202.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Traceability only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The change is comment text at seven live source sites; no -executable line moves, and `git diff -U0` carries zero non-comment added or removed lines. - -`server enforces, client is courtesy` was cited at those seven sites as a bare -**`ADR-0057 D10`**. The substantive claim is correct and is unchanged here; what was missing -is the **framework qualifier**. In this repository the bare string resolves to -`docs/adr/0057-console-ai-chat-one-conversation-docked.md` — a document about console AI chat -docking, which contains no `D10` at all and is the one a reader greps first. The intended -anchor is the *framework's* ADR-0057, whose D10 decides *"Setup-nav surfacing follows the -capability (ADR-0029 K2); the object stays open"*. - -Each site now carries the disambiguation already shipped by the two authorities in this -repository — `docs/adr/0036-field-conditional-rules.md:91` and -`packages/core/src/evaluator/fieldRules.ts:38` — rather than a third phrasing: -`the framework's ADR-0057 D10 — framework numbering; this repo's own ADR-0057 is an -unrelated document`. - -Three sites are deliberately left byte-untouched, all three already correct: -`packages/data-objectstack/src/appAccessProbe.test.ts:25` (a verbatim quotation of the -objectstack#8013 ruling, and about the capability/nav gate — the one family D10 really does -decide), plus the two authorities above. diff --git a/.changeset/ai-chat-public-share-base-4482.md b/.changeset/ai-chat-public-share-base-4482.md deleted file mode 100644 index 3ad06d0251..0000000000 --- a/.changeset/ai-chat-public-share-base-4482.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`AiChatPage`'s public share-link base now resolves through the one console-mount -resolver instead of a private copy of it (objectui#4482). - -The page built `publicShareBase` itself — read the injected ``, take its -pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third -independent implementation of the mount resolution `resolveConsoleUrl` centralizes. -objectui#4472 had just deleted the other two on that rule; this was the surviving -sibling. Its output was correct, so nothing a user hits was broken and nothing a user -hits changes: measured over the base-href matrix, the deleted builder and -`resolveConsoleUrl('s')` return identical URLs for every shape the console is served in -— `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable -builds, nested mounts, and no `` at all. - -The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`, -which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM -it returns `undefined` rather than a URL built from an origin that does not exist, so -`ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument — -the mount is only ever carried by the injected ``, and a resolver with no -other input cannot be pinned by a test that steers something production never reads. - -`resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected -`` element for each deployment shape, plus a structural case asserting no other -app-shell file reads the `` tag — so a fourth copy fails a test rather than -waiting for mount semantics to change under it. diff --git a/.changeset/ai-quota-ledger-vocabulary-3804.md b/.changeset/ai-quota-ledger-vocabulary-3804.md deleted file mode 100644 index 61ec9e5047..0000000000 --- a/.changeset/ai-quota-ledger-vocabulary-3804.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -Recognize the landed AI quota ledger vocabulary in the chat error path - -`parseAiQuotaError` now accepts the three SCREAMING_SNAKE ledger codes the cloud -token guardrail emits (`AI_ALLOWANCE_EXHAUSTED`, `AI_DESIGN_QUOTA_EXHAUSTED`, -`AI_DATA_CHAT_TRIAL_EXHAUSTED`) alongside the legacy lowercase trio, which stays -readable for producers that have not converged yet. The companion fields -(`messageEn` / `upgrade` / `topUp` / `resetsTonight`) are now read from the -declared envelope's `error.details` as well as their legacy top-level position, -with the declared position winning. - -A quota-exhausted user gets the upgrade / top-up CTA again instead of the -generic "Response failed" banner. The per-turn message cap's generic -`QUOTA_EXCEEDED` deliberately keeps its existing rate-limit path — it has no -upgrade or top-up next step. diff --git a/.changeset/ai-studio-accessor-5577.md b/.changeset/ai-studio-accessor-5577.md deleted file mode 100644 index dca9479328..0000000000 --- a/.changeset/ai-studio-accessor-5577.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead -of being spelled inline at two call sites (objectui#5577). - -`features.marketplace` already had a documented accessor whose docblock is where the -fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating -`/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*, -plus the "never infer this from the shape of a failure" warning. `features.aiStudio` -had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false` -and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine -had two spellings and neither reader could cite it. - -The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and -against a runtime-config snapshot whose `features` is absent that read is a TypeError -rather than a fail-open — the exact shape that crashed 29 tests across four suites in -PR #5575 before it was corrected. Measured here: no live path can currently deliver -such a snapshot to `ChatDock` (the module's singleton constructs `features` on every -write and exports no setter, and no suite mounts the dock's default body under a -partial stand-in), so this closes a reachable-by-construction crash rather than a live -one — and it closes it at the source by leaving no inline read to get wrong. - -`isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`: -neither is re-exported from `src/index.ts`, so the package's published `exports` surface -is unchanged. diff --git a/.changeset/alert-dialog-fixture-premise-pin-7693.md b/.changeset/alert-dialog-fixture-premise-pin-7693.md deleted file mode 100644 index 82d6073136..0000000000 --- a/.changeset/alert-dialog-fixture-premise-pin-7693.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change in `@object-ui/types`: the pin that recorded objectui#7693's filed premise (the four alert-dialog schema-catalog fixtures authoring an `actions` array) is re-derived onto the read dialect now that the card has landed. No published behaviour changes — `overlay.ts` and `zod/overlay.zod.ts` are untouched, and the only file edited under `src/` is a test. diff --git a/.changeset/alias-closure-relative-miss-5386.md b/.changeset/alias-closure-relative-miss-5386.md deleted file mode 100644 index f67ab4d329..0000000000 --- a/.changeset/alias-closure-relative-miss-5386.md +++ /dev/null @@ -1,39 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file changed is -`examples/console-starter/test/vite-alias-closure.test.ts`, and that example is private. - -Makes the alias-closure walker record an unresolvable **relative** import instead of -dropping it. - -`computeClosure()` walks two kinds of specifier. The bare-specifier branch pushed a miss -onto `unresolvable`; the relative branch dropped one with no record at all. So -`expect(closure.unresolvable).toEqual([])` was not a weak assertion for relative imports -— it was a structurally empty one. It could not fail no matter how many relative -specifiers the walk failed to follow, and the `filesWalked` floor was the only signal -that anything had gone wrong. - -That is how two earlier conversions to explicit extensions (objectui#4538, objectui#5214) -each truncated this walk while landing green: the floor had enough slack to absorb both, -and only went red once app-shell — the largest package — converted and took the count -under 500. The direct symptom was being swallowed one branch away the whole time. - -Measured on `main` at the time of this change, with the resolver ablated to its -pre-objectui#5357 behaviour to reproduce that regression class: 275 relative specifiers -dropped, `filesWalked` 1245 to 402, packages reached 29 to 22 — and `unresolvable` still -reporting `[]`. With this change the same ablation fails the suite with all 275 named, -each alongside the file that imports it. - -The miss is recorded only for specifiers that are *meant* to be modules — no extension, -or one of the JS/TS emitted extensions. `ts.preProcessFile` also reports `./styles.css`, -`./data.json` and `./logo.svg`, which `resolveModule` cannot resolve by design, so -recording those identically would fail the suite for a reason that is not a defect. -Assets and Vite resource specifiers (`?raw`, `?url`, `?inline`) are skipped into a -separate `nonModuleSkipped` list — explicitly, and observably, rather than by accident. - -Four fixtures pin the class against the real walker so a future edit cannot silently -re-blind the branch: `./Foo.js` resolving through to the `Foo.tsx` on disk, planted -unresolvable modules of both spellings being named, assets staying out of `unresolvable` -while still being accounted for, and the classifier's boundary cases. diff --git a/.changeset/analytics-coded-400-rejected.md b/.changeset/analytics-coded-400-rejected.md deleted file mode 100644 index 1b0272bfff..0000000000 --- a/.changeset/analytics-coded-400-rejected.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -`classifyAnalyticsFailure` now reads a 400 as a refusal of the query body we -sent regardless of which ADR-0112 `code` it carries, so `aggregate()` no -longer answers a rejected filter with client-side numbers from a different -door (objectui#7755). - -Before this fix, only 400 `VALIDATION_FAILED` (and a code-less 400) threw -`AnalyticsQueryRejectedError`. Any OTHER coded 400 — `service-analytics` ships -its own 400 `INVALID_FILTER` on a filter shape it refuses — matched none of -`classifyAnalyticsFailure`'s branches and fell through to `unknown`, which -`aggregate()`'s catch has no arm for, so it silently degraded to -`aggregateViaFind`: a re-read through `find()`'s `$filter` query-string -contract, which accepts array shapes the analytics request body does not. A -filter the analytics route refused could still be answered — with a -plausible, wrong number, and no sign the request had a defect. - -The fix is a floor UNDER the existing code branches, not a replacement for -them: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` still win `not-installed`, -`VALIDATION_FAILED` / `UNAUTHENTICATED` / `CUBE_NOT_FOUND` still win their own -outcomes first (objectui#5721). Only a 400 that none of those four already -claimed now falls to the new floor instead of past it. An unmatched NON-400 -coded error (e.g. a coded 5xx) is unaffected and keeps degrading exactly as -before — this fix is scoped to the 400 case only. diff --git a/.changeset/analytics-error-branch-mapping-5663.md b/.changeset/analytics-error-branch-mapping-5663.md deleted file mode 100644 index e0809a7683..0000000000 --- a/.changeset/analytics-error-branch-mapping-5663.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's -ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an -unauthenticated session stop being reported as a missing analytics capability -(objectui#5663). - -Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`: -the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and -the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The -mapping tested `res.status === 501 || res.status === 404` and called all of it -"the analytics capability is not installed", so every unknown dataset produced a -banner telling the operator to install `@objectstack/service-analytics` and mount -`AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on -four HotCRM Executive Overview widgets while the analytics service was installed -and answering — the real condition was an installed `app.objectstack.hotcrm` at -1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of -the system from the remedy the banner named. - -Three conditions now get three answers, each keyed on the code the framework -declares for it: - -- `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and - `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing - `AnalyticsNotInstalledError` and its copy — one remedy, one message. -- `NOT_FOUND` (404, unknown `datasetName`) throws the new - `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the - dataset and pointing at the installed app's version rather than at the server. -- `UNAUTHENTICATED` (401, `enforceAuth`) throws the new - `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the - request was refused before it ran and therefore says nothing about the - capability. - -The banner also used to print the server's own message in a parenthetical while -contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not -found.` under a headline claiming a missing capability. That is now structurally -impossible rather than merely fixed: the headline is a pure function of `code` and -the parenthetical is a verbatim quote of `message`, both read off the same -response, and a test walks every branch asserting each message carries its own -headline and none of the others'. - -Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its -constructor signature (it gains an optional third `serverCode` argument and a -`serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including -the metadata-admin dataset preview — are unaffected. A 404 carrying a code this -client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now -keeps its server detail instead of being relabelled as a missing capability; a 404 -or 501 carrying no code at all is still read as the capability being absent, since -the route's own `NOT_FOUND` always ships a code. diff --git a/.changeset/analytics-failure-code-first.md b/.changeset/analytics-failure-code-first.md deleted file mode 100644 index 8b9043df2c..0000000000 --- a/.changeset/analytics-failure-code-first.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -Classify `/analytics/query` failures by their ADR-0112 `code` rather than their HTTP status, so a chart is no longer answered from a different code path behind the wrong explanation. - -`classifyAnalyticsFailure` tested `status === 404 || status === 501` before the code operands on the same line, so the status short-circuited every one of them: any 404 on this face was classified "the analytics capability is not installed" whatever code it carried, and `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the conditions they name. Three unrelated conditions answer 404 on this url, so the status cannot tell them apart — the `code` is the contract. - -Two conditions change behaviour: - -- **404 `CUBE_NOT_FOUND`** (a misspelled or unregistered cube — an authoring mistake) now **throws** the server's own error verbatim, keeping `code` and the producer's repair instructions. It previously warned "install `@objectstack/service-analytics`" and silently degraded to `find()` + client-side aggregation — which cannot answer it anyway, because the fallback re-reads the same name through `/data`, where an unregistered object is a 404 `OBJECT_NOT_FOUND`. -- **401 `UNAUTHENTICATED`** (an anonymous or lapsed session) now **throws** `AnalyticsUnauthenticatedError` instead of degrading silently behind a `find()` that is about to be refused the same way. - -Unchanged: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` and code-less 404/501 answers still degrade loudly to the client-side fallback, 400 `VALIDATION_FAILED` still throws, and 5xx / network failures still degrade silently. diff --git a/.changeset/anon-seed-scope-pin-5828.md b/.changeset/anon-seed-scope-pin-5828.md deleted file mode 100644 index 12f0afa2b7..0000000000 --- a/.changeset/anon-seed-scope-pin-5828.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-only: lands the objectui#5746 enumeration harness as a regression pin under -objectui#5828. It mounts the real console boot path (real `AuthProvider`, real -`ConnectedShell` session gate, real `MetadataProvider`; only the auth and -metadata servers are doubled) and asserts what is true today — including that -the guest and `previewMode` boots write `objectui:metadata:app:@none:@anon`, -which is the open observation objectui#5828 carries, not a change made here. The -pin that can fail is S6: it goes red if objectui#5198's principal-scoped cache -key stops discriminating principals. No published behaviour changes. diff --git a/.changeset/anonymous-expression-user-id-6534.md b/.changeset/anonymous-expression-user-id-6534.md deleted file mode 100644 index c3c8459aae..0000000000 --- a/.changeset/anonymous-expression-user-id-6534.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The predicate identity bound for a signed-out visitor now carries `id: null`, so a -`ctx.user.id` visibility gate BITES instead of failing open (objectui#6534). - -`buildExpressionUser` has two branches, and only the signed-in one carried `id`. -So `'id' in buildExpressionUser(null)` was `false`, and an absent key is not -`false`: a CEL predicate naming `ctx.user.id` / `current_user.id` / `os.user.id` -hit an unbound key for a signed-out visitor and FAULTED. A faulting visibility -predicate fails OPEN (`evaluateVisibility`), so the gated field or action rendered -for exactly the principal it was written to exclude, with nothing on screen to say -the gate had not bitten — silently, for every signed-out visitor. - -Because the defect was in the shared normaliser rather than at a mount site, it -reached EVERY mount site, including `AppContent` and the console's -`InternalFormRoute`, both of which have always called the normaliser correctly. -This is the same fault-open mechanism objectui#6515 fixed one level up, where -`RecordFormPage` hand-rolled a descriptor missing `id` and `isPlatformAdmin`. - -`null` rather than `undefined`, and rather than leaving the key absent, is settled -by precedent on this exact object rather than chosen here. objectui#5424 removed -`roles` from it because a present-and-always-`undefined` key "is the shape that -teaches the wrong thing" — the context answers rather than being plainly absent, -and the answer is silently wrong; `undefined` here would reproduce that defect one -key over. Leaving it absent IS the defect. `null` is a VALUE a CEL author can -compare against, so `ctx.user.id == '…'` resolves to a clean FALSE. Measured, not -assumed: at a real mount site with `authState.user = null` and a field gated on -`ctx.user.id == 'u_admin'`, the field is now filtered OUT of the schema handed to -`ObjectForm`, where before it was present. - -This also closes the last asymmetry between the two branches. Both now advertise -the same six keys, which is the symmetry objectui#5424 was closing when it removed -`roles` — and the shape pin now asserts the key sets are equal, so a future edit -that adds a key to one branch and forgets the other fails whichever branch it -forgets. - -NOT CHANGED, deliberately: fail-open on a predicate that DOES fault. That is -shipped permission-boundary policy (objectui#6443 / #6487 / #6445) and remains -exactly as it was — an unevaluable `visible` still renders. This change removes a -REASON to fault; it does not touch what happens once a predicate has. No accept set -is widened, no gate is relaxed and no fallback is added: the only behavioural -movement is that an id-gated surface which used to render for anonymous visitors -now hides from them. diff --git a/.changeset/app-hidden-catalogue-flag-7542.md b/.changeset/app-hidden-catalogue-flag-7542.md deleted file mode 100644 index 547d0f353f..0000000000 --- a/.changeset/app-hidden-catalogue-flag-7542.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`AppComponentSchema.hidden` is now declared `boolean` — the spec's app-catalogue flag — instead of the `boolean | ExpressionWire` hide predicate it inherited from `BaseSchema`.** On the `app` node two keys collide in name and differ in meaning (objectui#7542): the zod mirror takes `@objectstack/spec/ui` `AppSchema.hidden` ("Hide from the App Switcher") by reference through `SpecAppFields`, where the spec's `z.boolean().optional()` lands after the base's key and overrides it, so the validator has always refused a predicate string or a CEL envelope object at path `hidden` on an `app` document — through `AppComponentSchema.safeParse` and through `safeValidateSchema` alike — while the published TypeScript interface, restating nothing, invited exactly that spelling. This is direction 1 of the card: the declaration is pulled back to what the validator enforces. Direction 2 — giving the catalogue flag its own name upstream in `@objectstack/spec` so the app node can inherit the renderer's predicate again — stays open as the alternative and is a protocol change, not taken here. - -Breaking on the TypeScript face only: a predicate string or envelope on an `app` node no longer type-checks, and an `app` node cannot use the predicate spelling every other node accepts. Authored JSON metadata is unaffected — the validator never admitted it — and the in-repo reader (`filterActiveApps` in `@object-ui/app-shell`) already treats the key as the boolean `hidden !== true`, never evaluating it. The renderer's `hidden` predicate on every other node is unchanged, `BaseSchema.hidden` is untouched, and the `KnownDrift` row objectui#7455 seeded for this pair is removed because the drift it recorded is gone. diff --git a/.changeset/app-shell-nodenext-pin-5440.md b/.changeset/app-shell-nodenext-pin-5440.md deleted file mode 100644 index f6674a1eb9..0000000000 --- a/.changeset/app-shell-nodenext-pin-5440.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Pin `module` / `moduleResolution` to `nodenext` in `@object-ui/app-shell`'s build config, matching the pins `@object-ui/react`, `@object-ui/fields` and five other packages already carry. This package builds with a bare `tsc`, which never rewrites import specifiers, so what the source writes is exactly what `dist` ships; under `nodenext` a missing relative extension is a compile error, so the extensionless-specifier defect that made published entries unloadable under plain Node cannot come back silently here. - -The lazy `@monaco-editor/react` imports in the metadata designer's source editors now read the package's named `Editor` export instead of its default. `@monaco-editor/react@4.7.0` is CommonJS and ships no `exports` map, so under `nodenext` the default resolves to the module namespace rather than to the component. The two names are one declaration in that package's own typings and the same object at runtime in both its CommonJS and ESM builds, so the editor that renders is unchanged. diff --git a/.changeset/app-shell-page-header-subtitle-4761.md b/.changeset/app-shell-page-header-subtitle-4761.md deleted file mode 100644 index 01333ab79c..0000000000 --- a/.changeset/app-shell-page-header-subtitle-4761.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's `` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761). - -This repository has two components named `PageHeader`. `@object-ui/layout`'s is -the renderer for the authored `page:header` / `page-header` node and converged -on `subtitle` in objectui#3789, because `subtitle` is the key -`@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s — -the console's own title row, drawn by `ObjectView` and `ObjectDataPage` — -spelled the very same concept `description` and had no `subtitle` at all. Both -rendered correctly; the defect was one concept carrying two key names one -package apart, the objectstack#4115 shape moved up a layer. An author reading -one component to learn the other was being taught a key the contract does not -have. - -**Not a breaking change, measured rather than assumed.** The convergence is a -plain rename with no alias, because this component is not on the published -surface: - -| gauge | result | -|---|---| -| exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) | -| `exports` map | declares exactly `.` and `./styles.css` | -| Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves | -| in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) | -| emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) | - -No supported specifier reaches the prop, so there was nothing to keep -compatible, and a renderer-side `description` alias would have been exactly the -second dialect AGENTS.md #0.1 forbids — the layout side had just finished -retiring one. Out-of-repo consumers cannot be enumerated from this repository; -what can be, and is, is the set of import paths through which one could have -reached this component, which is empty. - -Rendered output is unchanged: same element, same classes, same position. The -patch tier is a declaration that the tarball moved, not a claim that a consumer -must act. - -`packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the -pin the card asked for. It asserts the subtitle on the DOM a reader gets (a -`

`, in the title block, after the `

`), that `description` now draws -nothing and is rejected by the compiler, and — the assertion that actually goes -red if either side drifts again — that both packages' `PageHeaderComponentProps` -declare `subtitle`. diff --git a/.changeset/app-shell-provision-envelope-shape-6707.md b/.changeset/app-shell-provision-envelope-shape-6707.md deleted file mode 100644 index fdf65fd3cc..0000000000 --- a/.changeset/app-shell-provision-envelope-shape-6707.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`provisionProductionEnvironment` now REFUSES a success payload whose `data` -carries no `environment` row, instead of resolving best-effort (objectui#6707). - -objectui#6629 fixed this consumer to read the created environment from the -nested `environment` key, but deliberately left the envelope check alone: it -catches a **missing** `data` and says nothing about `data`'s **shape**. So a -producer that regressed to a flat payload would once again resolve successfully -with `id` and `hostname` both `undefined` — the same silent outcome #6629 had -just fixed, reachable again by a producer change alone. - -A flat payload is a producer contract violation, not a second dialect to be -tolerated, and tolerating it is how the original defect stayed invisible. The -call refuses it now, which routes a producer regression to this call's already -documented failure path rather than a successful-looking no-op: the sole caller -(`CreateWorkspaceDialog`) already wraps the call in `try`/`catch`, logs a -warning, and lets the onboarding gate re-provision lazily on first navigation. -Workspace creation itself is unaffected — the caller does not re-throw, and it -never read this call's return value. - -The refusal carries its own diagnostic, distinct from the missing-envelope one, -so a logged warning still distinguishes "the control plane did not wrap the -payload" from "it did not put the row where it says it does". - -The wire shape this is written against is confirmed producer-side rather than -inferred from this consumer — the distinction is load-bearing, because before -#6629 the only in-repo artifact pinning this payload was a hand-written mock -pinning the bug shape. Both sources are recorded on the function's docblock. diff --git a/.changeset/approvals-inbox-cell-remount-5348.md b/.changeset/approvals-inbox-cell-remount-5348.md deleted file mode 100644 index 4a11753a5b..0000000000 --- a/.changeset/approvals-inbox-cell-remount-5348.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Approvals Inbox stops remounting every row on every render. - -`ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions` -inside its own component body. React identifies a component by the identity of -its function, so each render produced three brand-new component *types* and React -unmounted and remounted every row's subtree instead of updating it — and the page -holds its clock in state and ticks it every 60s, so this fired on a timer whether -or not anyone was touching the page (objectui#5348). - -Two consequences were reproduced against `origin/main` before the fix, in -`apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`: - -- **Transient subtree state is discarded.** Focus placed on a row's Approve - button moved to `` on the next clock tick. -- **Input is silently swallowed.** A pointer sequence that spans a re-render — - press, tick, release — left the confirmation dialog unopened: the captured node - had been replaced, so React's delegated listener never saw the click. This is - the failure objectui#5211 hit and worked around at its call site - (`Unable to find role="alertdialog"`). - -The three cells are now at module scope beside `StatusBadge`, which was moved -there for the same reason and already carries the explanation. Everything they -closed over is passed in: `RequestCell` and `InlineActions` take the page's -scoped translator, and `RecordCell` takes `href: string | null` — one prop rather -than two, so the objectui#5211 readable/unreadable decision and the URL cannot be -handed in disagreeing with each other. - -The verification asserts the consequence, not the placement. A test that checks -the three functions now sit at module scope stays green for a refactor that moves -them and introduces a fourth inline component beside them; these cases compare -DOM-node identity for all three cells across a clock tick, which no remount can -pass, and re-drive the swallowed click. - -That guard is load-bearing because lint cannot supply one here. -`react-hooks/static-components` exists for exactly this class and is `error` in -this repo via the plugin's recommended set, yet it reports nothing on this page: -measured on `origin/main`, an arrow-form inner component injected into -`ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same -shape in a ten-line file produced two. The rule's analysis bails out on this -component, which is how three of them shipped. diff --git a/.changeset/approvals-queue-hidden-amount-6020.md b/.changeset/approvals-queue-hidden-amount-6020.md deleted file mode 100644 index 3c0b81e920..0000000000 --- a/.changeset/approvals-queue-hidden-amount-6020.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/console": patch ---- - -Approvals inbox: the queue rows and the amount sort now honour each request -object's own `hidden: true` field declaration (objectui#6020). - -The `hidden: true` trim added for the drawer summary card reached only the -drawer — the desktop queue row, the mobile card and the amount comparator -still read the field, so an amount an app author declared hidden rendered -inline in the queue and ordered the list, which leaked its relative magnitude -even to a viewer who never saw the figure. - -The queue spans many objects, so the trim is a per-object lookup and every row -is answered about its own object; a row left with no renderable amount now -sorts with the other amount-less rows. `hidden` stays a UI contract -(objectstack#10749) and the filter still fails open: an unanswered or failed -metadata read renders today's figure. diff --git a/.changeset/approvals-raw-payload-gate-5553.md b/.changeset/approvals-raw-payload-gate-5553.md deleted file mode 100644 index 3cd1ed2642..0000000000 --- a/.changeset/approvals-raw-payload-gate-5553.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Approvals Inbox no longer shows a business approver the submitted record's raw -row JSON. - -The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no -principal check of any kind — so every approver could expand (and one-click copy) the -complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`, -`organization_id`, bare lookup ids, and **the fields the object's metadata declares -`hidden: true`**. Reported from a live EHR deployment on 17.1.0 -(objectstack-ai/objectstack#10734), where that declaration is a patient-data control. -The app author had no legitimate lever to remove the panel — field `hidden`, view -columns, app navigation, permission sets and env vars are all ineffective against it — -so the remedies available in the field were patching the shipped bundle or injecting -CSS. - -The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's -`studioEntry` module: `studio.access` is a declared platform-scope capability that a -tenant org owner does not hold by design, and it already reaches the browser in -`systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served, -computed or made authorable — no new config key, no new i18n copy, and the panel is -byte-for-byte unchanged for the platform operator it was written for. A business -approver keeps the structured record summary, the approval chain, the activity feed -and the decision actions; only the raw snapshot is gone. - -The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from -`usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a -holder's button while the server still refuses the write is the worse outcome for an -action. This panel has the opposite stake, since the measured defect is a non-holder -seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend -predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200` -with no `systemPermissions` at all, and a reported empty array. A deployment whose -permission layer just failed must not be the one that leaks the snapshot. - -`ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the -acceptance condition is that something does *not* render — which an empty render -reproduces perfectly — every denial case also asserts the drawer it denies inside, and -the `studio.access` case drives the same fixture through the same helper and finds the -panel. `created_by` and `organization_id` are the witnesses: both are in the page's -`PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can -reach the DOM only through the raw panel. Ablating the gate (restoring the bare -`payload != null` condition) turns the three denial cases red on exactly that -assertion and leaves the holder case green. - -Out of scope, tracked separately: trimming the summary by object metadata, and the -server-side residual that sends the unfiltered snapshot to the client at all. diff --git a/.changeset/approvals-step-progress-vertical-5554.md b/.changeset/approvals-step-progress-vertical-5554.md deleted file mode 100644 index 8bccea37ef..0000000000 --- a/.changeset/approvals-step-progress-vertical-5554.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/app-shell': patch ---- - -The approval step progress bar is a vertical stepper, so long flows stop -clipping their tail steps. - -Both occurrences were a single non-wrapping flex row whose steps were each -`shrink-0`. A flex row's min-content width is the sum of its non-shrinkable -items, so the bar's intrinsic width grew without bound with step count and -label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK -step names measured **1070px inside a 527px container** (objectui#5554). - -The two hosts failed differently, and neither failure was recoverable by the -reader: - -- **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not - scrollable, so the nearest scroller was the drawer *panel*. Reaching steps - 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the - record card, the activity timeline and the action buttons off-screen and left - a near-blank panel. -- **`RecordApprovalsPanel`** (the record page's approvals panel) — this one - carried `overflow-x-auto`, so it scrolled itself rather than its container. - Better, but the tail steps still sat behind a scroll gesture with no visible - affordance. - -In both, readers took the clipped bar for the end of the data; the reporting -customer acceptance tester said so verbatim. Widening the window does not help: -the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. - -Both now render as a column: one row per step, a badge-and-rail gutter, and a -label that may wrap. Width is capped by the container at every step count and -every label length, which also suits both hosts' tall-and-narrow aspect. The -rail segment below each step keeps the tint rule the horizontal connector used -— it is coloured by the step it leads *into*. - -**Always vertical, with no step-count or measured-width threshold**, because -the overflow is driven by intrinsic content width (labels x count), not by -count alone: three 16-character CJK labels already crowd a 527px drawer, so any -count threshold picks a cutoff that is wrong for some real flow, and a measured -one reintroduces a viewport-dependent branch. The card's requirement is a fix -that cannot break at an untested viewport or flow length, and a layout with no -breakpoint and no measurement is the form that satisfies it. Horizontal-with- -scroll was ruled out for both occurrences: it leaves steps behind a gesture. - -Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and -`RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is -green against the broken code too — every step was always in the DOM, and the -clipping was layout — so the suites assert the property the defect names -instead: no row is `shrink-0`, every label is `min-w-0` and none is -`whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no -axis, overflow or width-pinning class carries a breakpoint prefix (so there is -no viewport with untested behaviour). The reported failing regime is exercised -directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins -that the layout classes are byte-identical across all four, so no count -threshold can put some other flow length back on the old path. - -The two steppers are kept identical by hand rather than extracted to a shared -component: they live in different packages, and deduplicating them is a -refactor with its own surface. Filed separately. diff --git a/.changeset/approver-display-name-5414.md b/.changeset/approver-display-name-5414.md deleted file mode 100644 index f441983851..0000000000 --- a/.changeset/approver-display-name-5414.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -The approval panel identifies the pending approver by name, not by a truncated raw id. - -A record waiting on a position rendered its approver as `positi…ager` — the -engine reference `position:sales_manager`, 22 characters, past the identity -formatter's 14-character truncation arm and middle-truncated to fit its chip. The -step names beside it were human prose; the one line answering *who is holding -this record* was an internal identifier, and not even a complete one. The same -reference reached the admin-override confirm dialog un-truncated, so a paragraph -of plain governance prose ended `— position:sales_manager` (objectui#5414). - -Both surfaces now resolve the reference before rendering, in three tiers, most -authoritative first. The server's own `pending_approver_names` wins whenever it -answers, and a backend that resolves its own slate costs the record page no extra -request. Otherwise the console reads the directory row the spec's approver -binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, -for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no -adapter and no row, the machine name still prettifies into prose rather than -truncating. The raw reference stays on hover, which is where an internal -identifier belongs. - -An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` -is actionable where `positi…ager` is not, and it is the motivating rescue case -for the admin-override path. Staffing is deliberately tri-state — a -`sys_user_position` read the viewer is not permitted to make leaves the seat's -staffing UNKNOWN and says nothing, because "I could not look" is a different -claim from "nobody holds it" and only one of them is safe to print on a -governance surface. - -Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` -and `approvalsInbox.approverNameSeparator`. The separator is a translated -punctuation key rather than `Intl.ListFormat`, which was measured on this tree -joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with -no separator, reading as one person's name. - -The directory-backed kinds and their value columns are read from -`@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new -approver type is covered the day the spec publishes it. Id-valued kinds -(`user` / `team` / `department`) keep the existing middle-truncation: a row id -has no prose to recover, and that arm is objectui#3461's answer, not this card's -defect. diff --git a/.changeset/approver-identities-read-positions-5424.md b/.changeset/approver-identities-read-positions-5424.md deleted file mode 100644 index 2137b2b8c2..0000000000 --- a/.changeset/approver-identities-read-positions-5424.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) - -Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no -deprecation window, and the protocol-17 session face emits no `roles` key at -all. Three client sites still read it: - -- **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's - Approvals tab and Home's To-do card. It read nothing else, so it sent **no - `role:` identity at all**: an approval addressed to a position rather than to - a person matched nothing and vanished from all three surfaces, silently. -- **`approvalsApi.buildApproverIdentities`** — "My Pending" and the - Approve/Reject enablement. It also splits the scalar `user.role`, so it - degraded rather than dying: it still yielded `role:user` while dropping every - business position name (`manager`, `finance_approver`, …). -- **`AppContent`'s expression user** — forwarded a `roles` key that was always - `undefined` into every CEL predicate context. Removed; `positions` and - `isPlatformAdmin` were already forwarded correctly beside it. - -The retired spelling is **not** kept as a fallback — pairing the two is what -ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the -declaration. - -`AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately -untouched: it is a semantics decision, not a rename, and is deferred to a -maintainer ruling. diff --git a/.changeset/appsidebar-deprecated-5720.md b/.changeset/appsidebar-deprecated-5720.md deleted file mode 100644 index 404094e329..0000000000 --- a/.changeset/appsidebar-deprecated-5720.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`AppSidebar` is now `@deprecated` — use `UnifiedSidebar` instead. - -A census (objectui#5720) found `AppSidebar` has no in-repo mount point -(`ConsoleLayout` renders `UnifiedSidebar`, not this component) and no -downstream consumer visible anywhere across this org's GitHub-visible -repositories. It stays exported — from the package barrel and the published -`dist/index.d.ts` — because `@object-ui/app-shell` is a public npm package -(`publishConfig.access: "public"`) and an external consumer outside this org -is structurally invisible to that census; that is why it is deprecated -rather than deleted outright. No behavior change in this release — the -component still renders exactly as before. Its admin nav cluster is a -near-duplicate of `UnifiedSidebar`'s and has already drifted from it (it -gates only `sys-marketplace` on the workspace-admin flag, where -`UnifiedSidebar` gates the whole cluster); that divergence is not being -reconciled, since the component is scheduled for removal rather than kept -in parity — see objectui#5817 for the removal plan. - -Migration: replace any `AppSidebar` usage with `UnifiedSidebar` from the same -package. diff --git a/.changeset/audit-field-def-reference-narrow.md b/.changeset/audit-field-def-reference-narrow.md deleted file mode 100644 index 6a32b5c227..0000000000 --- a/.changeset/audit-field-def-reference-narrow.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Narrow `AuditFieldDef`'s relationship-target read in `app-shell`'s record History -diff helpers to the spec spelling `reference`, carried as a bare `string`. - -No release: nothing published changes. `AuditFieldDef` and `auditHistoryDisplay` -are package-internal — neither is re-exported from `src/index.ts`, and the -package's `exports` map exposes only `.` and `./styles.css`, so no consumer can -reach either. Runtime behaviour is unchanged on every document that can reach the -helper: `normalizeSchemaReferenceKeys` stamps both snake_case spellings at the -metadata ingestion choke point, and the removed `string[]` carrier was already -refused at runtime by the `typeof` narrowing that stayed. diff --git a/.changeset/authguard-positions-5424.md b/.changeset/authguard-positions-5424.md deleted file mode 100644 index 438aa5d16a..0000000000 --- a/.changeset/authguard-positions-5424.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@object-ui/auth": minor ---- - -`AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424. diff --git a/.changeset/block-config-schema-parity-8216.md b/.changeset/block-config-schema-parity-8216.md deleted file mode 100644 index a1fb9131d0..0000000000 --- a/.changeset/block-config-schema-parity-8216.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only: a parity gate pinning every `BLOCK_CONFIG` designer field name to the schema its block is judged by (`@objectstack/spec/ui`'s `ComponentPropsMap`, or the `@object-ui/types/zod` node arm). No published behaviour changes — no runtime source was touched. diff --git a/.changeset/block-schema-docs-truth-4895.md b/.changeset/block-schema-docs-truth-4895.md deleted file mode 100644 index 3019d9ed00..0000000000 --- a/.changeset/block-schema-docs-truth-4895.md +++ /dev/null @@ -1,46 +0,0 @@ ---- ---- - -Docs and a gate ledger only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. No package `src/` is touched: the two files -changed are `content/docs/blocks/block-schema.mdx` and -`scripts/check-doc-component-types.mjs`. - -Corrects `content/docs/blocks/block-schema.mdx` to the vocabulary the repository actually -declares, per the maintainer ruling recorded on objectui#4895 (Option B: docs-truth fix, -the block family re-scoped as type-level). - -The page taught a `{ type: 'slot', name: 'content' }` node inside -`BlockSchema.template`. `template` is typed `SchemaNode | SchemaNode[]`, so that snippet -sat on the render path — and `slot` is registered nowhere. Measured against the -`check-doc-component-types.mjs` derivation of the registered universe (659 keys), the only -keys matching `/block|slot/` are `blockquote` and `ui:blockquote`. A reader who copied the -snippet got the renderer's `OBJUI-001` "Unknown component type" panel. That node is -deleted, and the page now teaches `slotContent`, the key `BlockSchema` and -`BlockInstanceSchema` actually declare. - -**`slotContent` is documented as declared-but-not-yet-consumed, not as the working path**, -because that is what it measures as. Outside `packages/types` — the interface, its Zod -mirror, and that package's own parse test — nothing in the repository reads `slotContent`, -`slots` or `template`. Rewriting one phantom into a second phantom is the failure this card -exists to close, so the page states the status plainly instead of implying a runtime that -does not exist. For the same reason the component-schema framing is dropped from -`block-library` / `block-editor` / `block-instance`: none of them appears in `AnySchema`, -the runtime node union in `packages/types/src/index.ts`. - -Two further measurements are written into the page because they are what a confused reader -needs. The four `` demos it embeds are ordinary registered component trees -(`card`, `flex`, `stack`, `text`, `icon`, `button`, `badge`) — none carries `type: 'block'`, -`slots` or `slotContent`, so nothing on the page was ever exercising the block vocabulary. -And the slot system that *is* wired end to end is a different family entirely: slotted -record pages (`kind: "slotted"`, `page.slots`), consumed by `usePageAssignment`, -`PageBlockCanvas` and `PageBlockInspector`. The page now links there rather than leaving the -two `slots` spellings to be conflated. - -The `slot` entry in `DOC_TYPE_EXEMPTIONS` pointed at this card and is dropped, as the ruling -requires. With the phantom node gone the entry would itself fail as `stale-exemption`, so it -is deleted rather than re-pointed; the three terse family reasons are re-pointed at the -ruling's framing in the same pass. - -Option A (build renderers for the family) is rejected on zero pull and Option C (retire the -family) is deferred; neither is implemented here. diff --git a/.changeset/built-moment-transition-5799.md b/.changeset/built-moment-transition-5799.md deleted file mode 100644 index b429463265..0000000000 --- a/.changeset/built-moment-transition-5799.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio//interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app::build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio. diff --git a/.changeset/built-transition-published-5799.md b/.changeset/built-transition-published-5799.md deleted file mode 100644 index 69bd9c0cfa..0000000000 --- a/.changeset/built-transition-published-5799.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -The built-moment transition (#5799) now fires on auto-publish environments too: `detectBuiltAppPackage` reads the raw build envelope (`status:'drafted'` OR `'published'`, packageId + an `app` item), because an auto-publish posture rewrites apply_blueprint's envelope to `published` and the drafted-only `draftReview` lift never fired there — measured live on staging, where reopening a built conversation stayed on the full page. diff --git a/.changeset/button-container-exception-reason-6804.md b/.changeset/button-container-exception-reason-6804.md deleted file mode 100644 index 5dda128717..0000000000 --- a/.changeset/button-container-exception-reason-6804.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Makes the `button` entry in `scripts/container-declaration-baseline.json` a **permanent, -reasoned exception** instead of a provisional one, executing objectui#6804's maintainer -ruling (2026-08-30). objectui#6779 had excluded `button` from the ratchet-to-zero list -"pending a separate card"; that card has now been ruled, and the exclusion stands: `button` -does not declare `isContainer`. - -The entry's `reason` now carries the ruling's ground rather than a forward reference — -`isContainer` means layout containment, not "this tag renders children", and `button` reads -`schema.children` only as a fallback for `schema.label`, so declaring it would make one -predicate mean two things and would delete the `Button` identifier from the JSX scope of -every `kind:'react'` page, against zero measured pull the other way. Its owning `issue` -moves from `objectui#6779` (which deferred the question) to `objectui#6804` (which answered -it), with the provenance kept in the reason. - -This matters because an exception with no recorded ground is indistinguishable from a -missed one, and that indistinguishability is the mechanism behind this defect class's three -independent rediscoveries (objectui#3900 / objectui#6740 / objectui#6764). - -Also records a measurement the note previously implied away: `button` is the only public -tag among the 45 violations listed, but not the only public tag in the containment story — -ADR-0080's `PUBLIC_BLOCKS` carries `badge` and `alert` as bare keys too, so of the 14 tags -the ruling covers, three are published contract. The 11 bare `sidebar-*` keys are not (the -public sidebar is the namespaced `page:sidebar`, which already declares `isContainer`). - -Ledger and test prose only. No published behaviour changes, no registration's metadata is -altered, and every assertion that keeps the exception honest is unchanged. diff --git a/.changeset/calendar-explain-network-escape-5438.md b/.changeset/calendar-explain-network-escape-5438.md deleted file mode 100644 index 18359f993f..0000000000 --- a/.changeset/calendar-explain-network-escape-5438.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file changed is -`packages/plugin-calendar/src/object-calendar-renderer.propsContract.test.tsx`. - -Stops `object-calendar-renderer.propsContract.test.tsx` from making four real outbound -connections to `127.0.0.1:3000` while it stays green. - -`happy-dom`'s default document origin is `http://localhost:3000/`, so a relative-URL -`fetch` that nothing intercepts resolves against that origin and leaves the process for -real. The escaping request is `@object-ui/plugin-detail`'s record-level explain probe -(`useRecordEditable`, `POST /api/v1/security/explain`): the file's one DEFAULT-navigation -`onEventClick` case opens `ObjectCalendar`'s overlay drawer, which renders -`RecordDetailDrawer` → `DetailView`, which gates its Edit/Delete CTAs on a per-record -write verdict. With no `apiFetch` on the `SchemaRendererProvider` this file wraps every -case in, the hook's `apiFetch ?? fetch` fallback reaches the bare global `fetch` — and the -hook fails open on the resulting connection failure, so the escape was invisible to every -existing assertion. - -Same defect class as objectui#3339 (`plugin-detail`, closed by PR #4105), objectui#5225 -(`plugin-report`) and objectui#5280 (`plugin-dashboard` `DatasetWidget`) — a new consumer -of an already-diagnosed hook, not a new root cause. Fixed the way #4105 settled it: -`installExplainDouble()` answers the probe from a recording double instead of the -network, installed for every case in the file (not only the one that reaches it, since -the probe is `DetailView`'s own wiring and invisible from this file's schema authoring). -`visible: true` reproduces the pre-fix fail-open behaviour exactly, so no existing -assertion changes meaning. - -A new pinned test — the file's counter-probe — opens the DEFAULT-navigation drawer and -asserts the double is actually reached, twice (`update` and `delete`), with the expected -URL and request body. Confirmed load-bearing by reverse verification: with the double -temporarily disabled, the same case brings back all four `ECONNREFUSED` and fails that -new assertion (`explainCalls` stays empty instead of reaching length 2). diff --git a/.changeset/calendar-readme-schema-keys-5045.md b/.changeset/calendar-readme-schema-keys-5045.md deleted file mode 100644 index eaf0ed0831..0000000000 --- a/.changeset/calendar-readme-schema-keys-5045.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -`README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema` -that does not exist. Measured against the interface itself -(`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's -only required key besides `type` — was published as `events?`, so a reader -following the README omits it and TypeScript rejects the node; `defaultDate` was -`string` where the schema says `string | Date`; and `onDateClick` was listed as a -schema key when it is a `CalendarViewProps` **component** prop, sending readers -to a different package's surface for a key `calendar-view` does not have (the -schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys -with nothing saying it was a summary (objectui#5045). - -The block now carries the requiredness the schema declares, names itself a -partial summary of `CalendarViewSchema`, and adds the author-facing -`defaultView` / `view` / `views` / `editable` / `date`. It also states plainly -what the registered `calendar-view` renderer actually reads — it builds events -from the node's `data` array and drops an authored `events` key (objectui#4433) — -so the corrected requiredness does not itself become a new wrong instruction. - -This is a documentation fix to a file `plugin-calendar` publishes to npm, which -is why it carries a version: the npm landing page only picks up the correction -on a release. No behaviour, export, type, or `dist` byte changes. The pin test -added alongside it publishes nothing. diff --git a/.changeset/calendar-unscheduled-area.md b/.changeset/calendar-unscheduled-area.md deleted file mode 100644 index 71ab42df47..0000000000 --- a/.changeset/calendar-unscheduled-area.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch -'@object-ui/i18n': patch ---- - -fix(plugin-calendar): a record with no date is no longer placed on today - -`ObjectCalendar` mapped a record whose declared `startDateField` carried no -value to `new Date()` — the current moment — so it rendered on today's cell as -an ordinary event, indistinguishable from a real one. The `isNaN` guard six -lines below could not catch it by construction: a no-argument `new Date()` is -always valid, so the absent-value case became a well-formed lie *before* the -check that would have caught it. - -The fabricating arm is deleted. Such records now leave the grid entirely and -appear in a collapsed "Unscheduled (N)" area below the calendar — a visible -count and an expandable list, with no invented date and no scheduling UI. The -`isNaN` filter keeps its original job for values that are present but -unparseable: absent and malformed stay two distinguishable outcomes. -`allDay: !endDate` now applies only to records that have a start, so a record -with no dates at all is unscheduled rather than silently all-day; a record with -a start and no end still renders all-day exactly as before. - -Adds `calendar.unscheduled` to all ten locale packs. diff --git a/.changeset/calendar-view-event-rename-5044.md b/.changeset/calendar-view-event-rename-5044.md deleted file mode 100644 index 0ba8d30bf3..0000000000 --- a/.changeset/calendar-view-event-rename-5044.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -`@object-ui/plugin-calendar` now exports the `CalendarView` component's runtime event -type as **`CalendarViewEvent`**, and keeps `CalendarEvent` as a **`@deprecated` alias** -of it. **Non-breaking:** the alias is a working re-export denoting the same type, so -code importing `CalendarEvent` from this package keeps compiling unchanged — nothing is -removed and no behaviour changes. - -Why: `@object-ui/types` exports its own `CalendarEvent`, the AUTHORING event -(`id: string`, `start` / `end` accept ISO strings with `end` required, plus -`description`), while this package's was the runtime event (`id: string | number`, -`start: Date`, `end?: Date`). Neither is assignable to the other, and IDE auto-import -chose between the two identical names essentially at random — the wrong pick surfaced as -a remote `TS2322` about `Date` rather than as a wrong import, which is how this package's -own README example stayed uncompilable through an earlier import-path fix. The authoring -type keeps the canonical `CalendarEvent` name; the runtime type gets the self-describing -one (objectui#5044, following the `ObjectCalendarProps` -> `ObjectCalendarComponentProps` -rename in objectui#4650). - -Write `CalendarViewEvent` in new code. diff --git a/.changeset/calendar-view-mode-agenda-retired-5740.md b/.changeset/calendar-view-mode-agenda-retired-5740.md deleted file mode 100644 index f50f849b41..0000000000 --- a/.changeset/calendar-view-mode-agenda-retired-5740.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema` -(objectui#5740 — the value-level residue of objectui#5667's key-level -convergence of `CalendarViewSchema` on the registered `calendar-view` -renderer's measured read set; ADR-0049 enforce-or-remove). - -The union declared a value nothing enforced: the registered renderer's `view` -input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves -any off-enum value — `'agenda'` included — to `undefined` (the component's -`'month'` default), and `CalendarView` renders no agenda view. An author -writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with -no error or warning. No in-repo, example, or catalog app authors -`view: 'agenda'` (measured during objectui#5667's sweep and re-measured for -this change, including the objectstack tree). - -**This narrows the accept set — unlike #5667's key retirements, which created -no new rejections.** `view` is a declared key, and declared keys are validated -even under `.passthrough()`, so `view: 'agenda'` is now a **validation error -that previously parsed green** (an `invalid_value` issue on the `view` path, -offering `month`/`week`/`day`). Undeclared keys still pass through unchanged. -Breaking on the published zod surface; ships as `minor` per this repo's -version-alignment policy (majors track `@objectstack`). - -The runtime boundary is unchanged: an off-union `view` in raw metadata still -falls back to the component's `'month'` default at the renderer, and the -registry input already declared the three-value enum. Docblocks, the schema -reference table, and the zod `describe` no longer teach an `'agenda'` -fallback. diff --git a/.changeset/calendar-view-schema-converge.md b/.changeset/calendar-view-schema-converge.md deleted file mode 100644 index 61f9a3c1d7..0000000000 --- a/.changeset/calendar-view-schema-converge.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-calendar': patch ---- - -`CalendarViewSchema` (TS interface and zod mirror) converges on the registered -`calendar-view` renderer's measured read set (objectui#5667, maintainer ruling -option A — the renderer is authoritative). - -**Breaking for consumers of the published type** (deliberate; per-repo policy -breaking changes ship as `minor` — the fixed group's `major` tracks -`@objectstack`): - -- Nine inert keys are retired: `events` (the interface's only required key, - which the renderer deliberately drops — objectui#4433), `defaultView`, - `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, - `onEventUpdate`, `onDateChange`. None had a read site on the authored-node - path and no measured app authors them (ADR-0049 enforce-or-remove). -- The type now declares what the renderer actually reads: `data`, `titleField`, - `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, - `currentDate`, `allowCreate`, `className`, plus the two host-only function - hatches it forwards (`onEventClick`, `onViewChange`). -- Practical radius, measured: `BaseSchema` carries an index signature and the - zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys - neither fail to compile nor get rejected at validation — they are simply no - longer declared, documented, or type-checked. The material accept change is - that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` - node without it now validates (previously the one key validation demanded - was the one key guaranteed to do nothing). - -Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README -and `content/docs/api/schema-reference.md` are repaired to the converged -surface in the same change, so no copy of the old contradiction survives. diff --git a/.changeset/canvas-design-run-5800.md b/.changeset/canvas-design-run-5800.md deleted file mode 100644 index 18e7c49388..0000000000 --- a/.changeset/canvas-design-run-5800.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one. diff --git a/.changeset/catalog-layout-props-sweep-4891-4890.md b/.changeset/catalog-layout-props-sweep-4891-4890.md deleted file mode 100644 index 92f13f1872..0000000000 --- a/.changeset/catalog-layout-props-sweep-4891-4890.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`stack` now reads its spacing from `gap` and nothing else — the undeclared -`spacing` key it also accepted is gone (objectui#4890). - -`StackSchema extends Omit`, whose spacing key is `gap`. -`spacing` was declared by nothing: not the TypeScript interface, not the zod -mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway, -as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole -story, since it existed to get past the type system saying the key was not there. -A lenient consumer leg does not stay in the consumer: it becomes a second -de-facto contract that producers write to, and 135 nodes across 39 files of the -shipped schema catalog did exactly that. Every one of them rendered correctly, so -nothing ever pointed at it, while the examples went on teaching the key to every -author who copied them. - -The trap it was one edit away from springing: `flex` — semantically a `stack` -with a `direction` — never read `spacing`, so re-typing any of those nodes would -have dropped the spacing to the default silently. Fixed at the producer -(AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the -alias is deleted rather than legalised into `StackSchema`, where it would only -have been a second name for `gap`. - -**If you author `spacing` on a `stack`**, rename it to `gap`; the value and the -rendering are unchanged. A `stack` still carrying `spacing` now renders the -default gap, exactly as a `flex` always did. - -Also in the same sweep, and visible only in the published example catalog rather -than in any package API: 140 catalog nodes that were already `flex` / `stack` / -`container` stopped hand-writing their own declared props in `className` -(`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`, -`flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all -(objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in -`className`, because the props are not responsive. Both facts are ratcheted in -`examples/schema-catalog/test/layout-props-conversion.test.tsx`. diff --git a/.changeset/chart-height-chain-5451.md b/.changeset/chart-height-chain-5451.md deleted file mode 100644 index 3ab5836770..0000000000 --- a/.changeset/chart-height-chain-5451.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -`ObjectChart`'s wrapper div now carries `h-full`, keeping the height chain intact from a dashboard grid cell's declared height down to the element Recharts measures. Previously the chain died at the plain auto-height wrapper: `height: 100%` on the chart container computed to `auto`, Recharts measured a permanent zero, and only the `CHART_MIN_HEIGHT` floor (#5503) kept dashboard charts visible — at a fixed floor height instead of filling the cell (#5451). Under auto-height parents `h-full` resolves to `auto`, so non-dashboard hosts are unchanged. diff --git a/.changeset/chat-transport-memo-4187.md b/.changeset/chat-transport-memo-4187.md deleted file mode 100644 index 93978f70c6..0000000000 --- a/.changeset/chat-transport-memo-4187.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -`useObjectChat` no longer rebuilds its `DefaultChatTransport` on every render -(objectui#4187). - -The transport `useMemo` listed the caller's `body` and `headers` in its dep list. -Both are object props and every caller passes a fresh literal each render — the AI -page's chat pane builds its `body.context` inline — so the memo never hit and a -transport was constructed on every render of every chat surface, which during a -streaming turn is once per token batch. - -`body` and `headers` are now read through refs inside -`prepareSendMessagesRequest`, the idiom this hook already uses for the live model -(`modelRef`) and the handoff conversation id (`parentConvRef`), and they are gone -from the dep list. Unlike memoizing at each call site, a future caller cannot -undo it. - -No user-visible behaviour changes: `@ai-sdk/react` keeps the transport in a ref -and re-keys its `Chat` only on `chat`/`id` (verified against the installed -4.0.68), which `useObjectChat` passes neither of, so the message thread was never -at risk — the rebuild was pure waste. The one real difference is *when* the two -values are sampled: a send now reads them at send time, so it observes the values -of the most recent render instead of those of the last render that happened to -rebuild the transport. That is never staler than before, and it is pinned by -`useObjectChat.transportIdentity.test.tsx`. diff --git a/.changeset/check-schema-positive-marker-and-skip-count.md b/.changeset/check-schema-positive-marker-and-skip-count.md deleted file mode 100644 index 84f3ac9f6b..0000000000 --- a/.changeset/check-schema-positive-marker-and-skip-count.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/cli': minor ---- - -`objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge. - -A root `type` was treated as a component key wherever it appeared. `type` heads at -least seven unrelated JSON vocabularies, and the most common of them is -`package.json`'s `"type": "module"` — so the first line a user saw running -`objectui check` in their own project was a warning about their own package -manifest. Measured at this repository's root: 46 warnings, 45 of them -`package.json` (objectui#5127). - -A file now enters type judgement only when its root carries a structural key -declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`, -`style`, the `visible`/`hidden`/`disabled` predicate family, `testId`, -`ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an -`.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply -never judged. The key set is read out of the node contract rather than invented, -and it is closed: it grows only when `BaseSchema` grows. - -A list of filenames to exclude was the alternative and was rejected: it is a -second hand-maintained list of the shape objectui#5115 had just finished -deleting, and it can only ever enumerate the foreign vocabularies someone already -thought of. This is a positive marker instead. - -Because the marker narrows what is checked, the command now also reports the -count of files that had a root `type` and no marker, together with the marker -keys that opt one back in. That number is the coverage this gate gives up until -schema files are recognisable, and printing it is what keeps the loss visible -rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was -never type-judged, before this change or after it. Exit codes are untouched: a -JSON parse failure remains the only thing that fails the run. - -No public `$schema` URL is introduced. An earlier revision also admitted a file -whose root `$schema` had an `objectui.org` host; the maintainer ruled against -minting that identifier (2026-08-20, objectui#5127), so the structural key is the -only marker. Because the matching was host-based rather than literal, that arm -can be added later without invalidating a single file. diff --git a/.changeset/chilled-donkeys-shave.md b/.changeset/chilled-donkeys-shave.md deleted file mode 100644 index 745cbf50eb..0000000000 --- a/.changeset/chilled-donkeys-shave.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change to the console's public-block binding-reach probe: its `sections` fixture sample is now spec-valid (an array of section objects, not a bare string), its `formType` sample is a real form variant, and the crash guard runs ahead of the branch split so it covers every candidate. No published behaviour changes — the renderers are untouched. diff --git a/.changeset/cli-app-generator-explicit-branch-cases.md b/.changeset/cli-app-generator-explicit-branch-cases.md deleted file mode 100644 index c40bb23468..0000000000 --- a/.changeset/cli-app-generator-explicit-branch-cases.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change to `@object-ui/cli`'s app-generator suite: two added cases pin the -generator's IN-WORKSPACE and STANDALONE context branches explicitly, instead of -inheriting whichever one the ambient cwd happens to select (objectui#7807). No -published behaviour changes. diff --git a/.changeset/cli-validate-field-widget-namespace-5449.md b/.changeset/cli-validate-field-widget-namespace-5449.md deleted file mode 100644 index 2984a2ef41..0000000000 --- a/.changeset/cli-validate-field-widget-namespace-5449.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/cli': patch ---- - -`objectui validate` now refuses a form field whose widget id names a namespace -other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` -has given since objectui#5375 (objectui#5449). - -The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema -declared `type` and `widget` as bare optional strings — so a field typed -`ui:password` validated clean while the runtime validator rejected the same -document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an -author actually runs before shipping, so it was the one handing out the false -green: an author did exactly the diligence objectui#5375 asks for and still -shipped metadata that renders a secret into a plain text box. - -A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's -precedence (`widget` before `type`), the key it blames, its error code and its -message verbatim, so the two entry points cannot describe one defect two ways. - -**This rejects documents that previously validated.** Only colon-qualified -field widget ids outside the `field:` namespace are affected — `field:`-prefixed -ids and bare names such as `password` still pass, registered or not. A field -carrying, say, `type: 'ui:password'` must be rewritten as `password` or -`field:password`; it never rendered as a password box in any case. - -Which of the repo's authoring-time validators is canonical remains open -(objectui#4631) — this states the rule on the zod side rather than unifying -them. diff --git a/.changeset/closure-claims-bounded-or-derivable.md b/.changeset/closure-claims-bounded-or-derivable.md deleted file mode 100644 index ccf0743e1b..0000000000 --- a/.changeset/closure-claims-bounded-or-derivable.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Documentation and gate-only change (objectui#6186). The `thresholds` closure claim -carried by `@object-ui/sdui-parser`'s unconsumed-widget-option census was written -twice — in the census header and in the plugin-dashboard docs page — and is now -single-sourced onto the census header, with a new gate re-deriving it from source -on every test run. The block-schema page's whole-tree negative is bounded to the -block family's own keys. - -The only edit to published source is the census module's comment header: no -exported symbol, runtime behaviour or emitted diagnostic changes, so this releases -nothing. diff --git a/.changeset/cloud-connection-bind-failure-i18n-5054.md b/.changeset/cloud-connection-bind-failure-i18n-5054.md deleted file mode 100644 index 68a88aac2c..0000000000 --- a/.changeset/cloud-connection-bind-failure-i18n-5054.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -A cloud-connection bind failure now reads in the user's language whichever clock -noticed it (objectui#5054). - -One abandoned device approval could be noticed by either of two clocks, and the -Cloud Connection panel had a different answer for each. When the panel's own -`expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — -translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered -HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` -threw a bare `Error` carrying only that sentence, and the catch rendered it -verbatim. Same user, same failure, two languages, decided by which clock got -there first — visible on a zh console as the same abandoned approval reading -Chinese or English depending on whether the tab sat open past `expires_in`. - -`getJson` now carries the envelope's `declaredCode` and `code` across its throw, -and a single closed map turns the two RFC 8628 outcomes a user can actually cause -into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, -`access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten -locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream -spelling there — `code` is `DEVICE_CODE_FAILED` for both. - -Every other code is unchanged: `invalid_grant`, and anything upstream invents -next, still render the wire `message`, which stays the single source of truth for -failures this console has no copy for. No API, export or resolver was widened. diff --git a/.changeset/clusterradius-unit-doc-5020.md b/.changeset/clusterradius-unit-doc-5020.md deleted file mode 100644 index 00f5731ef8..0000000000 --- a/.changeset/clusterradius-unit-doc-5020.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-map': patch ---- - -`ObjectMapProps.clusterRadius`'s JSDoc said "in pixels"; `clusterMarkers` has -always used it as a coordinate-degree grid cell edge (`radius / 2 ** zoom`, -divided into the marker's `[lng, lat]` degrees), not a screen-space radius — -a host tuning clustering granularity by the documented unit would get a -completely different result than intended (objectui#5020). - -No behavior, default, or name changes: clustering, the >100-visible-marker -auto-threshold, and tap-through zoom are unaffected, and `clusterRadius` has -no call sites outside `plugin-map/src` today (re-confirmed repo-wide, -including `apps/`, `examples/`, and the `objectstack` spec/server repo — the -default of `50` is what runs everywhere). This is a doc-comment correction -only, bringing the JSDoc in line with the README's already-correct wording -(post objectui#5002). - -The latitude-anisotropy trade-off (a degree grid distorts east-west as -latitude rises) is a known design trade-off, not part of this fix. diff --git a/.changeset/component-docs-disabled-inherited-7239.md b/.changeset/component-docs-disabled-inherited-7239.md deleted file mode 100644 index a09347042f..0000000000 --- a/.changeset/component-docs-disabled-inherited-7239.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Docs-only: 13 `content/docs/components/**` pages spelled an inherited -`disabled?: boolean` in their illustrative interfaces, which went stale when -objectui#7087 removed the 18 narrowings and left the member inherited from -`BaseSchema` as `boolean | string`. The pages now spell the shipped union -(objectui#7239), and a new test-only pin -(`packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts`) -holds all 14 inherited rows to it while keeping the 8 independent item/option -rows at plain `boolean`. No published package behaviour changes. diff --git a/.changeset/component-meta-converge-and-deprecate-alias-5893.md b/.changeset/component-meta-converge-and-deprecate-alias-5893.md deleted file mode 100644 index 657412bdca..0000000000 --- a/.changeset/component-meta-converge-and-deprecate-alias-5893.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ComponentMeta` is now declared once and re-exported, and `PluginComponentMeta` is -deprecated in favour of `ComponentMeta` (objectui#5893). - -## The convergence - -`@object-ui/types` published `ComponentMeta` twice, from two different declarations: -`base.ts` and `plugin-scope.ts` (the latter published as `PluginComponentMeta`). They -were structural copies, not an alias pair. `plugin-scope.ts`' `ComponentMeta` is now -`export type { ComponentMeta } from './base.js'` — the disposition objectui#4580 ruled -for the identical shape, *a structural copy would reproduce the defect the moment either -side moved*, and the same move objectui#5671 made for the sibling type `ComponentInput` -in the same file. - -Either side had already moved. `base.ts` declared eleven keys; the plugin-scoped copy -declared nine — the same nine, **minus `tags` and `description`**. So a plugin author -typing against the plugin-facing declaration could not write two keys the main surface -advertises, and which the runtime validator already accepted: `ComponentMetaSchema` in -`zod/base.zod.ts` declares all eleven, so two of the three authorities agreed and the -plugin-facing one did not. `resizeConstraints`' six members were identical in both, so -the delta was exactly those two keys. - -What changes for a consumer: `tags` and `description` become writable on the -plugin-facing type. Nothing narrows — no key is removed and no key's type changes, so no -existing registration stops compiling. The convergence buys **acceptance** of two keys; -it buys no rejection of anything. `ComponentMetaSchema` is a plain `z.object` with no -`.strict()`, so it strips unknown keys rather than refusing them, and that is unchanged -here. - -## The alias deprecation, sequenced after it - -`PluginComponentMeta` — the published alias for the plugin-scoped declaration — is now -`@deprecated` in favour of `ComponentMeta`. **`PluginComponentMeta` is the name to search -for** if you import it; replace it with `ComponentMeta` from the same entry point. - -This is stage 1 of objectui#5674's two-stage retirement (maintainer ruling, 2026-08-22: -deprecate for a release, then remove). Nothing is removed here — the export still exists -and still names the same type. - -The ordering is deliberate and is why the two halves ship together. Until the convergence -above, the alias named a genuinely different nine-key interface; deprecating it then -would have warned consumers about a name that was still about to change meaning. It is -deprecated now, at its final meaning. - -**Why a deprecation window rather than a deletion.** The measurement that licenses -deleting an export from a published package is *"no importer"*, and what can be measured -from inside this repository is only *"no importer here"*. In-repo, `PluginComponentMeta` -has exactly one occurrence — its own export line — searched across every root -(`packages/`, `apps/`, `content/`, `docs/`, `skills/`, `examples/`, `e2e/`, `scripts/`, -`eslint-rules/`, `public/`, `.changeset/` and the root docs) plus the sibling -`objectstack` framework checkout, with controls searched identically so a broken search -could not read as a clean one. What no search here can see is a consumer on npm. **That -external caveat is unchanged from objectui#5674 and is not being dropped:** the window -converts a silent break into a warned one before stage 2 lands. Stage 2 removes the alias -and the now-dead re-export in `plugin-scope.ts` that exists only to feed it, and ships as -a `minor` under this repo's policy that its own breaking changes never declare `major`. - -## Pinned by identity, not by member set - -A new test asserts that `plugin-scope.ts` re-exports the declaration and declares no -`ComponentMeta` of its own. A member-set assertion cannot do this job: TypeScript is -structurally typed, so a local re-declaration carrying the same eleven keys is mutually -assignable with the imported one and passes every type-level check. A member-identical -structural copy is exactly what objectui#4580 predicted would drift and exactly the state -this card recorded — this copy started identical and acquired its two-key delta later. -The member-set checks are kept alongside the identity pin, labelled as the control that -shows what it cannot see. diff --git a/.changeset/componentinput-reexport-4972.md b/.changeset/componentinput-reexport-4972.md deleted file mode 100644 index 910e0a7348..0000000000 --- a/.changeset/componentinput-reexport-4972.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/types': minor ---- - -`ComponentInput` is now declared once and re-exported, instead of restated in three -places (objectui#4972). - -`@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' -plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) -were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now -re-exports of that one declaration, which is the disposition objectui#4580 ruled for the -identical shape — *a structural copy would reproduce the defect the moment either side -moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. - -Either side had already moved. `base.ts` declared thirteen keys; both copies declared -nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every -component registration actually imports**. Those four keys were unwritable at any real -registration — a plain TypeScript error at the call site — while `ComponentInputSchema` -(the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face -advertised four keys the authoring face rejected. Measured over the repository, no -registration had tried to write one yet, so nothing a user hits was broken today; what -changes is that the four keys become writable, and there is no longer a second -declaration for the next widening to miss. - -`ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single -declaration imported by all three sites (objectui#3832); this converges the rest of the -interface. - -Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is -byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported -here only with its control — a probe that added a *required* key to `ComponentInput` left -the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of -`export *` lines that names `ComponentInput` zero times. The gauge that can actually fail -is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does -`@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted -declarations that change in either package. - -`WidgetInput`'s union-arm capability is deliberately untouched — a different gate path -and a separate judgment. diff --git a/.changeset/componentrendererprops-reexport-4594.md b/.changeset/componentrendererprops-reexport-4594.md deleted file mode 100644 index 52cad5745a..0000000000 --- a/.changeset/componentrendererprops-reexport-4594.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`ComponentRendererProps` is now declared once and re-exported, instead of -hand-declared a second time in `@object-ui/core` (objectui#4594). - -`@object-ui/core`'s `ComponentRendererProps` (`src/types/index.ts`) was a -non-generic interface typing `schema` as `SchemaNode`, while -`@object-ui/types`' declaration of the same name is generic — -`ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` with -`schema: TSchema`. Same name, both exported from their package entry, from two -packages the same consumers import together: which declaration a call site got -depended on which package it reached for, and the two disagree about whether a -primitive node is admissible. Core's is now a re-export of types', which is the -disposition objectui#4580 ruled for `SchemaNode` two lines above it in the same -file, and objectui#4972 for `ComponentInput` — *a structural copy would -reproduce the defect the moment either side moved*. - -**Published-surface effect, and the reason it is not neutral.** Resolved -through the TypeScript checker from `core/dist/index.d.ts` over a clean rebuild -of both legs, `ComponentRendererProps` as reached through `@object-ui/core` -moves from non-generic with -`schema: BaseSchema | string | number | boolean | null | undefined` to -`ComponentRendererProps` with `schema: TSchema`, defaulting to -`BaseSchema`. `schema` therefore **narrows** back to the object form — core's -copy had silently widened when objectui#4608 made core's `SchemaNode` a -re-export of types' union — and the type gains a parameter. **Nothing imported -it**, on either side, re-verified repo-wide on the merged ref, so no call site -can observe either move; the narrowing is recorded here because it is a change -to a published type, not because a consumer is affected. - -A compile-time pin now holds the reconciliation from -`@object-ui/react` — the only position that resolves both packages through -`node_modules` — alongside the existing `SchemaNode` one. It is a test-only -addition and emits nothing, so `@object-ui/react` takes no bump of its own. diff --git a/.changeset/components-react-page-published-dts-text-5666.md b/.changeset/components-react-page-published-dts-text-5666.md deleted file mode 100644 index 6b021a41f0..0000000000 --- a/.changeset/components-react-page-published-dts-text-5666.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/components': patch ---- - -Published declaration text for the react page renderer changed: `kind:'react'` page `source` styling is now stated in the shipped `.d.ts` - -`dist/renderers/layout/react-page.d.ts` is in this package's npm tarball, and the -declaration emitter reproduces that module's file-header documentation into it -verbatim. Two things a consumer reads — on hover over `ReactKindPage` in an editor, -or straight out of the tarball — now say something different: - -- The injected-scope note no longer tells authors that layout is left to "plain - HTML + Tailwind". It says plain HTML. -- A new paragraph states the styling contract for `kind:'react'` page `source`: - `source` is runtime metadata, not build input. Style with inline `style` objects - using `hsl(var(--token))` theme colors, and render overlays through `ObjectForm` - with `formType` `"drawer"` or `"modal"` rather than a hand-rolled `fixed inset-0` - backdrop. Do **not** author Tailwind utility classes in page `source`: the - console's Tailwind is compiled at build time by scanning the console's own `src` - and there is no safelist, so an authored utility class silently produces no CSS - and no error anywhere. `os validate` reports it as - `page-source-className-tailwind`. (ADR-0065; ADR-0080's 2026-06-30 amendment; - see `content/docs/guide/react-pages.md`.) - -That is guidance an author can act on, and it contradicts what this package's -declarations previously told them, so it is a change to the published surface -rather than an internal edit. No runtime behaviour changed and no export moved. - -This entry is corrective. The text landed under objectui#5461 with a changeset that -declared only `@object-ui/types`, on the stated grounds that the edits "do not -project into any `.d.ts`". Rebuilding the package shows that they do — the reasoning -and the measurement are recorded in `scripts/check-changeset-presence.mjs`'s header, -where the next author will meet them. diff --git a/.changeset/confirm-card-terminal-5695.md b/.changeset/confirm-card-terminal-5695.md deleted file mode 100644 index 7d3d67a83e..0000000000 --- a/.changeset/confirm-card-terminal-5695.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/plugin-chatbot': minor -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. diff --git a/.changeset/confirm-handler-published-type-5835.md b/.changeset/confirm-handler-published-type-5835.md deleted file mode 100644 index d141b7648e..0000000000 --- a/.changeset/confirm-handler-published-type-5835.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Type `RecordDetailView`'s confirm handler as the published `ConfirmationHandler` instead of an inline re-spelling of the same shape, and pin that both of `app-shell`'s confirm runtimes hand `ActionConfirmDialog` the same field set. Releases nothing: the annotation is erased at build time (the emitted JS for `RecordDetailView.tsx` is byte-identical before and after), the handler is not exported, and no published surface changes. diff --git a/.changeset/confirm-runtime-close-parity-6034.md b/.changeset/confirm-runtime-close-parity-6034.md deleted file mode 100644 index 94766b7b4a..0000000000 --- a/.changeset/confirm-runtime-close-parity-6034.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Console action runtime: closing an action confirm dialog now keeps the dialog's -text instead of blanking it mid-fade. - -`useConsoleActionRuntime` reset its confirm state by replacing the whole object -(`{ open: false, message: '' }`), which cleared `message` and dropped `options`. -Radix keeps `AlertDialogContent` mounted through its exit animation, so the -dialog's description went blank and its title and button labels reverted to -their defaults while it was still fading out. It now flips only `open` and keeps -every field, matching `RecordDetailView`'s second confirm runtime, which already -closed this way. Both runtimes feed one `ActionConfirmDialog`; the parity pin now -covers the close path as well as the open path. diff --git a/.changeset/console-action-dispatch-envelope-5611.md b/.changeset/console-action-dispatch-envelope-5611.md deleted file mode 100644 index a7e8555f25..0000000000 --- a/.changeset/console-action-dispatch-envelope-5611.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Internal: the console's admin-override notice now travels on a declared dispatch -type instead of a cast (objectui#5611). No published API and no runtime -behaviour changes, so this changeset declares "no release" rather than a bump — -`@object-ui/app-shell` source moved, nothing it exports did. - -`overrideNotice` is the safety copy shown once, ahead of a privileged admin -override that finalises an approval step over approvers who have not acted. Its -producer (`DeclaredActionsBar`) reached its reader (`useConsoleActionRuntime`'s -param-collection dialog) through a `dispatch as ActionDef` cast on one side and -`action?: any` on the other, so nothing declared the key anywhere and the two -could drift apart in silence — rename it on either side and the notice stops -appearing with every test still green, because each side's suite spells the key -itself. - -Both ends now share one declaration: `ConsoleActionDispatch` -(`ActionDef & { overrideNotice?: string }`), a HOST-composed envelope that lives -at the seam, in the one package where producer and reader both live. The cast is -gone and both param-collection handlers narrow off `any` — which is what puts -those functions under the compiler at all. - -The published `ActionDef` deliberately does NOT declare the key (maintainer -ruling 2026-08-22): it is the authored-metadata mirror, and `overrideNotice` is -the first key no author supplies, so declaring it there would make an unenforced -key legally writable in metadata. That prohibition still holds exactly as -written — `ActionDef` and `ACTION_DEF_KEYS` are unchanged. - -`@object-ui/core` is NOT untouched, and the reason is a separate declaration: -the same ruling's item 4 adds `HOST_DISPATCH_ACTION_KEYS` to the key inventory -so the dev-mode warning stops calling the host-composed key unknown. That change -carries its own changeset (`host-dispatch-action-keys-5611.md`) and its own -patch bump; this one remains the app-shell half, which publishes nothing. diff --git a/.changeset/console-boot-request-dedup-5544.md b/.changeset/console-boot-request-dedup-5544.md deleted file mode 100644 index 44270d6e83..0000000000 --- a/.changeset/console-boot-request-dedup-5544.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The console's cold load no longer asks `/api/v1/runtime/config` or -`/auth/me/localization` twice (objectui#5544). - -Two pairs of boot callers were racing each other for the same URL, with no shared -provider between them, so no guard inside either component could see the other: - -- `GET /api/v1/runtime/config` — the pre-React branding script inlined in - `apps/console/index.html` (it runs during HTML parse so the tab title and - favicon are the operator's before the bundle is fetched) and - `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the - expensive one: the console `await`s `initRuntimeConfig()` before - `createRoot().render()`, so the duplicate sat on the critical path to first - paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed - boot concurrency further past the server's pool knee. -- `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true - first visit and `LocalizationFetchProvider` on every boot. The seed keeps - running past its 500 ms race by design and the provider mounts the moment that - race resolves, so on a first visit the two overlap. Measured ×2 on staging. - -`@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET -while one is already in flight join that request instead of starting another. It -shares the in-flight promise and nothing else — the entry is deleted the instant -the request settles, so there is no cache, no TTL and no stale window, and a -caller arriving after settle fetches fresh exactly as before. Rejections fan out -to every sharer with the status intact (`LocalizationFetchProvider`'s retry -policy still sees its own 503), each caller receives its own copy of the parsed -body, and only GETs are eligible — a non-GET is refused rather than quietly -rewritten. - -Requests that differ in credentials mode or headers keep separate identities, so -the console's two deliberate `auth/get-session` calls — one Bearer-only with the -cookie omitted to detect a stale token, then one through the cookie — stay two -requests. Collapsing those would have destroyed the signal the first one exists -to read. - -No component receives anything different: same payloads, same errors, one fewer -round trip. diff --git a/.changeset/console-boot-splash.md b/.changeset/console-boot-splash.md deleted file mode 100644 index 7849076e82..0000000000 --- a/.changeset/console-boot-splash.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/console': patch ---- - -fix(console): paint a boot indicator during the pre-React white frame - -The console's `index.html` shipped an empty `
`, so a hard -navigation showed a pure-white page until the module graph had downloaded, -evaluated and resolved the two round trips `main.tsx` awaits before -`createRoot().render()`. Measured on this repository's production build with -Playwright + CDP screencast (frames classified pixel-by-pixel, corroborated by -the Paint Timing API): 1224-2016 ms of pure white on an unthrottled localhost, -2289-2297 ms at 20 Mbps/80 ms RTT, and 5808-6167 ms on a Fast-4G profile. - -The document now carries the indicator itself — an inline style block and the -same gradient tile `LoadingScreen` opens with — so it paints from the HTML -parser, before the first chunk is requested. It is removed on React's first -commit into `#root`, so it never doubles up with `LoadingScreen`, which -continues to own the init screen and its ten-language copy. diff --git a/.changeset/console-form-container-specs-one-declaration-5596.md b/.changeset/console-form-container-specs-one-declaration-5596.md deleted file mode 100644 index 6f199b8c8d..0000000000 --- a/.changeset/console-form-container-specs-one-declaration-5596.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The two form CONTAINER contracts now have ONE declaration each, derived from -`@objectstack/spec`, and the console reads them instead of its own copies. - -objectui#5542 converged the LEAF of this contract — the field spec — and left the -two containers above it untouched, because converging them was a bigger call than a -mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared -twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once -in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean -subset — these two had **already drifted, in both directions**, so neither copy was a -subset of the other and there were two live answers to "what may an author write": - -- `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; - the console declared none of them. The console's `columns` admitted the string arm - (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. -- `FormViewSpec` — the console declared `label` / `groups` / `sharing` / - `submitBehavior`; app-shell stopped at `type` plus `sections`. - -The drift is decided by asking the **contract**, not by picking a side. `columns` -does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` -with the four numeric literals, folded to a number by its own transform), so -app-shell's numbers-only declaration was rejecting metadata the platform accepts — -objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is -the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured -against the installed `@objectstack/spec` 17.0.0), because a form config is titled, -not labelled. The value that read actually finds is the VIEW's identity label, which -arrives on the `ExpandedViewItem` envelope or beside the config on a flattened -runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to -the body it unwraps, rather than smuggled onto the form contract. - -Both types are therefore **derived from the spec's own `FormSection` / `FormView` -with named narrowings** — the repo's sanctioned form for a spec-shaped local type -(`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the -two layers agree on comes from the spec and cannot fall behind it; the four positions -where this layer is deliberately narrower are each named in an `Omit` list and -restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving -it would silently re-open #5542), and `label` / `description` / `visibleWhen` / -`visibleOn` keep the shapes this repo's renderers and evaluators actually consume -rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s -`submitBehavior` union — previously hand-written under the comment "Mirrors the spec -FormView.submitBehavior union" — is now read back off the shared type, making the -mirror structural. `@object-ui/app-shell` re-exports both names from its package root -(type-only, erased at build — nothing is added to the bundle), because a type that -cannot be imported is a type that gets retyped. - -The pins are what make future drift loud, and each half is pinned on both sides. -`form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the -non-narrowed half of each type against the spec's own symbol, so re-hand-writing -either declaration fails `type-check` the day the spec moves rather than years later -when someone reads two files side by side — and the console's pins read both types -back out of the **exported** `buildSections` signature rather than naming them, so a -re-inlined local copy fails even if it agrees on every key on the day it is written. -Their liveness controls are what stop them being phantom checks: the removed copies -are pinned NOT equal to the shared types (proving the `Equal` helper still -discriminates), the renderer's honoured `RenderableSection` is pinned not equal -either (so the authored-document and honoured-row types cannot be collapsed again), -and an undeclared key is still rejected (so the derivation smuggled in no index -signature or `any`). Every narrowing carries a matching negative pin, so "derived" -cannot quietly become "widened to whatever the spec says". - -Behaviour is unchanged — the runtime always accepted these keys. The vitest halves -prove it: a section spelling its column count as the string `'3'` lays out identically -to the numeric `3` on both sides, and a section carrying the keys only one side used -to declare builds the same rows. diff --git a/.changeset/console-form-field-spec-one-declaration-5542.md b/.changeset/console-form-field-spec-one-declaration-5542.md deleted file mode 100644 index 59b1a9fdc5..0000000000 --- a/.changeset/console-form-field-spec-one-declaration-5542.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The form-field authoring contract now has ONE declaration, and the console reads it -instead of its own copy. - -objectui#5040 was not a missing key. It was that **two hand-written descriptions of -one contract drifted**, and nothing could notice, because each was only ever checked -against itself. PR #5537 converged the two app-shell descriptions into -`views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: -`FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same -name, in a different package — so the same failure mode stayed fully available. - -Measured key by key before choosing a route, because the two honest outcomes are -"same contract, import it" and "genuinely narrower layer, rename it and pin the -subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, -every one identical in type, none console-only — and it sat in a position that -describes an **authored document**: `FormSectionSpec.fields`, read straight off the -`/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both -files even spell the same six-member `type` union and call the element type -`FormFieldSpec`). The narrow, renderer-honoured shape is a different type that -already exists in that file, `RenderableField`. So this was one contract described -twice, and the console's description was wrong about the document: legal metadata — -`visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, -and ten more keys — was undeclared there. That is #5040's own symptom, "the type -rejects the configuration the runtime accepts", which no runtime test can see. - -`@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root -(type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` -imports it and deletes the local declaration. Reachability is the load-bearing half: -a type that cannot be imported is a type that gets retyped, and retyped copies drift. -`form-spec.ts` itself is untouched. - -`FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the -field-spec type back out of the **exported** `buildSections` signature rather than -naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even -if the copy agrees on every key on the day it is written — which is exactly what did -not happen to the copy this change removes. Its liveness controls are what stop it -being a phantom check: the removed nine-key shape is pinned NOT equal to the shared -type (so the `Equal` helper is proven to still discriminate), `RenderableField` is -pinned not equal to it either (so the honoured-row and authored-document types cannot -be collapsed again), and an undeclared key is still rejected (so the import did not -smuggle in an index signature). Behaviour is unchanged: the runtime always accepted -these keys, and the vitest half proves the same rows are built. diff --git a/.changeset/console-form-marked-refusal-5210.md b/.changeset/console-form-marked-refusal-5210.md deleted file mode 100644 index 3c95052807..0000000000 --- a/.changeset/console-form-marked-refusal-5210.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/components': patch ---- - -The console form now renders a refusal message the producer explicitly marked -as user-facing, instead of always substituting a generic string -(objectui#5210). - -An application's hook guards could not talk to their users. When a hook refused -a write with 403, the form replaced the server's text with -`form.noPermissionToSave` unconditionally — the recorded objectstack#3821 fix, -which exists because a raw refusal body puts untranslated platform diagnostics -(`FORBIDDEN: insufficient privileges to update showcase_private_note -pi-TgoJ4_DM55Fqz`) in front of end users. The external report behind this change -had 11 guards whose deliberate, localized guidance — which role owns the action, -whom to ask — never reached anyone, and named the incentive that creates: -returning 400 instead of 403 for permission failures, degrading the status -semantics logs, monitoring and API consumers depend on. - -The maintainer ruling (2026-08-19) was a producer-side opt-in rather than a -chattier 403 branch, and the platform half shipped as objectstack#9934: a hook -marks its refusal text with `userMessage` at throw time. This is the consumer -half. - -- `@object-ui/react` gains `declaredUserMessage(err)` — the one "is this - marked?" read. It answers the marking verbatim, from the two places the - adapter boundary parks the envelope (the error itself, where - `@objectstack/client` lifts it, and `details`), and `null` for everything - else. -- The form prefers a marked message over both its generic strings, on ANY - status — the marking is status-agnostic; 403 is where this was reported, not - a fence the contract draws. - -**Unmarked refusals are unchanged**: a 403 with no marking still shows the -generic `form.noPermissionToSave`, and the raw text still goes to the browser -console only. objectstack#3821's protection is preserved by construction, not -by re-guessing which 403 bodies are presentable — the mark and the marked text -are one field, so no boundary that rewraps or substitutes `message` can promote -platform prose into the user-facing channel, and platform code never sets it. diff --git a/.changeset/console-formpage-runtime-default-seed-5727.md b/.changeset/console-formpage-runtime-default-seed-5727.md deleted file mode 100644 index d69e013e00..0000000000 --- a/.changeset/console-formpage-runtime-default-seed-5727.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's form routes no longer seed a RUNTIME `defaultValue` into the -control, so the server-side resolution the declaration asks for actually happens -(objectui#5727). - -`readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared -default unconditionally. A `defaultValue` may be a literal, or an *instruction* -the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`, -`current_user`) or a CEL Expression envelope. Seeding one of those literally put -the text `NOW()` into a datetime input on both `/forms/:name` and the public -`/f/:slug` route, and submitting it sent that string as the field's value — -which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved -the declared default and the column stored the token text instead of a timestamp. - -The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same -published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its -seeding with, and the one this renderer already reads once removed (through -`isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default -leaves the key ABSENT rather than empty, because absent is precisely the case the -engine resolves. - -Nothing else about the prefill precedence moves: a literal default still seeds, a -stored record value still wins over a default, and an explicit `prefill_=` -param still wins over both — including for a field whose default is a runtime -token, since a value a producer supplies is not a declaration awaiting -resolution. diff --git a/.changeset/console-formpage-visible-predicates-5594.md b/.changeset/console-formpage-visible-predicates-5594.md deleted file mode 100644 index 60c95e1a1c..0000000000 --- a/.changeset/console-formpage-visible-predicates-5594.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's standalone form renderer now evaluates conditional field visibility. - -`apps/console/src/components/FormPage.tsx` is a **second, independent form renderer** -— its own `buildSections`, its own JSX — and it serves both the public -`/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of -the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` / -`visibleOn` *read* inside that file returned zero. So a field an author conditioned on -`record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec` -normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both -authors and honours — rendered unconditionally on both routes. Fail-open and silent: -the author saw the field always, with no diagnostic. - -objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different -chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form` -`sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx` -is on that chain at no point, and #2212's regression pin lives with the chain it fixed, -so nothing in the suite could see this copy. One contract, two implementations, each -only ever checked against itself. - -The wiring is **#2212's ruling applied verbatim** rather than a second predicate -semantics invented for this renderer, because two form renderers disagreeing about what -`visibleWhen` *means* would be a worse defect than one renderer ignoring it. The -predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`, -`evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and -`{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*` -= the stored record an edit form started from), and the fail-open-but-loud behaviour on -an unevaluable predicate are the shared ones by construction. Resolution is -canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers: -`sectionFields.ts` and app-shell's `readVisibility`. - -Two things deliberately did **not** change. A field hidden by its predicate still -submits its value — conditional visibility is a rendering rule in both renderers, and -making it a submit-payload rule would be a new contract decided once for both, not -invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain: -the second-renderer question is real, but it belongs with the #5596 convergence track, -not with a predicate that is dead today. - -`FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this* -renderer on purpose — a pin that cannot see the second copy is how the first gap -survived. With the fix reverted and the pin in place the suite reports -`11 failed | 1 passed (12)`; the one green is the control that has to be green (a field -with no predicate still renders), without which every "the field is absent" assertion -would be equally satisfied by a renderer that draws nothing at all. diff --git a/.changeset/console-lazy-docs-portal-5467.md b/.changeset/console-lazy-docs-portal-5467.md deleted file mode 100644 index 9b44325e88..0000000000 --- a/.changeset/console-lazy-docs-portal-5467.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to. - -`AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the -app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same -three statically for the platform portal at `/docs` (ADR-0046 section 6), so all -of them sat in the eager graph regardless and the `import()` moved nothing — -three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build` -(objectui#5467). A static import on either side silently defeats the split for -both, and the only signal is a build warning that fails nothing. - -`App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`, -matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even -though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot, -so nothing imported `DocsIndex` dynamically, but left static it alone would keep -`DocShell`, `use-book-data` and `book-nav` eager and the portal would only -half-leave the closure. - -Measured on this branch with the `dist/eager-closure.json` gauge added by -objectui#5324, both builds exiting 0: - -| | before | after | -|---|---|---| -| `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 | -| eager closure, gzipped | 3,881,609 B | 3,870,058 B | -| eager chunks | 58 | 52 | - -Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz), -`CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B), -`componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown -folds into the entry chunk rather than dropping — which is why the entry chunk -grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by -11,551 B. The entry stays far under that budget's 350 KB line, and the eager -closure is the number a page load actually pays. - -What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this -looked like a bigger win than it is. Three eager chunks import it statically, -and only one of them was this portal: `plugin-chatbot` reaches it directly, and -`packages/fields`' `MarkdownContent` — lazy in source — is folded into the -eagerly imported `ui-components` chunk by the `advancedChunks` group that claims -every `packages/fields` module. That is objectui#5325's mechanism, not this -card's, and it is why the saving here is 0.30% rather than 4%. diff --git a/.changeset/console-preboot-branding-origin-5660.md b/.changeset/console-preboot-branding-origin-5660.md deleted file mode 100644 index d642b3dd34..0000000000 --- a/.changeset/console-preboot-branding-origin-5660.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's pre-boot branding script now resolves its server origin from -`VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of -`window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set -(objectui#5660). - -The two callers of `GET /api/v1/runtime/config` share one in-flight request, and -that sharing keys on the FULL URL. So the origin was half of the contract: the -inline script in `index.html` read one spelling and `src/main.tsx` read another, and -whenever they disagreed the two never found each other. A same-origin production -build hid it completely — both spellings collapse to `''` — so the split surfaced -only in a dev pointing the console at a separate server, where it cost two requests -to two different servers and let pre-boot branding paint from the wrong one. - -The pre-boot fetch is kept, not deleted: it is the request the module side JOINS. -`sharedGetJson()` can only hand an earlier request to a later caller, and this -script is the earlier one by construction — it runs during HTML parse, before the -bundle is fetched, while `initRuntimeConfig()` is awaited before -`createRoot().render()`. Deleting it would not remove a request; it would move the -single remaining one later, onto the critical path to first paint, and leave the -page's empty `` and favicon unbranded until React mounts. - -Vite substitutes its HTML env token only when the variable is set and leaves it -verbatim otherwise, so the unset case is read as same-origin `''` rather than -allowed to reach the URL as a path segment. diff --git a/.changeset/console-studio-builder-decorative-lazy-5486.md b/.changeset/console-studio-builder-decorative-lazy-5486.md deleted file mode 100644 index e557f79f0b..0000000000 --- a/.changeset/console-studio-builder-decorative-lazy-5486.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing. - -The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a -`Suspense` fallback — naming the same barrel the line above it imports -statically for `registerAppComponent`, and the same barrel `App.tsx` pulls -`BuilderLanding` from to render the standalone `/studio` landing full-screen. -Either reason alone makes the `import()` unable to move a module into another -chunk (objectui#5486). - -**This moves no modules and is not a bundle improvement.** `BuilderLanding` was -already in the eager graph via `App.tsx` and still is. Measured on -`dist/eager-closure.json`, both builds exiting 0: the eager closure holds the -same 52 chunks with the same names, and the only difference is 130 B gzipped -(413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback -text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure, -because nothing could. - -What it does fix is honesty. The old code told every reader the builder was -deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every -console build — the console's count of those drops from 44 to 43, with the 43 -remaining ones all belonging to the `packages/fields` barrel (objectui#5325). -A permanent warning that fails nothing is how a team learns to skim past build -warnings, and a decorative `lazy()` is how the next reader learns something -false about the chunk graph. - -The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx` -lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in -statically, and is genuinely deferred; it is untouched. Making the *builder* -genuinely lazy would mean taking `App.tsx` off the static import too, changing -how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind -the barrel — a separate measured card, not folded in here. diff --git a/.changeset/console-user-preference-request-budget.md b/.changeset/console-user-preference-request-budget.md deleted file mode 100644 index a629873f79..0000000000 --- a/.changeset/console-user-preference-request-budget.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Tests only: pin the console `sys_user_preference` request budget across one -mount (objectui#5544). Three distinct preference keys, one read each, and every -consumer still receives its own row. No runtime code changes, nothing to release. diff --git a/.changeset/container-declaration-ratchet-6779.md b/.changeset/container-declaration-ratchet-6779.md deleted file mode 100644 index b9c378806f..0000000000 --- a/.changeset/container-declaration-ratchet-6779.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: a universal runtime census pin that fails when any registration -renders an authored `children` list while its registry meta omits `isContainer`, -plus `scripts/container-declaration-baseline.json` — a ratchet-to-zero list of the -44 existing violations (objectui#6779). No published behaviour changes; no -registration's metadata is altered by this change. diff --git a/.changeset/core-datascope-field-and-comparison-guards-7751.md b/.changeset/core-datascope-field-and-comparison-guards-7751.md deleted file mode 100644 index 482715ad71..0000000000 --- a/.changeset/core-datascope-field-and-comparison-guards-7751.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`DataScopeManager` now **denies** a row when a scope rule names something that is not the record's own data, and when an ordered comparison would only succeed by coercing one of its two sides. It used to **admit** those rows. - -Two fail-opens on a row-level permission boundary, both measured against the previous release's source, both the same silent direction as objectui#7378 — a result set that is too large, with no error and no console line, which looks exactly like a correctly configured permissive scope. - -**The field a rule names is now read as an own member of the record, or not read at all.** `{ field: 'constructor', operator: 'ne', value: anything }` returned the ENTIRE dataset: the name resolved on the prototype chain, `Function !== 'x'` is true, and every row passed the rule that existed to hide it. The read now has three cases instead of one. A name in the refused list (`__proto__`, `constructor`, `prototype`) denies. An own member is read as before. A name that is not an own member but still resolves on the record's prototype chain — `toString`, `valueOf`, `hasOwnProperty`, or a field inherited from an `Object.create` parent — denies, because the value exists but is not this record's data. A name that resolves nowhere is a genuinely absent field and still reads as `undefined`, so the ordinary "this row has no `status`" rules keep every verdict they have always had. - -That third case is deliberately stricter than `evaluateCondition` in `@object-ui/permissions`, which this card was filed to converge with. Reading with `hasOwnProperty` alone — the sibling's shape — collapses "inherited" into "absent", and absent ADMITS on a negative operator, so the sibling still returns `true` for `{ field: 'toString', operator: 'neq' }` on every record (filed as objectui#8044). Converging on the sibling's exact lines would have closed three spellings and left the class open, and would itself have widened one case: an inherited field value flips from denied to admitted under `ne`. Distinguishing inherited from absent closes the class and keeps the change a narrowing everywhere. - -**Ordered comparisons (`gt` / `gte` / `lt` / `lte`) now require both sides to be the same comparable kind.** `{ field: 'age', operator: 'gte', value: 0 }` admitted records whose `age` was `null`, `'10'`, `true`, `false`, `''` or `[]` — every one of them through a coercion to a number that the rule's author never wrote. Both sides must now be numbers, or both strings, or both `Date`s. - -Same KIND, not "both numbers". The sibling requires `typeof === 'number'` on both sides; copying that predicate would have denied every row for `{ field: 'created', operator: 'gte', value: '2023-01-01' }`, since ISO date strings, plain string ranges and `Date` objects all order correctly on this evaluator today and none of those comparisons coerces anything. The hazard is cross-kind comparison, so cross-kind is what is refused. - -**`contains` now requires its rule value to be a string** rather than calling `String()` on it, so `{ operator: 'contains', value: 1 }` no longer matches the record value `'10'`. Same unwritten coercion as the ordered arms; the sibling already refused it. - -**The narrowing, named plainly for anyone upgrading with rules already stored.** A legitimate rule loses rows in exactly three shapes. A numeric rule (`age gte 18`) over a dataset where numbers arrive as strings — from JSON, a CSV import, an unparsed form field — stops matching those records; `'20'` was admitted by coercion and is now denied, and the fix is to parse the field at the producer rather than to widen the rule. A rule reading a field that records inherit from a shared prototype rather than own stops matching. And a `contains` rule written with a non-string value stops matching. Measured over a 2772-case differential matrix of value kinds, operators and record shapes: 352 verdicts narrowed, **zero widened**, and zero change to the genuinely-absent-field family. - -Operator SPELLING is untouched, deliberately: `ne` / `nin` here versus `neq` / `not_in` in the sibling, and the sibling's `is_null` / `is_not_null` which this evaluator does not implement, remain exactly as they were. That divergence is objectui#7750's question. - -Graded `minor` because a release reader can observe the narrowing on stored data; no declared type changed and the set of inputs the evaluator accepts has not widened. diff --git a/.changeset/core-datascope-unknown-operator-denies-7378.md b/.changeset/core-datascope-unknown-operator-denies-7378.md deleted file mode 100644 index b2f09e03f8..0000000000 --- a/.changeset/core-datascope-unknown-operator-denies-7378.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`DataScopeManager` now **denies** a row when a row-level scope rule carries an operator its evaluator does not implement. It used to **admit** the row. - -Behaviour change on a permission boundary, stated plainly. `evaluateFilter` implements nine operator spellings — `eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `contains` — and its `default` arm returned `true`, so a stored `RowLevelFilter` carrying any other spelling passed every record the rule existed to hide, silently: no error, no console line, only a result set that was too large, which looks exactly like a correctly configured permissive scope. The arm now returns `false`, the answer `evaluateCondition` in `@object-ui/permissions` already gives from its own `default` arm. Because `applyFilters` ANDs a scope's rules, one unrecognised rule now denies every row in that scope. - -Who this reaches, measured on this release's base rather than assumed. The `RowLevelFilter['operator']` union is closed, so no TypeScript caller can write an unimplemented spelling, and no code in this repository constructs a `RowLevelFilter` outside the evaluator's own test. The path that changes is scope configuration read back from stored or hand-written JSON and handed to `setFilters` / `registerScopeWithConfig`, where the operator arrives as a plain string the type never checked. A deployment holding such a rule with a spelling outside the nine — including the spec's canonical `equals` / `not_equals` / `greater_than` / `starts_with` and the null-ness family `is_null` / `is_not_null`, none of which have an arm — sees fewer rows from that scope after upgrading, never more. Those spellings are not implemented here; they are refused instead of admitted. Whether to canonicalise them through the spec's `canonicalAstOperator` is left open on objectui#7378. - -Graded `minor` because a release reader can observe the narrowing on stored data; the declared type is unchanged and the set of spellings the evaluator accepts has not widened. diff --git a/.changeset/create-plugin-in-repo-scaffold-note-7524.md b/.changeset/create-plugin-in-repo-scaffold-note-7524.md deleted file mode 100644 index 466ad2c770..0000000000 --- a/.changeset/create-plugin-in-repo-scaffold-note-7524.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Comment-only change to `@object-ui/create-plugin`: `src/templates.ts` gains a docblock -note recording that scaffolding into this monorepo is already refused by named pins -(objectui#7524), and that the emitted per-package `test` block stays correct for the -generator's actual audience. No generated file changes, so nothing is published. diff --git a/.changeset/crud-guide-declarative-view-binding-5446.md b/.changeset/crud-guide-declarative-view-binding-5446.md deleted file mode 100644 index 936ad20c97..0000000000 --- a/.changeset/crud-guide-declarative-view-binding-5446.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: extended `packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx` -to measure the `content/docs/guide/building-crud-app.md` rewrite for objectui#5446 (a -docs-only fix — the guide now teaches the declarative `dataSource: { object, view }` -binding instead of the `view` / `data.queryParams` keys `ObjectGrid` never read). No -published package behaviour changes. diff --git a/.changeset/current-user-default-prefill-5683.md b/.changeset/current-user-default-prefill-5683.md deleted file mode 100644 index b519b0fb64..0000000000 --- a/.changeset/current-user-default-prefill-5683.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/permissions': minor -'@object-ui/plugin-form': minor ---- - -Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. diff --git a/.changeset/cyan-donkeys-tickle.md b/.changeset/cyan-donkeys-tickle.md deleted file mode 100644 index acd39b5025..0000000000 --- a/.changeset/cyan-donkeys-tickle.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Tooling only, no package released: port `scripts/check-entry-guard.mjs` from objectstack and wire it into `Lint`, so a `scripts/**` entry guard hand-typed with `process.argv[1]` cannot land. The 29 guards this tree already carries are baselined SHRINK-ONLY and converted separately (objectui#6092). diff --git a/.changeset/dashboard-aria-member-retired-5830.md b/.changeset/dashboard-aria-member-retired-5830.md deleted file mode 100644 index a104aca90f..0000000000 --- a/.changeset/dashboard-aria-member-retired-5830.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares -the `aria` member (`{ ariaLabel?, ariaDescribedBy?, role? }`). Its doc comment -claimed alignment with `@objectstack/spec AriaPropsSchema`, but the spec removed -`dashboard.aria` at the #3896 audit close-out — `DashboardSchema.shape.aria` is -a tombstone that refuses any value and tells authors to delete the key — and no -dashboard renderer ever read `schema.aria` (objectui#5830). - -What an author loses is the **type-level suggestion** only: the key was already -refused at parse (the Zod twin inherits the spec tombstone by reference), and -`BaseSchema`'s index signature means an existing `aria:` line still compiles. -There is **no runtime behaviour change** — the key never rendered, and stored -documents carrying it already failed validation before this release. diff --git a/.changeset/dashboard-config-aria-retired-5852.md b/.changeset/dashboard-config-aria-retired-5852.md deleted file mode 100644 index 9ce2a42a1a..0000000000 --- a/.changeset/dashboard-config-aria-retired-5852.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Retired the designer-surface dashboard `aria` pair — `DashboardConfig.aria` and `DashboardConfigSchema.aria`** (objectui#5852). - -Both spellings are named verbatim above so a host can grep its own sources: the -retired member is `aria`, on the TypeScript interface `DashboardConfig` -(`@object-ui/types`, `designer.ts`) and on its Zod mirror `DashboardConfigSchema` -(`@object-ui/types/zod`). It declared `{ label?: string; description?: string }`. - -**Why.** The spellings `label`/`description` match neither `@objectstack/spec`'s -`AriaProps` vocabulary (`ariaLabel` / `ariaDescribedBy` / `role`) nor anything a -renderer maps, so no read point could have consumed them even in principle. -Re-measured on `main` at the retirement: zero `.aria` reads in -`packages/plugin-designer/src`, `packages/plugin-dashboard/src` and -`apps/console/src`; zero occurrences of either name anywhere in the `objectstack` -repo; and `DashboardConfigPanel.tsx` — the panel the interface's own doc comment -says it serves — imports neither name. - -**The two directions differ, and neither is a no-op:** - -- **TypeScript (a narrowed suggestion, not a compile break).** `DashboardConfig` - carries a `[key: string]: any` catch-all, so an existing `aria:` line still - compiles; what is gone is the editor suggestion and the false implication that - the key was part of the contract. -- **Zod (a behaviour change — read this one).** `aria` is now an ADR-0049 - retirement tombstone (`z.never().optional()`), following this package's - existing convention. Previously an authored `aria` was **accepted and - preserved** in `safeParse` output; it is now **refused by name**, with `aria` - in the issue path and a message telling the author to delete the key. A plain - deletion was deliberately not taken: `DashboardConfigSchema` is a bare - `z.object` with no `.strict()`, so deleting the key would have made an - authored `aria` **silently disappear** from the parsed output instead — a - quiet data loss in place of a loud refusal. - -**External caveat.** In-repo consumer count is zero, but that is not the npm -count: `@object-ui/types` is published, and stored dashboard configuration is -not reachable from this repo. A host that authored `aria` on a `DashboardConfig` -document will now see a validation error naming the key where it previously saw -a silently carried value. The remedy is to delete the key — it never reached a -renderer. diff --git a/.changeset/dashboard-declare-honoured-inputs.md b/.changeset/dashboard-declare-honoured-inputs.md deleted file mode 100644 index c0129d95e5..0000000000 --- a/.changeset/dashboard-declare-honoured-inputs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -`dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title. diff --git a/.changeset/dashboard-expandable-family-convergence-5692.md b/.changeset/dashboard-expandable-family-convergence-5692.md deleted file mode 100644 index b7fcc7a8cf..0000000000 --- a/.changeset/dashboard-expandable-family-convergence-5692.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`plugin-dashboard`'s two private copies of the reference-bearing field family now -read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating -it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside -`computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the -already-converged consumers carry, so a member-identical private copy fails -rather than quietly re-forking the table. - -Two behaviour changes on the dashboard table's `$expand`, in opposite directions: - -- **A `tree` column is now expanded.** A self-referencing hierarchy field is - reference-bearing and a member of the shared family, so its cell renders the - parent record's display name instead of a bare id — the same treatment the form - and grid roads already gave it. -- **A `reference`-typed column is no longer expanded.** Measured before removing - it: `reference` is absent from `@objectstack/spec`'s closed `FieldType` - vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant - object schema can declare a field whose stored type is `reference`. Dropping it - is a no-op on real data; the spelling is a legacy dialect alias on the - action-param surface, folded to `lookup` before any field-type data is read. - -`EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license -widening a published shared set. diff --git a/.changeset/dashboard-header-collapse-5812.md b/.changeset/dashboard-header-collapse-5812.md deleted file mode 100644 index 9c0d11f887..0000000000 --- a/.changeset/dashboard-header-collapse-5812.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`DashboardRenderer` no longer emits an empty dashboard header wrapper. The -wrapper used to render whenever `header` was declared, while each of its -children — title, description, actions — was additionally suppressible. With -the console page chrome present (`hideHeaderText`, set because the chrome -already renders the dashboard's title and description) and no `header.actions`, -every child evaluated falsy and the DOM still received -`<div class="col-span-full mb-4"></div>`: zero children, yet a full grid row -(measured 64px) plus `mb-4` of dead band above the filter bar, on every console -dashboard page (objectui#5812, measured on HotCRM 17.1.0). - -The three children are now computed first and the wrapper renders only if one -of them survives. Nothing else changes: a standalone embed (no chrome) renders -title and description exactly as before, and declared `header.actions` keep the -wrapper alive even under the chrome, since the chrome renders text only. Authors -needed this — dropping `header` from the metadata to reclaim the pixels would -have cost the standalone embed its title, which is what `header` is for. diff --git a/.changeset/dashboard-readme-card-widget-7035.md b/.changeset/dashboard-readme-card-widget-7035.md deleted file mode 100644 index b80bb90ba3..0000000000 --- a/.changeset/dashboard-readme-card-widget-7035.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Docs + test only, releases nothing. - -`packages/plugin-dashboard/README.md`'s "Dashboard with Charts" example taught two -widgets spelled `type: 'card'` with a nested `body`. `'card'` is not a member of the -widget vocabulary objectui#4600 closed, so the whole document was refused by -`@object-ui/types/zod`'s `DashboardComponentSchema` — measured, not assumed. They are -now the chart-family widgets the renderer actually dispatches (`line` / `pie` with -inline rows under `options.data`), and a new test parses every dashboard example on -that page through the shipped schema so the shape cannot come back unnoticed. - -No package source changed; the new file is a test. diff --git a/.changeset/dashboard-self-contained-unreachable-arm-4620.md b/.changeset/dashboard-self-contained-unreachable-arm-4620.md deleted file mode 100644 index 7d8e70c295..0000000000 --- a/.changeset/dashboard-self-contained-unreachable-arm-4620.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained -(card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally -(objectui#4620). - -`isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the -`isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The -`datasetBound` side of that inner fork could never execute: reaching it required -`isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged — -the removed arm never ran, and the reachable fork in the Card branch (the one that gives -a dataset-bound metric its title and border chrome) is untouched. - -The cost was to readers, not to users: the shape read as "both branches handle -dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto -`DashboardGridLayout` had to pay for the reachability argument before it could decline to -copy the dead limb. A comment now names the invariant in place so the arm is not re-added. diff --git a/.changeset/dashboard-stable-empty-rows-4629.md b/.changeset/dashboard-stable-empty-rows-4629.md deleted file mode 100644 index 9fc9806548..0000000000 --- a/.changeset/dashboard-stable-empty-rows-4629.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for -"no rows yet" instead of a fresh array literal per render (objectui#4629). - -Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so -whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind` -path that resolves to an object — the fallback produced a NEW array identity on -every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so -every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the -`isSystemField` pass, the `fieldLabel` lookups) and then discarded by the -`finalData.length === 0` early return. In `ObjectPivotTable` the value is handed -straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot -rebuilt its row/column sets, bucket map and totals on every render over no rows -at all. - -Nothing rendered wrong before or after; this is wasted work in the empty window, -plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is -the same module-scope frozen empty `data-table.tsx` adopted for its own -`EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings. diff --git a/.changeset/dashboard-table-header-one-spelling-5425.md b/.changeset/dashboard-table-header-one-spelling-5425.md deleted file mode 100644 index 8a0de721b2..0000000000 --- a/.changeset/dashboard-table-header-one-spelling-5425.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does. - -`ObjectDataTable` derives headers on two paths — from the author's declared -`columns`, and from the object schema when no columns were declared. The -declared path (and the static `data-table` half of the same widget family) -already used `humanizeFieldKey`, whose docstring names it "the single home for -the convention, because both halves of the `table` widget family need it and -they must agree". The auto-derived path carried a third, inline spelling that -split camelCase but never turned `_` into a space, so it left a raw underscore -on screen. Measured over the same object's columns: - -``` -path close_date needs_analysis -object-bound, AUTO-DERIVED (before) Close_date Needs_analysis -object-bound, AUTO-DERIVED (after) Close Date Needs Analysis -object-bound, DECLARED columns Close Date Needs Analysis -static `data-table`, no columns Close Date Needs Analysis -``` - -One dashboard can hold all three widgets over one object, so a single field key -rendered under two spellings — the defect class objectui#5425 rules out. The odd -path adopts the shared convention rather than the convention gaining a fourth -dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and -after — the coincidence that kept the snake_case divergence unnoticed), and a -translated header still wins: only the fallback handed to `fieldLabel` changed. - -Dimension MEMBER labels are untouched by this. The same card reported dashboard -members rendering a prettified enum instead of the picklist's translated label, -measured on 17.1.0; re-measured on this branch it no longer reproduces — the -analytics label net shipped in 17.5.0 routes every non-metric dataset dimension -through the field's declared options and the locale bundle. That behaviour had -no test stated in the card's terms and now has one, over the four dashboards the -card measured, including the property that a bar axis and a pivot header cannot -disagree about one stored value. diff --git a/.changeset/dashboard-table-measure-desc-first-5845.md b/.changeset/dashboard-table-measure-desc-first-5845.md deleted file mode 100644 index 6eb74642b6..0000000000 --- a/.changeset/dashboard-table-measure-desc-first-5845.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard `table` widget — a numeric **measure** column now sorts **descending -on the first click** (objectui#5845). - -The sortable headers shipped in objectui#5827 started every column ascending, -which left the card's own motivating complaint — the largest industry rendering -last — two clicks away. For a measure in an analytics widget the question a -click asks is "who is biggest", so that column now cycles -**descending → ascending → the dataset's own order**. - -Unchanged: a **dimension** column, and a measure whose values are not numbers -(a `min()`/`max()` over a text field), keep **ascending → descending → the -dataset's own order** — the idiom the console's own DataTable uses. The -first-click direction is decided by the same measure-and-all-values-are-numbers -classification that decides right-alignment, so it follows the column's *role* -rather than "it looks like digits": a digit-keyed dimension (a year, a quarter) -still starts ascending. - -Blanks still sort **last in both directions** — that arm is now what a measure -column's very first click runs — and `aria-sort` reports the direction actually -applied. diff --git a/.changeset/dashboard-table-widget-polish-5827.md b/.changeset/dashboard-table-widget-polish-5827.md deleted file mode 100644 index 9714c2330f..0000000000 --- a/.changeset/dashboard-table-widget-polish-5827.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering -fixes so it reads as a table rather than a raw dump (objectui#5827): - -- **Numeric columns right-align**, header and cells. `tabular-nums` was already - applied to every cell and could not do its job while the digits were flushed - left. A column qualifies when it is a MEASURE and every non-null value in it - is a number — the declared column `type` cannot decide it, because the - analytics executor stamps `type: 'number'` on every measure regardless of the - aggregate, so a `min()`/`max()` over a text field would have been - right-aligned on the strength of a label. Dimensions stay left-aligned even - when their bucket keys are digits: a grouping axis is not a quantity. -- **Sortable headers.** Each header is a keyboard-reachable button that cycles - ascending → descending → back to the dataset's own order, with the console's - existing sort indicator and `aria-sort` on the `th`. Blanks sort last in - BOTH directions. The sort is client-side over the rows already fetched — the - widget issues one `queryDataset` call and paginates nothing, so no round trip - is involved. -- **The empty-dimension bucket sorts last** in the default order instead of - floating to the top. It keeps its `—` label; only its position moves. -- **Row hover feedback on every row**, not only on a drillable one. A drillable - row keeps its stronger accent fill. - -The CSV export now follows the order the table is showing, which is the same -"the CSV is the table's data" convention its cell text already followed. -Drill-through is unaffected: rows are reordered as `(row, incoming index)` -pairs, so a drill still resolves through the server's parallel raw-value -sidecar. - -Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and -chart paths, and every non-dataset table in the console. diff --git a/.changeset/dashboard-title-locale-writeback-5428.md b/.changeset/dashboard-title-locale-writeback-5428.md deleted file mode 100644 index 849b85b703..0000000000 --- a/.changeset/dashboard-title-locale-writeback-5428.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-designer': patch -'@object-ui/app-shell': patch ---- - -A widget title stored as an inline per-locale map is editable again in both dashboard -authoring surfaces, and a save writes back only the active locale's entry -(objectui#5428). - -`@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at -17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both -authoring panels edit a title in ONE single-line input. Writing the input's value back -as the whole value would collapse every other locale on the first keystroke, so both -surfaces took the same conservative branch: show a map-valued title resolved, and make -it READ-ONLY. - -That branch could not lose data, but it rested on a premise the spec had already -invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was -plain `string` through rc.5" — stated sixty lines below a comment in the same file -documenting the rc.6 widening that makes a stored map reachable. Both could not hold. -The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward -was not protect an unreachable path: it denied an author the ability to edit a widget -title in their own locale. - -objectui#5301's maintainer ruling settled the write rule for the sibling surface — a -save replaces only the active locale's entry and preserves the others — and -`@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because -the read and the write have to agree. Both panels now adopt it: - -- `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; -- `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. - -A plain-string title keeps saving as a plain string, so the common path is unchanged. -An edit made in a locale the stored map does not carry ADDS an entry under that locale -rather than overwriting the entry the display fell back to. - -The pins are preservation pins, not "the input is editable" pins: at both surfaces a -keystroke on a map-valued title must leave every other locale's entry byte-identical. -Reverse-verified by mutating each write back to the flattening form and confirming those -assertions go red at both surfaces. - -Not a multi-locale editor: an author still reaches only the entry for the locale they -are in. Authoring every locale from one panel remains an open product question. The -stale deferrals both comments carried pointed at objectui#4163, which closed as -completed on 2026-08-15 while the placeholders were still in the tree; they are replaced -with the rule that is actually in force rather than re-pointed at another tracker. diff --git a/.changeset/dashboard-widget-dataset-stale-cast-5067.md b/.changeset/dashboard-widget-dataset-stale-cast-5067.md deleted file mode 100644 index ba87f15eca..0000000000 --- a/.changeset/dashboard-widget-dataset-stale-cast-5067.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Internal only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`DashboardRenderer.tsx`'s dataset-bound check (`const datasetBound = !!(widget as -any).dataset`) drops its `as any` (objectui#5067). The comment's stated reason — -"the bundled DashboardWidget type gains `dataset` only after objectui bumps -`@objectstack/spec`" — is stale: the repo already carries `@objectstack/spec@17.0.0`, -whose `DashboardWidget` declares `dataset`, and `packages/types/src/complex.ts`'s -`DashboardWidgetSchema` inherits it through its `extends Omit<Partial<SpecDashboardWidget>, -…>` (`dataset` is not in the `Omit` list). `widget.dataset` type-checks directly; the cast -was pure redundant widening, and a harmful one — after `as any`, `.dataset` reads as -`any`, so a future spec change to that key's shape would not go red here. - -**No runtime behaviour changes.** `!!(widget as any).dataset` and `!!widget.dataset` -evaluate identically for every input; `as any` is a compile-time-only annotation. Proven -with a reverse check: temporarily typo'ing the property to `widget.datasetTypoXYZ` turns -`pnpm --filter @object-ui/plugin-dashboard type-check` red with `TS2339: Property -'datasetTypoXYZ' does not exist on type 'DashboardWidgetSchema'`, confirming the removed -cast was suppressing real type coverage rather than papering over a genuine gap. - -The ADR-0021 point the original comment made — a dataset-bound widget renders through -the governed `queryDataset` path (`DatasetWidget`) instead of the inline object-aggregate -schema — is unchanged and kept; only the now-false justification for the cast is rewritten. diff --git a/.changeset/dashboard-widget-type-closed-enum.md b/.changeset/dashboard-widget-type-closed-enum.md deleted file mode 100644 index 11f2c6bbf9..0000000000 --- a/.changeset/dashboard-widget-type-closed-enum.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/types": minor -"@object-ui/plugin-designer": patch -"@object-ui/app-shell": patch ---- - -Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectui's own component extension. - -`DashboardWidgetSchema.type` was `string` on the TypeScript interface and `z.string()` in the Zod twin — an unbounded hatch. A typo'd family, a chart type the spec retired, and a component type nothing registers all type-checked and validated, surfacing only as the renderer's red `OBJUI-001` panel at runtime. - -It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum). - -Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one. diff --git a/.changeset/data-table-header-declared-key-5351.md b/.changeset/data-table-header-declared-key-5351.md deleted file mode 100644 index c0b154de2d..0000000000 --- a/.changeset/data-table-header-declared-key-5351.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/components': minor -'@object-ui/plugin-dashboard': minor -'@object-ui/plugin-detail': minor ---- - -`data-table` reads the declared `header`; the producers translate `label` into it. - -`TableColumn` declares `header: string` and does not declare `label`. The -renderer's column normalization nonetheless read `header: col.header || col.label`, -so the same key had one spelling the type admits and one only the runtime did. -That alias is gone (objectui#5351), and the translation it used to perform happens -once at each producer instead: metadata vocabulary in, adapter vocabulary out. - -**This narrows what `data-table` accepts, so read this if you author `data-table` -nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a -directly authored `data-table` now renders a **headerless** column over live -cells. Spell it `header` — the key `TableColumn` has always declared. Columns -reaching `data-table` through `object-data-table`, `object-grid` or a related -list are unaffected: those producers resolve `header` for you from the spec's -`ListColumnSchema.label`, so every spelling they accepted before they still -accept. - -`@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader -producers use to cross that boundary. It is adapter-first (`header` wins over -`label`), so an author who addressed the table directly is never overwritten. - -`object-data-table` also gains a fix from the same move: a column carrying a -`label` used to render a **blank** header there even while the alias existed, -because the widget's field-meta enrichment overwrote the authored `label` before -the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". - -The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here -and still resolves. Retiring it is objectui#5120's remaining step, which is -gated on two published skill guides that teach that spelling. diff --git a/.changeset/date-range-default-range-binding-4984.md b/.changeset/date-range-default-range-binding-4984.md deleted file mode 100644 index 60e7298530..0000000000 --- a/.changeset/date-range-default-range-binding-4984.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`DashboardComponentSchema.dateRange.defaultRange` is now bound to -`DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a -hand-written 14-member union (objectui#4984). - -The union was byte-faithful to the spec — all 14 members, same order — so nothing -a user hits changes today. What was missing is the tie that keeps it faithful: -`resolveDashboardFilterDefs` takes `Pick<DashboardComponentSchema, 'globalFilters' | -'dateRange'>`, so this union is what typechecks every TS-constructed dashboard, and -a preset the spec ADDS would have been a legal document that objectui's own types -said could not exist — the "narrower than the contract it implements" shape whose -consequence in objectui#4163 was that the bad reads were invisible to `tsc`. - -No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union -on an interface member has no symbol to collide with, while rule 2's claim heuristic -was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes -the file's existing "Aligned with @objectstack/spec" comment structural rather than -prose. - -The emitted `.d.ts` collapses the inline union to the imported alias; the published -type surface is unchanged — measured with the TypeScript checker over the emitted -declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, -and `defaultRange` resolving to the same 14 string-literal members before and after). diff --git a/.changeset/dead-metadata-converters-6224.md b/.changeset/dead-metadata-converters-6224.md deleted file mode 100644 index 63c5a43b3d..0000000000 --- a/.changeset/dead-metadata-converters-6224.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Delete the dead `src/utils/metadataConverters.ts` module. It had zero importers, was not -re-exported from the package barrel, and no `exports` subpath reached it — `toObjectDefinition`, -`toFieldDefinition`, `MetadataObject` and `MetadataField` were never part of the published -surface, so nothing external can break. The module was removed rather than left alone because it -carried two patterns the live code no longer uses: a name-heuristic `isSystem` (the server's real -`system` flag is the source of truth) and a three-way `referenceTo` tolerance for a target the -spec spells `reference`. A dead copy that disagrees with the live one is what a future author -copies from. diff --git a/.changeset/dead-refresh-callback-objectview.md b/.changeset/dead-refresh-callback-objectview.md deleted file mode 100644 index 6f077fa36d..0000000000 --- a/.changeset/dead-refresh-callback-objectview.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Internal only, no released behaviour change: `plugin-view`'s `ObjectView` no -longer declares a `handleRefresh` callback that nothing referenced. - -The callback bumped `refreshKey` but was never passed to the toolbar, exposed on -a handle, or wired to any control — it advertised a refresh entry point the -component does not have, which cost objectui#4549 a detour to rule out. The -reachable refresh paths are unchanged (`onMutation` auto-subscribe, delete, bulk -delete, form success), and the real toolbar Refresh button continues to live in -`plugin-list`'s `ListView`, reached through `renderListView`. diff --git a/.changeset/default-children-retired-5051.md b/.changeset/default-children-retired-5051.md deleted file mode 100644 index 0605f03d1c..0000000000 --- a/.changeset/default-children-retired-5051.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor -'@object-ui/components': minor ---- - -The register-meta key `defaultChildren` is retired (objectui#5051). - -It was declared in four places, produced in eleven, and read in **none**. The designer's -drop path builds a new node from its twin key only — `PageDesigner.tsx`, -`props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item -that declared `defaultChildren` dropped an **empty** node and the declared children never -materialised. Nothing rendered the wrong thing; an entire declaration surface was simply -inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer -ruling of 2026-08-19, the key is removed rather than wired up; if designer -default-children UX is ever product-wanted it returns as its own designed card. - -**If you author plugins against the published register-meta table, drop the key.** It is -gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A -meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, -and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the -key is silently dropped from the parse output instead of failing validation. TypeScript -authors get the loud signal instead: all three `ComponentMeta` declarations -(`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no -longer offer it, so re-declaring it is now a compile error. - -**No runtime behaviour changes in either direction.** No code path read the key before -this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were -feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types -into the designer produces exactly the node it produced yesterday. - -Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key -is absent from the parse output, with a surviving sibling asserted present through the -same parse as the control) plus the two TS twins with `@ts-expect-error`, and -`packages/core` pins the registration surface the eleven producers were written against. -Both are compile-time-enforced through each package's chained `tsconfig.test.json`. diff --git a/.changeset/default-view-agenda-retired-5784.md b/.changeset/default-view-agenda-retired-5784.md deleted file mode 100644 index ae59ebf296..0000000000 --- a/.changeset/default-view-agenda-retired-5784.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`'agenda'` leaves `defaultView` on all three of its declaration faces -(objectui#5784 — the `defaultView` sibling of objectui#5740's retirement on -`CalendarViewSchema.view`; ADR-0049 enforce-or-remove): the -`ObjectCalendarSchema` TS interface (an inline union `#5740`'s -`CalendarViewMode` narrowing could not reach), the zod `ObjectCalendarSchema`, -and the list-view `calendar` config's objectui-only `defaultView` extension. -All three are now `['month', 'week', 'day']`. - -The declarations admitted a value nothing enforced: `ObjectCalendar`'s props -declare `defaultView?: 'month' | 'week' | 'day'`, its schema read casts to the -same three values, and `CalendarView` renders no agenda view. An author -writing the type-legal, zod-valid `defaultView: 'agenda'` on an -`object-calendar` node or in a list view's `calendar` config got a month -calendar with no error or warning. The spec side already agrees: -`@objectstack/spec`'s `ObjectCalendarProps.defaultView` is -`['month', 'week', 'day']`. No in-repo, example, or catalog app authors -`defaultView: 'agenda'` (measured for objectui#5784 with positive controls, -including the objectstack tree — its only `agenda` token is the Agenda -job-scheduler library). - -**This narrows the accept set: an author who writes `defaultView: 'agenda'` -will now be refused at validation.** `defaultView` is a declared key, and -declared keys are validated even under `.passthrough()`, so -`defaultView: 'agenda'` is a **validation error that previously parsed -green** (an `invalid_value` issue on the `defaultView` / -`calendar.defaultView` path, offering `month`/`week`/`day`). Undeclared keys -still pass through unchanged. Breaking on the published zod surface; ships as -`minor` per this repo's version-alignment policy (majors track -`@objectstack`). - -The runtime boundary is unchanged: `ObjectCalendar` still resolves an -off-union raw `defaultView` away to its `'month'` default. Docblocks and both -zod `describe` strings now teach the three-value set. diff --git a/.changeset/defaults-maps-mirror-discovery-7884.md b/.changeset/defaults-maps-mirror-discovery-7884.md deleted file mode 100644 index 3e2c7c8dc8..0000000000 --- a/.changeset/defaults-maps-mirror-discovery-7884.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change (objectui#7884): the `defaults-maps-mirror-en-pack` gate's rule "every -row names a key the en pack actually defines" now runs over the `createSafeTranslation` -defaults tables discovered from source, instead of a hand-written list of three imported -maps that judged 400 of 1056 rows. The AST walk objectui#3512 already had moved into -`@object-ui/test-support` (private, never published) so both gates share one definition of -the population rather than growing a second traversal. No published behaviour changes. diff --git a/.changeset/degenerate-magnitude-charts.md b/.changeset/degenerate-magnitude-charts.md deleted file mode 100644 index 77c597f168..0000000000 --- a/.changeset/degenerate-magnitude-charts.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -Pie, donut, funnel and treemap now say when rows carry no magnitude they can draw. - -These four families size a mark BY its measure, so a row whose value is zero, -negative, `null` or unparseable stays in the data and is given no area. Measured -in Chromium across 74 tiles: an all-zero pie put ZERO non-white pixels on the -page while its DOM carried 31 descendants and a real `svg`; a treemap handed -`40 / null`, `40 / 0` or `40 / -25 / -12` rendered one full-bleed leaf that was -byte-identical to a genuinely one-row treemap; and a funnel handed `40` beside a -`null` drew no segments at all and labelled the tile with the row that had no -value. - -When no row can be sized, these charts now render the file's refusal shell -(`no-positive-magnitude`) instead of a blank tile. When only some rows can be -sized, the chart still draws and carries a note counting the ones it could not. -All-positive charts, charts handed no rows at all, bar charts, and both sankey -answers are unchanged. diff --git a/.changeset/degrade-set-twin-pin-5880.md b/.changeset/degrade-set-twin-pin-5880.md deleted file mode 100644 index d87ccf0015..0000000000 --- a/.changeset/degrade-set-twin-pin-5880.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change: pins the target-required degrade set (`{lookup, master_detail}`) equal -across `@object-ui/app-shell`'s `paramToField` and `@object-ui/plugin-grid`'s -`bulkParamToField`, which were two independent statements of one rule with nothing -mechanical holding them together. No published behaviour changes, no new exported symbol, -and neither set's membership was touched — the pin derives each package's effective set -through its own adapter and asserts the contents equal. diff --git a/.changeset/delete-dead-release-workflow-5405.md b/.changeset/delete-dead-release-workflow-5405.md deleted file mode 100644 index b1c00f27a1..0000000000 --- a/.changeset/delete-dead-release-workflow-5405.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -CI/docs only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -Deletes `.github/workflows/release.yml`, a workflow that never ran once in this -repository's history, and removes the two places on -`content/docs/guide/ci-cd-pipeline.md` that documented it as a release path. - -It triggered on `push: tags: ['v*']`. No tag matching that glob has ever existed here: -of the 2896 tags on the remote, every one is a Changesets per-package tag -(`@object-ui/<pkg>@<semver>`) and not one begins with `v`. Nothing creates tags except -the Changesets action, so the trigger had no way to fire. - -It is obsolete rather than misconfigured, which matters because the two have opposite -fixes. The workflow parsed and registered fine (GitHub lists it `active`) and would fire -if a `v*` tag were pushed — the job it would do, "Create GitHub Release", is simply -already being done by `changeset-release.yml`, whose Changesets action publishes GitHub -Releases tagged `@object-ui/<pkg>@<version>`. Its own npm publish step was still -commented out under "Uncomment the following steps when ready to publish to npm" while -the repo has been publishing to npm through Changesets for months, and its one-version -model (a single `v<semver>` for the whole repo, pointing at the root `CHANGELOG.md`) -never matched the 39-package fixed group this repo actually releases. - -The doc edit is not optional housekeeping: `scripts/__tests__/ci-cd-pipeline-doc.test.ts` -pins that page to `.github/workflows/` in both directions, so a page still naming -`release.yml` after the file is gone fails "never names a workflow file that does not -exist". - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/designer-field-key-parity-gate.md b/.changeset/designer-field-key-parity-gate.md deleted file mode 100644 index af9f06da57..0000000000 --- a/.changeset/designer-field-key-parity-gate.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Tooling and tests only; no published behaviour changes. - -Adds `check:designer-field-key-parity`, a gate that compares the field designers' -statically declared payload shapes (`FieldMetadataPayload`, `ServerFieldSchema`, -`DesignerFieldDefinition`) against the accept set of the installed -`@objectstack/spec` `FieldSchema`, plus the draft-I/O round-trip half as a test in -`@object-ui/app-shell`. Both are new checks over existing code — nothing shipped -in a package changed, and no offending key was fixed: each one the gate surfaced is -filed as its own card and recorded in the gate's ledger. diff --git a/.changeset/designer-publish-package-binding.md b/.changeset/designer-publish-package-binding.md deleted file mode 100644 index 4bba3134eb..0000000000 --- a/.changeset/designer-publish-package-binding.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/app-shell': minor ---- - -The metadata designer states its package on the publish step, not only on the save (#5420) - -Studio's designer save→publish loop bound the draft to a software package on the -save (`PUT ?mode=draft&package=<id>`) and then sealed it with a publish that named -no package at all. `objectstack#10354` (shipped in `@objectstack/rest` 17.2.0) taught -`POST /meta/:type/:name/publish` to accept `?package=<id>`, so the second call can now -state the same binding the first one already states. - -- `MetadataClient.publish()` accepts `packageId` and sends `?package=<id>`, the same - wire spelling and the same `encodeURIComponent` treatment `save()` gives it. -- `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation - (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The - `?package=all` "show everything" scope keeps folding to "no package". - -The parameter is **omitted**, never sent empty, when the designer holds no binding. -Empty and absent are the same to the framework's normaliser today, but absent is the -shape the save door already followed, and the framework's promotion path branches on -the key being present downstream. - -What this buys is **reachability**, not speed: it lets `#9612`'s package-closure -narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That -narrowing has a second, independent gate this does not touch — objects carrying no -`_packageId` provenance are kept unconditionally — so on a tenant-authored overlay -corpus stating the package still narrows nothing. diff --git a/.changeset/dom-leak-sweep-components-5574.md b/.changeset/dom-leak-sweep-components-5574.md deleted file mode 100644 index ce389acf86..0000000000 --- a/.changeset/dom-leak-sweep-components-5574.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The one file changed is -`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`, which -`packages/app-shell/tsconfig.json` excludes from the build program -(`"exclude": [… "**/*.test.tsx"]`). - -Measured rather than asserted, because emitted-output behaviour varies per package: -`packages/app-shell/dist` was built twice from a cleared `dist/` **and** a cleared -`tsconfig.tsbuildinfo` — once at the `origin/main` version of that file, once at this -branch's version — and sha256-compared. 862 emitted files, 431 of them `.d.ts`, every -hash equal. (The first attempt at this measurement read as an empty `dist/`: `tsc` is -`composite`, so with the build info left in place it skipped the emit entirely and -produced no output to compare. Clearing the build info is part of the measurement, -not a detail.) - -Widens the objectui#3291 DOM-leak canary sweep to `packages/components/src/renderers/**`. -That family — 158 registry-reachable types across five namespaces — was outside the -gate's discovery entirely, which is why `ui:grid`'s leak had to be found by hand. The -first run records a ledger: 119 of 158 targets leak, in eight measured shapes, every -renderer named. Nothing is skipped or allow-listed; the per-target assertion stays exact -set equality in both directions, so a renderer fix cannot go green until its ledger row -is deleted in the same change. diff --git a/.changeset/dom-leak-sweep-fixture-depth-5630.md b/.changeset/dom-leak-sweep-fixture-depth-5630.md deleted file mode 100644 index d471f6afd3..0000000000 --- a/.changeset/dom-leak-sweep-fixture-depth-5630.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only: deepen the `element:definition-list` / `element:repeater` / -`element:metadata_viewer` fixtures in the DOM-leak sweep -(`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`) past their -empty-state placeholder branch, so their clean reading covers the real, -populated markup instead. No renderer source changed and no published -behaviour changes. diff --git a/.changeset/draft-changes-panel-eager-attribution-comment.md b/.changeset/draft-changes-panel-eager-attribution-comment.md deleted file mode 100644 index e3d23fb00c..0000000000 --- a/.changeset/draft-changes-panel-eager-attribution-comment.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Comment-only correction in `packages/app-shell/src/preview/DraftChangesPanel.tsx`. The block above the `/meta` spelling fold stated that this file's `@objectstack/spec/shared` import puts that subpath on the console's eager graph, and attached a byte figure and a prohibition to that claim. Ablation on `origin/main` shows the import is worth ~+285 bytes gzipped: the subpath is held eager by a runtime import inside `@objectstack/core`, upstream of this repo. The comment now records the mechanism and points at the upstream card. No source behaviour is touched and no published behaviour changes. diff --git a/.changeset/drawer-bucket-pointer-ruling.md b/.changeset/drawer-bucket-pointer-ruling.md deleted file mode 100644 index 8b3aaa773c..0000000000 --- a/.changeset/drawer-bucket-pointer-ruling.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Comment text only, zero-pixel: the record-drawer `size: 'lg'` bucket question is -recorded as ruled, and the three in-code pointers stop naming closed cards as its -open home. - -`ObjectKanban`, `ObjectCalendar` and `ObjectGantt` each carried a comment saying -the bucket question "stays open" on objectui#6303 / objectui#6259. Both cards had -closed, so the pointer chain terminated on closed cards and the question it -pointed at was tracked nowhere. It has since been ruled: objectui#6584, -2026-08-27 — the record-navigation drawer stays on the CSS literal, with no -convergence on the `size: 'lg'` bucket, because that bucket (`min(92vw, 960px)`) -is up to 53% wider below a 1600px viewport and there was no demand for the -change. - -The three comments now cite objectui#6584 and state the ruling and its date. The -measurement prose they carry (`min(92vw, 960px)` vs `min(960px, 60vw)`) is kept -verbatim — it is still true and it is the reason the ruling went the way it did. -`RecordDetailDrawer`'s `width` docblock, the single code home of the literal that -all three renderers fall through to, gains the same note so the record lives at -the line a future editor would actually change. - -No published behaviour changes: no default, `width` expression, `size` prop or -runtime value is touched. diff --git a/.changeset/dts-build-exit-code-5370.md b/.changeset/dts-build-exit-code-5370.md deleted file mode 100644 index b6fe0bc6dc..0000000000 --- a/.changeset/dts-build-exit-code-5370.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Build tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched: `@object-ui/layout`'s `vite build` -now exits non-zero when the declaration step reports type errors, instead of printing them -and exiting 0 (objectui#5370). The typings and the JavaScript it emits are unchanged. diff --git a/.changeset/dts-explicit-extensions-5365.md b/.changeset/dts-explicit-extensions-5365.md deleted file mode 100644 index 73de8c9d34..0000000000 --- a/.changeset/dts-explicit-extensions-5365.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/layout': patch ---- - -The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. - -`vite-plugin-dts` emits one declaration file per source file, and TypeScript -copies a module specifier into the declaration verbatim. `export * from './ui'` -therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in -`@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted -trees. Node16/NodeNext resolution does not extension-search a relative -specifier, so the compiler could follow none of the hops and every symbol they -carried read as absent from the package: - -``` -error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. -``` - -Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls -in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from -`layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. -On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. - -The emitted `.js` never had the defect — rolldown resolves the same specifier -away — which is why `pnpm check:esm-specifiers`, whose verdict is about -specifier-preserving `.js` builds, correctly never scanned either package. The -fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, -shared by both `vite.config.ts` files), not in the sources: the same source line -produces a clean `.js` and a broken `.d.ts`, so no source edit can express the -difference. The rewriter resolves each specifier against the source tree the -output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — -throws on anything it cannot resolve, and after the build re-parses the emitted -declarations to assert every relative specifier both carries an extension and -names a file the build really emitted. - -`packages/fields` takes the `nodenext` pin as a result — the same two lines -`packages/react` has carried since objectui#4538 — so the property is enforced by -the compiler on the consumer side rather than by review. `packages/app-shell` -does not: it type-checks clean without the pin and still shows 23 errors with it, -none of them from these two packages. That residue is filed separately. diff --git a/.changeset/eager-closure-budget-5324.md b/.changeset/eager-closure-budget-5324.md deleted file mode 100644 index 230d596709..0000000000 --- a/.changeset/eager-closure-budget-5324.md +++ /dev/null @@ -1,45 +0,0 @@ ---- ---- - -CI and build tooling only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. No package `src/` is touched. - -The console "performance budget" now weighs the **eager closure** instead of one chunk. - -`.github/workflows/performance-budget.yml` gzipped `apps/console/dist/assets/index-*.js` -and compared it against a 350 KB line. Measured on `77f846a8b`, that chunk is 25,910 bytes -gzipped, while the closure it statically pulls in — every chunk the browser must fetch and -parse before the app renders — is 3,881,609 bytes across 58 of 507 chunks. The gate passed -on 0.67% of the payload it claimed to govern, and `advancedChunks` routes vendor and -workspace code into named chunks on purpose, so most regressions land outside the entry -chunk. objectui#5266 is the worked example: 89 KiB gzipped added to every page load, landing -in `vendor-objectstack-*.js`, structurally invisible here (objectui#5324). - -`emitEagerClosureReport` in `apps/console/vite.config.ts` walks rolldown's own -`chunk.imports` from the entry chunks — static edges only, because the dynamic edge is the -lazy boundary — gzips the bytes actually written to disk, and writes -`dist/eager-closure.json`. `scripts/check-eager-closure-budget.mjs` applies the ceiling. -The split is deliberate: a size ceiling enforced inside `vite build` would fail every -Vercel preview and every local build, which is how a budget gets switched off rather than -fixed. Exit codes are distinct — `1` over budget (a verdict about the bundle), `2` no -trustworthy measurement (a verdict about the gauge) — so a broken gauge is never reported -as a clean bundle, and vice versa. - -Every check in that path is a counter-probe, because this gate's failure mode is silent: a -walk that finds too little, a stale report, an absent field read as zero all produce a -SMALL number, and a budget reads a small number as good news. So the build refuses to -publish a figure unless `react-dom` is inside the closure and at least one chunk is outside -it, and the checker refuses a report whose totals disagree with its own chunk list, whose -version it does not recognise, or that has collapsed to its entry chunk — that last one -being precisely the gauge this replaces. - -The ceiling is 3,960,000 gzipped bytes: today's measurement plus 78,391 bytes of headroom. -It passes on current `main`, and the headroom is deliberately narrower than the 89 KiB -regression the gate exists to catch, so a repeat of objectui#5266 fails it (verified: the -baseline plus 89 KiB comes out 12.4 KB over). Both constraints are asserted in -`scripts/__tests__/check-eager-closure-budget.test.ts`, not merely argued in a comment. - -This is a truthful current-state ceiling, not a target. 3.79 MB gzipped before first render -is a bad payload and the honest long-term line is far below it; lowering it is a separate -decision with its own work behind it. The entry-chunk budget and its 350 KB line are -unchanged — replacing a blind gauge is not licence to drop the check that was already there. diff --git a/.changeset/eighty-pandas-listen.md b/.changeset/eighty-pandas-listen.md deleted file mode 100644 index 9278e33bd8..0000000000 --- a/.changeset/eighty-pandas-listen.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: `ObjectView`'s two `contractEnvelope` pins (objectui#6726, objectui#6840) now assert on `deliveredThrough(asData)` directly instead of calling it from inside a `waitFor` predicate, and the helper waits on `find()`'s own settled answer before reading the last delivery. No published behaviour changes. diff --git a/.changeset/esm-load-comment-mask-5382.md b/.changeset/esm-load-comment-mask-5382.md deleted file mode 100644 index 70abee6d08..0000000000 --- a/.changeset/esm-load-comment-mask-5382.md +++ /dev/null @@ -1,41 +0,0 @@ ---- ---- - -CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched. - -Fixes the comment mask in `scripts/check-node-esm-load.mjs` (objectui#5382). Leg 1 of -the gate blanked comments out of every source with two ordered regexes and then matched -specifiers in the result. The block-comment pass ran first and had no notion of already -being inside a `//` line, so a slash-star sequence occurring in ordinary line-comment -prose — a package glob, a path pattern, a wildcard import, all of them ordinary here — -opened a comment that ran to the next closing delimiter anywhere in the file and blanked -every line between, live code included. - -Re-measured on `main` at 478ec54ce over the 805 emitted sources of the 13 -specifier-preserving packages: the mask found 2132 relative specifiers and the TypeScript -parser found 2133. The one it could not see is a real `import` in -`packages/app-shell/src/preview/DraftChangesPanel.tsx`, hidden by a line comment naming -the `@objectstack` chunk group by glob two lines above it. - -That direction is the bad one. Since `SPECIFIER_DEBT` emptied, leg 1 is a hard -requirement rather than a ratchet, so a blind spot in it is somewhere a regression can -sit permanently while the run reports clean and only the nightly load leg can see the -consequence. - -Leg 1 now reads each file exactly as written and takes its module edges from -`check-phantom-dependencies.mjs`'s shared TypeScript scanner — the same one -`check-package-self-import.mjs` uses, so three gates cannot drift apart on what a module -edge is. Comments, strings, template literals and regex literals stop being questions -this gate has an opinion about. Reported line numbers stay the compiler's: the specifier -literal is located inside the statement the parser already identified, which matters -because `tsc` reports this class at the specifier and the statement opens on a different -line for 255 of 2066 relative specifiers here. - -The gate's verdict is unchanged — 0 findings before, 0 findings after — because the -newly visible import already carries its `.js` extension. What changed is that it is now -visible. The whole cheap leg went from 0.71s to 2.43s. - -`readTsconfig()` in the same script strips comments with the same kind of -context-unaware regex and is a separate live instance of this class. It is deliberately -untouched here and remains open as objectui#5367. diff --git a/.changeset/evaluateexpression-jsdoc-links-5580.md b/.changeset/evaluateexpression-jsdoc-links-5580.md deleted file mode 100644 index d8167b1959..0000000000 --- a/.changeset/evaluateexpression-jsdoc-links-5580.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction` -JSDoc are now qualified, so each resolves to the entity it means (objectui#5580). - -`ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method -on `ExpressionEvaluator` (bare expression, throws) and the module-level export -(context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block -referred to both under the one spelling, four lines apart. - -The prose link was not merely ambiguous, it was bound wrong. Measured with -`checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}` -resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the -sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}` -binds to the method, but only because no module-level `evaluate` exists to outrank -it, so the rule "an unqualified link resolves to the enclosing class's member" does -not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`, -which the checker resolves to the `MethodDeclaration`. - -The `@example`'s final line is the module-level export — its second parameter is a -context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat -two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover -carries no import to disambiguate. It now names the module-level export and shows the -import it needs. - -This is prose only: the diff is confined to a block comment and no declaration moves. -It is scored `patch` rather than the empty-frontmatter form because the block is -emitted into what npm ships — measured, this edit moves both -`dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js` -(this package builds with a bare `tsc`, which preserves comments in the JS emit), and -the ten changed lines in that JS are all comment lines. - -`registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather -than asserting it in prose, since a `{@link}` that binds to the wrong entity is -indistinguishable in source from one that binds right. diff --git a/.changeset/filecell-error-slot-5431.md b/.changeset/filecell-error-slot-5431.md deleted file mode 100644 index c6a890b5e6..0000000000 --- a/.changeset/filecell-error-slot-5431.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/fields': minor ---- - -**API addition (public-surface widening):** `FileCell` — the compact upload -control `@object-ui/fields` exports for line-item grid cells — gains the -published optional `error?: string` slot, mirroring `LookupField` and -`FileField`: the same validation slot `@objectstack/spec/ui`'s -`FieldWidgetPropsSchema` declares and `FieldWidgetComponentProps` names -(objectui#3222). When set, `FileCell` puts `aria-invalid` on its own focusable -picker button; the message text stays with the host (objectui#5431). - -`GridField` now passes that slot for a required-but-empty `file` cell — the one -cell type objectui#3318's per-cell `aria-invalid` delivery left out. Before -this, a required `file` cell flagged only the visual ring and `title` on the -`td`; no element in the cell subtree announced the state, so assistive tech was -told nothing (a wrapper-only mark is exactly what objectui#5223 forbids). Text, -number, select, and lookup cells were wired in PR #5429; `file` cells now -behave identically. diff --git a/.changeset/fix-parity-aria-line-ref.md b/.changeset/fix-parity-aria-line-ref.md deleted file mode 100644 index 9197bb525c..0000000000 --- a/.changeset/fix-parity-aria-line-ref.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change to the parity gate's `aria` exemption comment; no published behaviour changes. Fixes a stale `file:line` reference to `packages/plugin-detail/src/index.tsx` (was `:335-337`, now `:554-556`) in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`. diff --git a/.changeset/flow-entry-condition-builder.md b/.changeset/flow-entry-condition-builder.md deleted file mode 100644 index 52c43a38dc..0000000000 --- a/.changeset/flow-entry-condition-builder.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -flow designer: the Start node's **Entry condition** now opens in the row-based -condition builder, with raw CEL one click away as the escape hatch (objectui#6226). - -The builder is given the trigger record's own vocabulary — bare field names and -`previous.<field>`, resolved by `flow-scope.ts` — rather than the record-scoped -default its five other consumers use, so the subjects it offers are the spelling -this surface evaluates and teaches. 15 of the 17 entry conditions shipped in the -example apps now open as structured rows; the remaining two mix `&&` with a -parenthesised `||` group, which is a grammar limit of the row model and keeps -them on raw mode by design. - -Compiled output is unchanged: the same CEL the runtime already evaluates, in the -author's own quoting. The legacy `criteria` key, a schedule/manual/webhook -trigger (which binds no record), and every other `expression`-kind flow field -keep the single-line input they render today — the builder is opt-in per field -descriptor and requires a declared vocabulary, never inferred. diff --git a/.changeset/flow-resume-result-5417.md b/.changeset/flow-resume-result-5417.md deleted file mode 100644 index 5de2804350..0000000000 --- a/.changeset/flow-resume-result-5417.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -A screen flow's resume result reaches the user — on both outcomes (objectui#5417). - -A dogfood walkthrough reported that a refused `resume` and a successful one -"render identically: the dialog closes and the page is unchanged", leaving no -gesture that distinguishes "created" from "rejected". Re-measured against `main` -before any change, one half of that was already fixed — `interpretFlowResponse` -reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its -prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors -after the version the report was measured on. There was no interpreter bug and -no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were -real, and they are what changed: - -- **A terminal failure no longer closes the dialog.** The reason it closed is - unchanged and is not reversed: on a `FLOW_FAILED` the engine has already - consumed the suspension, so a resubmit can only reach "No suspended run" and - must not be offered. Closing was one way to withhold that dead retry and the - expensive one — the user had just typed a form they could no longer see, and - the engine's sentence names a value that left the screen with it. The dialog - now stays open with the submit affordance withdrawn: the flat footer swaps - Submit for Close, and an `object-form` step drops its Save (which also stops a - second click from duplicating the record it had already persisted). -- **The refusal has a second, non-expiring carrier.** The toast stays — it is - viewport-fixed, so it still reaches a user scrolled past a tall step's header - — and an inline destructive `Alert` (`role="alert"`) now holds the same - sentence inside the dialog, beside the values that produced it. A retryable - refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, - and its banner clears as soon as the user starts editing. -- **A successful run invalidates what the flow WROTE, not just what the user is - looking at.** Both hosts answered `onComplete` with - `notifyDataChanged({ objectName: <this page's object> })`, so a flow that - created a quote from an Opportunity page never told the related list that - would now contain it — the record did not appear until a manual reload. The - runner cannot know which objects a flow touched, so it emits - `{ objectName: '*' }`: the same scope, for the same stated reason, that the - record page's manual ⟳ already uses. Everything mounted refetches in place - over the invalidation bus, with no remount. - -The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded -English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, -`nextStep`, `completed`) in all ten packs, plus reuse of -`common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The -server's own refusal sentence is still passed through untranslated — it is prose -the automation engine composed for a human, not copy with a key. diff --git a/.changeset/form-control-renderers-dom-props-5632.md b/.changeset/form-control-renderers-dom-props-5632.md deleted file mode 100644 index c13febb1a2..0000000000 --- a/.changeset/form-control-renderers-dom-props-5632.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/components': patch ---- - -**Behaviour change:** the eighteen renderers whose host element is a form -control no longer forward their whole prop bag to it. They route it through a -form-control DOM declaration — the same `pickDomProps` mechanism `grid`, `flex`, -`stack`, `container` and `text` were converged on — so an authored schema key -becomes an HTML attribute only if the contract declares it one (objectui#5632, -the `BARE_SPREAD_MINUS_NAME` slice of objectui#5574). - -The eighteen: `action:button`, `action:icon`, `ui:button`, `ui:checkbox`, -`ui:combobox`, `ui:date-picker`, `ui:email`, `ui:file-upload`, `ui:input`, -`ui:input-otp`, `ui:password`, `ui:radio-group`, `ui:sidebar-menu-button`, -`ui:slider`, `ui:sonner`, `ui:switch`, `ui:textarea`, `ui:toggle`. - -What this stops reaching the DOM: the renderer's own declared props, consumed -off `schema` to render the control AND spread onto the element a second time as -attributes HTML does not define, plus the authored node's SDUI metadata and the -flattened `props` container. Measured across `examples/schema-catalog`, rendered -through the real `SchemaRenderer`: 284 illegitimate attributes over 287 -form-control nodes — `button[label]` 140, `button[icon]` 25, `input[inputtype]` -23, `input[label]` 19, `toggle[label]` 14, `radio-group[options]` 8, -`date-picker[placeholder]` 7, `file-upload[label]` 7, `file-upload[buttontext]` -7, `checkbox[label]` 6, `textarea[label]` 6, `toggle[arialabel]` 6, -`switch[label]` 4, `password[label]` 3, `email[label]` 2, `radio-group[direction]` -2, and six singletons. The same probe reads 0 after, with `grid`'s 26 nodes at 0 -both times as the control and an unchanged node census across the two runs. - -`name` and `disabled` are DELIBERATELY still forwarded. Both are legal on a form -control and neither was ever part of the leak — the ledgered shape for this -group is thirteen attributes, not fourteen, precisely because HTML defines -`name` on these hosts. The whitelist this group uses is therefore the shared -SDUI one PLUS those two, not the bare `toDomProps` the container renderers take: -that would have stripped the form-serialization key off every control and -silently re-enabled every disabled one, and no gate in the repo would have gone -red for it. - -Nothing an author writes renders differently: every leaked key was already being -read off `schema` and applied. `id`, `role`, `tabIndex`, `className`, `style`, -event handlers and the open `data-*` / `aria-*` families are unchanged. - -Anything that read one of the leaked attributes off the DOM — a CSS attribute -selector such as `[label="Save"]`, or a test asserting `inputtype` on a rendered -`ui:input` — must read the schema instead. No `@object-ui` code did; this is -called out because the attributes were externally visible while they lasted. diff --git a/.changeset/form-designer-namespaced-field-type-4838.md b/.changeset/form-designer-namespaced-field-type-4838.md deleted file mode 100644 index 88b03f1e62..0000000000 --- a/.changeset/form-designer-namespaced-field-type-4838.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's form designer canvas now emits the namespaced `FormField.type` spelling -instead of passing `objectDef.fields[x].type` through raw (objectui#4838). - -Per the maintainer ruling on that card, a bare spec-type name (`markdown`, -`html`, `richtext`, …) is **not** a legal `FormField.type` — one widget, one -legal spelling, the namespaced widget id. `ObjectFormDesigner` was the measured -producer of the bare spelling: it handed a raw object-metadata type straight to -`isWideFieldType`, a helper whose vocabulary is `FormField.type`. It now -normalizes through `mapFieldTypeToFormType`, the one place that widget decision -is made. - -User-visible effect: a `repeater` field is finally laid out full-row on the -canvas, matching the runtime form. `repeater` is a spec `FieldType` that -resolves to the wide `field:grid` widget, but bare `repeater` is not one of -`WIDE_FIELD_TYPES`' bare members, so the raw pass matched nothing — the canvas -showed it at normal width while the real form spanned it. Fields whose spec name -doubles as a widget id (`textarea`, `markdown`, `html`, `richtext`, `grid`) are -unaffected; they matched under both spellings. - -The two tolerant consumers this makes look redundant are deliberately left -alone, each scheduled under its own follow-up with deprecation care: the -`field:`-prefix fallback in `renderFieldComponent`, and `WIDE_FIELD_TYPES`' dual -spellings. Removing a tolerance is the consumer-tightening half, and other -producers have not been normalized yet. diff --git a/.changeset/form-field-predicate-current-user-6010.md b/.changeset/form-field-predicate-current-user-6010.md deleted file mode 100644 index 6946763e4b..0000000000 --- a/.changeset/form-field-predicate-current-user-6010.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/components': minor ---- - -**Behaviour change.** Form section and field `visibleWhen` predicates that silently failed -OPEN now actually evaluate — and a rule that resolves FALSE now hides the field -(objectui#6010). - -`current_user` was bound on two of the three `visibleWhen` surfaces and not the third. A -page component or app/nav gate got it (`ExpressionProvider` → `SchemaRenderer`), and a -per-option gate got it (`resolveCascadingOptions(…, predicateScope)`), but every -`resolveFieldRuleState` call in the form renderer passed `undefined` for the scope -argument, so a form SECTION or FIELD predicate saw `record` and `previous` and nothing -else. `'sales_manager' in current_user.positions` therefore named an **unbound root** -there, and the visibility fallback is fail-open — so the gate did not hide the field from -the people it named, it **showed the field to everyone**, with no signal beyond one -deduped `console.warn`. - -**What changes for you, in the direction that matters:** if you authored a form-field or -form-section `visibleWhen` naming `current_user`, saw the field render, and concluded the -rule was permissive — it was not permissive, it was broken, and it is now enforced. That -same field will now **hide** for every user the predicate resolves FALSE for. Audit any -`visibleWhen` on a form field or section that references `current_user` / `user` / -`ctx.user` / `os.user` before upgrading; a predicate that was quietly inert becomes live. - -Two things deliberately do **not** change: - -- **A genuinely unbound root still fails open.** A predicate the engine cannot evaluate at - all still logs one warning and leaves the element visible. Only *evaluated-and-false* - hides. `visibleWhen` remains presentation, not access control — use field-level security - or RLS to stop someone reading something. -- **The deprecated `visibleOn` alias** on a form field now binds the same scope, because - ADR-0089 D2 folds it into `visibleWhen` at parse; binding one scope for the canonical - spelling and another for its alias would have reproduced the same defect one spelling - over. The synthesised legacy `condition: { field, equals }` predicate is unaffected — it - is generated from a structured object and can only ever name `record.<field>`. - -This restores the contract both ADRs already declared: ADR-0068 D1 — *"a predicate authored -against any one form evaluates identically"* — and ADR-0089 D1 — *"runtime record surfaces -bind `record` + `current_user`"*. All five surfaces are now pinned against one authored -predicate text in -`packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx`, -so the next divergence is loud instead of silent. diff --git a/.changeset/form-reset-notification-channels-5235.md b/.changeset/form-reset-notification-channels-5235.md deleted file mode 100644 index 6a2d2c3f14..0000000000 --- a/.changeset/form-reset-notification-channels-5235.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The form renderer now keeps a `defaultValues` reset off `onChange` and off the -`form_change` `onAction` for **every** caller — including one that memoizes the -callback (objectui#5235). - -"A record landing is not a user edit" was already this file's documented, -pinned behaviour, but two of the three channels delivered it by accident of -React's effect ordering: every layout DESTROY runs before any layout CREATE, so -a caller passing a fresh callback each render had its value subscription torn -down before the reset and re-established after. The guarantee was therefore -delivered by the callback's *identity changing*. Wrap the same callback in -`React.useCallback` — taught everywhere as a semantically neutral performance -optimization — and the identity stays put, the effect never re-runs, the -subscription survives the reset, and the whole loaded record comes back to the -host as if the user had typed it: the false "the user edited this" signal -objectui#2968 was filed about, in a form no type, doc or call site warned about. - -The reset now states what those two channels report, the way `onDirtyChange` -already did (it computes its payload against the freshly installed baseline and -calls the host outright). Callers passing inline arrows see byte-identical -behaviour; callers who memoize stop receiving a phantom edit. - -Not a contract change: whether a value channel *should* report a programmatic -reset stays open in objectui#5235. This only removes the answer's dependence on -caller identity. diff --git a/.changeset/formatpercent-comment-citations-4596.md b/.changeset/formatpercent-comment-citations-4596.md deleted file mode 100644 index 6e68a460df..0000000000 --- a/.changeset/formatpercent-comment-citations-4596.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Comments only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared (objectui#4596). - -Why the empty form is the right declaration here: both changed comments are **internal -reasoning**, not consumer-visible API documentation. One sits inside `formatMeasure`'s -body; the other is a test-file header. No released behaviour changes, and no declaration -changes — `dist/utils/dataset-format.d.ts` is byte-identical across this change -(measured: sha `be5f5938…`, 10,580 bytes on both sides), so nothing a consumer types -against or reads on hover moves. - -Stated honestly, because an earlier draft of this note got it wrong: the shipped -JavaScript **does** change. `tsconfig.base.json` sets `removeComments: false` -deliberately and `@object-ui/core` builds with a plain `tsc`, so a body comment is -emitted — `dist/utils/dataset-format.js` goes from 14,716 to 15,457 bytes, and it is the -only one of the package's 180 dist files that moves. The test file is excluded from the -build program (`src/**/__tests__/**`) and never reaches `dist` at all. Bytes moving is -not the criterion; released behaviour and consumer-visible surface are, and neither does. - -Three comments cited `formatPercent` as the live example of the divide-by-100 percent -route. Each was accurate when written and stopped being true when objectui#4590 landed: -`formatPercent` renders through `style: 'percentPoints'` with no division. The comments -now argue the route on its own merits without the expired citation, and record what -replaced it — no caller in this repo takes the divide-by-100 route today. `formatPercent` -was the last one; the two remaining `style: 'percent'` sites hand `Intl` a FRACTION, which -is that style's own contract, and the route otherwise survives only where a test builds it -in order to show it disagreeing. - -The measured argument underneath is unchanged, and the tie / extreme-magnitude pins that -keep the percentage-points route honest are untouched. The `27,581 of 1,200,013` figure -now names the grid it came from — objectui#4576's tie-dense grid, 0.005 steps to 2,000, -precisions 0/1/2, on `formatMeasure`'s call shape — so it stops reading as a discrepancy -against objectui#4590's `27,577 of 1,200,003`, which is the same grid re-measured through -`formatPercent`. diff --git a/.changeset/formfieldspec-dependson-5040.md b/.changeset/formfieldspec-dependson-5040.md deleted file mode 100644 index 183a8fc65c..0000000000 --- a/.changeset/formfieldspec-dependson-5040.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -metadata-admin: `FormFieldSpec` declares `dependsOn`, and the widget half reads -the same declaration instead of its own copy of it (objectui#5040) - -`FormFieldSpec` — the authoring type for a metadata-admin form layout, the -element type of `FormSectionSpec.fields[]` — did not declare `dependsOn`. -`widgets.tsx` held a second, inline description of the same object as -`WidgetProps.fieldSpec`, and that one did, because two registered widgets read -it as their primary configuration: `field-selector` resolves -`dependsOn || reference || 'objectName'` to decide whose field catalog to -offer, and `dynamic-config` uses it to pick a sub-schema out of -`WidgetContext.dynamicSchemas`. One value travelling down one channel, -described twice, disagreeing on the one key that decides what those widgets -show — so - -```ts -{ field: 'fields', widget: 'field-selector', dependsOn: 'objectName' } -``` - -the only configuration that makes `field-selector` work, was a `TS2353` for -anyone who typed their spec. It survived because in-repo specs reach the form -through `as any` / loose types, so the authoring type was never asked. - -No runtime behaviour changes: `MetadataField` already handed `dependsOn` -through and both widgets already read it. What changes is the type face — it -now admits what the runtime has always accepted. The two descriptions are one -declaration, extracted to a leaf module -(`views/metadata-admin/form-spec.ts`) that both halves import, because -`SchemaForm.tsx` imports `./widgets.js` and a back-edge would close a cycle. -`SchemaForm.tsx` re-exports `FormFieldSpec` and `VisibilityPredicate`, so every -existing importer is unaffected. - -`dependsOn` is `string | string[]` here, deliberately **not** -`@object-ui/types`' wider canonical `DependsOnInput`, which also admits -`{ field, param }` objects: both readers index `[0]` and use the result as a -field name, so the wider shape would be a type that lies. Converging the two is -its own decision, pinned as a refusal rather than taken silently. diff --git a/.changeset/formpage-conditional-rule-surfaces-5627.md b/.changeset/formpage-conditional-rule-surfaces-5627.md deleted file mode 100644 index 6f9028e014..0000000000 --- a/.changeset/formpage-conditional-rule-surfaces-5627.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`FormPage` — the console's own form renderer, serving both the public `/f/:slug` -route and the internal `/forms/:name` one — now honours the three conditional-rule -surfaces it still dropped after objectui#5594: section-level `visibleWhen` / -`visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` / -`requiredWhen` (objectui#5627). - -This is the second form renderer in the repo, and it honoured exactly one of the -four surfaces the sibling chain does. A section an author conditioned away rendered -in full — heading and every control — on both routes including the anonymous one. -The object-level half was worse than fail-open hiding: `readonly` was whatever the -static flag said, so a field a `readonlyWhen` should have locked stayed editable and -paired with the server's fail-closed unbound-scope behaviour into "the user edits, -the save reports success, and the value never lands". - -Both halves evaluate through the SHARED machinery rather than a fourth consumer-side -copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three -field rules — which brings the settled rulings with it, including the `serverOwnedValue` -carve-out that keeps a create form from requiring a producer-owned control (#4069 / -#4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first -`visibleWhen ?? visibleOn` read every sibling reader spells. - -Visibility stays a RENDERING rule at both granularities: a hidden section's fields -still submit their values, exactly as a hidden field's have since #5594. diff --git a/.changeset/formpage-maxlength-override-5595.md b/.changeset/formpage-maxlength-override-5595.md deleted file mode 100644 index 337dd53ada..0000000000 --- a/.changeset/formpage-maxlength-override-5595.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`buildSections` now honours a FormView field's `maxLength` override instead of always -taking the object's ceiling (objectui#5595). - -The function merges a form's field overrides with the target object's field definitions, -and its own docstring states the rule: *"Field-level FormField overrides take precedence -over object defaults."* Every key in the loop is built that way — `override.label ?? -def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder` -— except one, which read `def.maxLength` unconditionally. So an author who set a tighter -per-form limit (a short public intake form over a column whose object-level ceiling is -generous) got the generous one. - -The failure was silent in the worst direction: no diagnostic, no warning, and the form -still submits, so the symptom is a value the author believed the input refused being -accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two -`maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"` -arm. - -`override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys, -so an explicitly declared `0` stays a value the author wrote rather than falling through -to the column's ceiling. This narrows only what the input allows; the object's storage -ceiling still decides at submit time, so nothing that was accepted before is now rejected -anywhere but at the keyboard. - -Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all -until objectui#5542, so no one typing a spec in this app could write the override in the -first place, and the inert merge branch was never exercised. #5542 converged that type -onto the shared app-shell declaration, which does declare the key — making the gap -expressible, and therefore findable. - -The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in -`FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than -assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered -evidence for this fix, and it is what made the gap findable in the first place, so it -keeps its place and names the honoured answer. diff --git a/.changeset/formpage-record-id-param-comment-4319.md b/.changeset/formpage-record-id-param-comment-4319.md deleted file mode 100644 index d20c6fa828..0000000000 --- a/.changeset/formpage-record-id-param-comment-4319.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Comment-only repair in `apps/console`'s `FormPage`: the `FORM_RECORD_ID_PARAM` -docblock no longer borrows `createdRecordPath.ts` as a second witness for -`@object-ui/app-shell`'s root-barrel unreachability. That sibling case was -resolved when the host-app resolver was published from the package root, so a -reader following the cross-reference found the opposite of what it promised. -The surviving justification is unchanged and still true — the root barrel does -not re-export `./urlParams` — and now states that fact directly. No published -behaviour changes. diff --git a/.changeset/gantt-navconfig-comment-once.md b/.changeset/gantt-navconfig-comment-once.md deleted file mode 100644 index 0390f51d96..0000000000 --- a/.changeset/gantt-navconfig-comment-once.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -`ObjectGantt` internal cleanup, measured as a zero-pixel change: - -- The comment block above the `navConfig` default repeated its own last two - lines verbatim, leaving a mid-sentence fragment. The sentence is now stated - once, in full. -- The drawer default no longer spells the spec-deprecated `width` - (`@deprecated [#2578 -> size]`). It is now `{ mode: 'drawer' }`, so - `resolveOverlayWidth` returns `undefined` and `RecordDetailDrawer`'s own - `width` default supplies the identical `min(960px, 60vw)`. The resolved - overlay width is unchanged on every viewport, and is now pinned by a test. - -No published behaviour changes. diff --git a/.changeset/gantt-owns-data-comment-7222.md b/.changeset/gantt-owns-data-comment-7222.md deleted file mode 100644 index d45e927904..0000000000 --- a/.changeset/gantt-owns-data-comment-7222.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Comment-only correction in `@object-ui/plugin-list` (objectui#7222): the note above `ganttOwnsData` in `ListView.tsx` claimed the rows `data` prop "short-circuits the renderer's own fetch". It does not — no host prop reaches `ObjectGantt` at all, because the registered `object-gantt` renderer forwards nothing but `schema`. No published behaviour changes. diff --git a/.changeset/gantt-restated-keep-rationale.md b/.changeset/gantt-restated-keep-rationale.md deleted file mode 100644 index 671cfb83ba..0000000000 --- a/.changeset/gantt-restated-keep-rationale.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Comment-only change in `@object-ui/plugin-gantt`: no published behaviour, no -declaration and no type changes. `GanttConfigRestated`'s docblock in -`ObjectGantt.tsx` said its members are kept for ONE reason — their JSDoc being -the only prose in this repo for what the renderer does with each key. Measured -on `main` that was false in two ways: four of the twelve members carry no JSDoc -there (`parentField`, `baselineEndField`, `assigneeField`, `effortField`), and -`ObjectGanttSchema` in `@object-ui/types` has documented all twelve since -objectui#6472. The docblock now states both keep-reasons — the prose, and being -an operand of `ObjectGantt.configPin.test.ts` — so the deletability test a -reader applies to `parentField` arrives at "keep". diff --git a/.changeset/gantt-row-walk-nontotal-reads.md b/.changeset/gantt-row-walk-nontotal-reads.md deleted file mode 100644 index 17a9e7786b..0000000000 --- a/.changeset/gantt-row-walk-nontotal-reads.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Documentation and pinned tests only; no published behaviour changes. `plugin-timeline`'s gantt row walk now carries the measured enumeration of its non-total reads — six in `findUnusableGanttDate`, three in `calculateDateRange` — in place of the prose count that was short, and the adversarial set exercises both classes instead of asserting them. diff --git a/.changeset/gantt-viewmode-declared-both-branches-5074.md b/.changeset/gantt-viewmode-declared-both-branches-5074.md deleted file mode 100644 index 650da3c57f..0000000000 --- a/.changeset/gantt-viewmode-declared-both-branches-5074.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -**`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both -gantt renderer branches honour it** (objectui#5074, maintainer ruling -2026-08-19: declare-and-wire; the spec half landed first upstream). - -- `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, - DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` - enum by reference, so the member list cannot drift. Deliberately no - default: an omitted `viewMode` keeps letting a persisted layout - (`persistLayoutKey`) seed the timeline granularity before the renderer's - `'day'` fallback. -- The timeline branch (`GanttView`) now receives an authored `viewMode`. - Previously only the resource-workload branch (`resourceView` + - `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt - view was silently ignored. -- The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both - branches read the declared `ganttConfig.viewMode`, which also honours the - key when authored inside the spec's `gantt` config block. -- Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value - (e.g. `viewMode: 'hour'`) becomes a zod validation error where it - previously passed through unvalidated. Values on the published spec enum - are unaffected. diff --git a/.changeset/gate-listlocalinstalls-marketplace-5620.md b/.changeset/gate-listlocalinstalls-marketplace-5620.md deleted file mode 100644 index cb6c4165c2..0000000000 --- a/.changeset/gate-listlocalinstalls-marketplace-5620.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MarketplacePackagePage`'s third fetch effect — `listLocalInstalls` — is now gated on -`marketplaceEnabled` and `isAdmin`, the same two predicates the page's other two fetch -effects already check, in addition to (not instead of) its existing -`features.installLocal` check (objectui#5620). - -`listLocalInstalls`'s only consumer is `localInstalls.find(...)` in the content branch, -which is unreachable whenever the page has already returned `MarketplaceDisabled` (no -marketplace on this runtime) or `MarketplaceAccessDenied` (a refused viewer) — both -decided ahead of the content branch since objectui#5533 and objectui#5583. Before this -fix, a runtime with `features.installLocal: true` still fired the request — and -discarded its answer — on a marketplace-off runtime and for a non-admin, the same -wasted-round-trip class objectui#5533 established the fix for on this page, on the flag -that card was not about. - -`features.installLocal` remains its own axis: a runtime can mount a local kernel install -path with no marketplace proxy at all, so the fix adds the two predicates as a -conjunction rather than replacing the existing check. The request still fires exactly -when it did before AND the viewer would actually see its answer. diff --git a/.changeset/governed-surface-human-merge-5149.md b/.changeset/governed-surface-human-merge-5149.md deleted file mode 100644 index 7d63234c48..0000000000 --- a/.changeset/governed-surface-human-merge-5149.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Instruction files only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. `AGENTS.md` records the governed surface -(`AGENTS.md`, `CLAUDE.md`, `.claude/**`, `docs/adr/**`) and the rule that agent seats -leave a PR touching it in draft for a human merge. diff --git a/.changeset/grid-column-spelling-docs-5352.md b/.changeset/grid-column-spelling-docs-5352.md deleted file mode 100644 index d80cdfb618..0000000000 --- a/.changeset/grid-column-spelling-docs-5352.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Docs and skills only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. No package `src/` is touched, so no -`@object-ui/*` package changes behaviour and there is nothing here for a consumer -to upgrade to. - -Corrects the two published corpora that taught `object-grid` columns in a `name` -spelling `ObjectGrid` does not read. `ListColumnSchema` (`@objectstack/spec/ui`) is a -strict object whose column-identity key is `field`; `{ "name": ... }` is refused by -name (`unrecognized_keys: ["name"]`) and, at runtime, contributes no column. - -- `content/docs/api/schema-reference.md` — the `ObjectGridSchema` example authored a - MIXED array (four bare strings followed by one column object). Two defects in one - array: the object entry spelled `name`, and mixing forms is itself unsupported — - `normalizeColumns` dispatches the whole array on `columns[0]`, so a column object - standing behind a bare string is dropped whatever it spells. Renaming the key alone - does not fix it; the example is now uniformly `ListColumn` objects. The `columns` - row of the property table now names `field` and states the no-mixing rule. -- `skills/objectui/guides/page-builder.md` — the grid example's three columns were - all-object in the `name` spelling, so the grid rendered its row-number column and no - data columns at all. Now spelled `field`. A note was added between the grid and form - examples, which sit adjacent and mean the OPPOSITE thing by the same pair of words: - `ListColumn.field` names the object field a column shows, while `FormField.name` - names the field a form input writes. That adjacency is the documented cause of this - defect family (`packages/core/src/utils/column-identity.ts`). - -The adjacent `object-form` example is unchanged and was never wrong — `FormField.name` -is that layer's real key. diff --git a/.changeset/grid-default-filters-lowering-4082.md b/.changeset/grid-default-filters-lowering-4082.md deleted file mode 100644 index 1fa03310a2..0000000000 --- a/.changeset/grid-default-filters-lowering-4082.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -`ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of -byte-copying it onto `$filter` (objectui#4082). - -The query assembly already lowered the canonical `filter` key through `toFilterNode` — -the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch -beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the -one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter` -and `plugin-view`'s non-grid fetch both already route the same value through -`toFilterNode` / `mergeFilterNodes`. - -Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is -declared `Record<string, any>` (the MongoDB-style shape) and `isFilterAST` is false for a -plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one -answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. - -`toFilterNode` handles both without new logic: objects route through -`convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot -passes through untouched, so nothing is lowered twice. It also folds an absent or empty -source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` -used to send `$filter: {}`, asking the server a question with no content in a shape it -refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. - -**Grade — this is less dormant than the card assumed.** objectui#4082 was filed -observation-class on "no measured producer", reasoning that `defaultFilters` is not in -`object-grid`'s registered `inputs` so an author writing it only draws a save-gate -warning. That reasoning covers authors, but not the framework: `plugin-view`'s -`ObjectView` writes the slot itself, forwarding an active named view's `filter` as -`defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — -and `plugin-view`'s own README documents `listViews.<name>.filter` as -`[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as -`400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no -`renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, -and `ListView`'s lowering does not cover it. So a schema-registration host — the -documented authoring path — reached the raw assignment whenever an active named view -carried a filter. Not asserted here: a failing request captured against a running -deployment. `app-shell` is unaffected either way; it supplies `renderListView` and -delegates to `ListView`, which lowers. - -Not in scope, and deliberately not done: retiring `defaultFilters`. This is -consumer-side only — the key the schema admits is unchanged, and its precedence behind -the canonical `filter` is unchanged. - -The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the -measurements. diff --git a/.changeset/grid-dom-attribute-whitelist-4787.md b/.changeset/grid-dom-attribute-whitelist-4787.md deleted file mode 100644 index 1dc2d35067..0000000000 --- a/.changeset/grid-dom-attribute-whitelist-4787.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer -land on the rendered `<div>` as invalid HTML attributes (objectui#4787). - -`grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and -`style`, so everything else `SchemaRenderer` hands a registered component reached the -element. Measured on a canary node, eight attributes leaked: -`columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, -`props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an -unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as -`columns="[object Object]"`. Layout was unaffected, so every catalog grid example -rendered with them — the reason this went unnoticed. - -The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist -objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the -SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, -`role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the -designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be -forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly -any more, including keys `GridSchema` does not have yet; enumerating today's keys to -strip would have re-rotted on the next schema addition. - -No authored input changes and no layout changes: the grid's own vocabulary was always -read off `schema`, never off these props. diff --git a/.changeset/grid-prefix-read-no-cast-6458.md b/.changeset/grid-prefix-read-no-cast-6458.md deleted file mode 100644 index d40529d32e..0000000000 --- a/.changeset/grid-prefix-read-no-cast-6458.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Internal type hygiene in `ObjectGrid`'s column resolver: the authored `prefix` -column key is now read directly instead of through `(col as any)`, since -`ListColumn` declares it. No published behaviour changes — the cast erased at -runtime, so the same property was read before and after; what changes is that -`ColumnPrefix`'s typing now reaches the prefix cell renderer, and a new pin -(`columnReadBoundary-6458.test.ts`) refuses any future cast read of a key the -spec's `ListColumnSchema` declares. diff --git a/.changeset/grid-select-published-fields.md b/.changeset/grid-select-published-fields.md deleted file mode 100644 index 932e1c1a56..0000000000 --- a/.changeset/grid-select-published-fields.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio 的记录网格只请求服务端真有的列——「添加字段」不再把整个网格打成「该视图的查询被拒绝」 - -在 Data 支柱点一下「添加字段」,整片网格立刻变成错误态,并建议「清除筛选条件」——而现场根本没有筛选条件。 - -根因是投影的来源错了。`gridColumns` 取的是**草稿**对象的字段名,而这个数组是下游取数的输入;`addField` 只往本地草稿推一个 `field_<N>`。于是列一变就重新取数,`select` 里带着服务端不存在的列,data API 按设计拒绝——它的错误信息还专门解释了为什么不能静默丢弃未知列:那会把窄投影悄悄答成宽投影。 - -修法的边界是**实测**出来的,不是猜的:把字段存成草稿返回 200 且 `state=draft`,紧接着的 `select` 指名它**仍然** 400。物化发生在**发布**时,所以「有没有保存」是错的问题,「服务端有没有」才是——答案在 `layered().effective` 这条基线里。 - -因此新增 `publishedFieldNames`(加载时取自基线),并让 `gridColumns` 只保留其中存在的列。新字段照常在右侧检查器里被选中和配置(那本来就是配置它的地方),发布之后它可查询了,才作为列出现在网格里。 - -过滤放在列数组这一处、而不是取数那一侧,是为了让「网格要什么」只有一个真相源。 - -回归测试 `DataPillar.gridProjection.test.tsx` 断言在**交给对象视图的列数组**上——那个数组就是投影本身;如果只监视取数调用,日后把数组在下游改一手也能过,缺陷就悄悄搬回来了。撤掉这一行过滤,第二条精确转红、第一条仍绿。 diff --git a/.changeset/grid-unresolved-column-diagnostic-5349.md b/.changeset/grid-unresolved-column-diagnostic-5349.md deleted file mode 100644 index 43d1c5a7d1..0000000000 --- a/.changeset/grid-unresolved-column-diagnostic-5349.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -`ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence. - -objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so -`ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer -reads. That was right — the spec refuses `accessorKey` and `header` by name, and the -census found zero authored usages. But it relocated a failure mode instead of removing -it: a column authored in a spelling the renderer does not read contributed nothing, and -nothing said so. No error, no warning, no empty state — the author got a grid with its -row-number column and no data columns, which is a success receipt for a disagreement -between the renderer and the author. - -An authored column that can never resolve now emits one `console.warn` naming the -address rather than the symptom: which block (`object-grid` or the `view:grid` alias), -which object and label, which `columns[i]`, the keys that entry actually carries, and the -rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }` -the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped -it" (the export-format warning), rather than adding a second differently-shaped one. - -Rendering is unchanged in every case: this is additive. The diagnostic reads the -`columns` input and nothing else — it never asks whether the grid found rows, because -`object-grid` legitimately draws them from five different places (a bare `data` array, -`data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and -passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView` -does). All five are pinned by test, in both directions. A `hidden: true` column is -authored intent and is never reported, and so are the arms that legitimately produce no -columns of their own: no `columns` key, an empty `columns` array, and the `string[]` -spelling. - -A throw was rejected: a grid that renders nothing today would become a page that renders -nothing. diff --git a/.changeset/grid-user-actions-collision-5240.md b/.changeset/grid-user-actions-collision-5240.md deleted file mode 100644 index 8f7c79a354..0000000000 --- a/.changeset/grid-user-actions-collision-5240.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -`object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. - -`userActions` names two different blocks. On a **view** it is toolbar policy — -the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, -`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by -name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / -`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is -the only shape `listViewPredicates` can read, since its loop skips every -non-object value. - -`ObjectGrid` read the key view-first when building the `$select` projection -(`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a -perfectly legal toolbar block therefore shadowed the object's CRUD predicates, -the harvest found none, and the predicate's operand left the projection. CEL then -faults on the absent key, fails closed, and the row Edit/Delete button disappears -for everyone with nothing pointing at the projection — objectui#3501's failure, -reached with a success receipt at every step. - -The view-level block is not hypothetical: `SpecBridge.transformListView` copies -it onto the `object-grid` node the renderer receives, and `app-shell`'s -`ObjectView` builds one unconditionally. - -The harvest now reads the resolved object block only. Both `userActions` read -sites carry a comment naming the collision, and -`__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes, -the producer that writes the view one, the harvest's blindness to it, and the -projection that must keep the object's operand with a toolbar block present. - -Toolbar policy itself is untouched — it was never read through this path. diff --git a/.changeset/grid-widget-aria-invalid-3318.md b/.changeset/grid-widget-aria-invalid-3318.md deleted file mode 100644 index e644f0adf7..0000000000 --- a/.changeset/grid-widget-aria-invalid-3318.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -Grid field widget: announce a form-level validation failure to assistive tech. - -A required `grid` submitted while still empty rendered its "is required" message -but marked nothing — every row was a ghost row, and ghost rows were skipped by -the widget's per-cell validity channel. A sighted user saw the red message; a -screen-reader user was told nothing at all. - -The host failure now drives the per-cell channel the widget already owns: when -the `error` slot is set on an empty grid, the ghost entry row's required cells -flag, and the mark sits on each cell's own control rather than on the `td` -wrapper (a `td` is not focusable, and assistive tech reads validity from the -control). Populated grids are unaffected — they already marked their own empty -required cells inline. diff --git a/.changeset/hidden-predicate-widen-7455.md b/.changeset/hidden-predicate-widen-7455.md deleted file mode 100644 index 9db73d7124..0000000000 --- a/.changeset/hidden-predicate-widen-7455.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`BaseSchema.hidden` now declares the predicate string the renderer already evaluates** (objectui#7455, maintainer ruling 2026-09-03). - -`hidden?: boolean` becomes `hidden?: boolean | string`, and the Zod mirror's `z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible` (#4581) and `disabled` (#4580 ruling Q3-A) on both faces. `hidden` was the third key on the same evaluated path and the only one still declared boolean-only. - -This is a **widening**, not a replacement: every boolean `hidden` keeps parsing and keeps type-checking unchanged, and the renderer's behaviour is untouched by this change — `SchemaRenderer`'s `shouldHide` chain already routed this key through `hasDeclaredPredicate` and evaluated it, which is the evidence the widening rests on. What changes is that authors and their tooling can now write `hidden: "${data.status === 'draft'}"` without casting past the declaration, and the Zod mirror stops refusing it (before this, that value failed `safeParse` with `invalid_type` at path `hidden` while the identical string on `visible` parsed). - -`hiddenOn` is unchanged and remains the sibling expression spelling. The CEL envelope object form is still declared on none of `visible` / `hidden` / `disabled`; objectui#7530 rules on all three together. - -Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). diff --git a/.changeset/hollow-view-overlay-hydration-pin-5773.md b/.changeset/hollow-view-overlay-hydration-pin-5773.md deleted file mode 100644 index a389f4ca47..0000000000 --- a/.changeset/hollow-view-overlay-hydration-pin-5773.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file added is -`packages/app-shell/src/views/InterfaceListPage.hollowOverlayHydration.test.tsx`. - -objectui#5773 — pins the intended post-objectui#5233 behaviour at the seam the card -named: `InterfaceListPage`'s hollow-view hydration effect merging a personalization -overlay row it fetched through `listViewOverrides`/`getView` (the same undocked-narrowing -read path `narrowPersonalizationOverlay`'s docblock in `packages/data-objectstack/src/index.ts` -describes). - -Reachability was measured, not reasoned about — constructed through the REAL -`ObjectStackAdapter` write (`updateViewConfig`/`buildPersistedViewBody`, the same seam -`ObjectView.overlayPatchOnly.test.ts` uses) and the REAL `InterfaceListPage` render, not a -hand-written override fixture. Two cases: - -- A hollow ADR-0017 expansion item (system view, no `columns`) plus the CURRENT - (post-#5233) thin overlay row a toolbar toggle writes today: the page renders - `defaultColumnsFromObject`'s defaults — reachable, and the intended behaviour per the - card's disposition (an overlay was never a legitimate source of a view body). -- The same hollow view with a PRE-#5233 fat overlay row (the shape an install that has - not touched this view since before the fix is still carrying, per - `ObjectView.overlayPatchOnly.test.ts`'s own "PRE-FIX" framing): the page renders the - frozen stale columns instead — a control proving the pin above discriminates a real - hydration-effect outcome rather than passing vacuously. - -Confirmed load-bearing by reverse verification: with the hydration effect's guard -short-circuited (`if (true) return;`), the CONTROL case flips from the frozen `["status"]` -to the un-hydrated defaults `["name","status"]` — the assertion depends on the fetch + -merge actually running. No production code changed; `InterfaceListPage.tsx`'s hydration -effect already behaves this way today, this was untested rather than unreachable. diff --git a/.changeset/home-ai-studio-flag-5521.md b/.changeset/home-ai-studio-flag-5521.md deleted file mode 100644 index 411aa0a99d..0000000000 --- a/.changeset/home-ai-studio-flag-5521.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Console Home stops offering the metadata-authoring front door on a deployment -whose own runtime config says authoring is not offered there (objectui#5521). - -The "Build an app" cover card is now withheld when -`GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed -hosted-SaaS shape that card led a plain tenant into the full authoring flow -behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry -holds zero authoring handlers — the entry was offered and the refusal arrived at -submit. The lockdown criterion for that shape is two-part, UI entry hidden AND -API refused; only the backend half was green. - -- The card is **hidden, not dimmed**, because that is the flag's own declared - meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when - false, the SPA hides the AI authoring affordances", and the serving plugin - documents "set false to force-hide the authoring UI". -- `features.marketplace` keeps the different presentation objectui#5504 gave it - — a dimmed card plus a visible localized reason. That flag means a route is - reachable; this one means force-hide. "Start with a template" is untouched: - installing a marketplace package is not AI metadata authoring and answers to - its own flags. -- No reason line is rendered in its place. `home.build.noCapability` says the - *account* lacks "Manage Metadata"; on a runtime with no authoring at all the - surface is absent for everyone, and pointing a viewer at a permission that - would not help them is the misdirection objectui#5557 is about. -- Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()` - already encodes: a runtime predating the flag, or one whose config fetch - failed, keeps the card exactly as visible as before. - -No new authorable config key, no new server surface, and no new copy — the flag -was already being served and already reaches the browser. diff --git a/.changeset/host-dispatch-action-keys-5611.md b/.changeset/host-dispatch-action-keys-5611.md deleted file mode 100644 index 1f43f6e852..0000000000 --- a/.changeset/host-dispatch-action-keys-5611.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/core': patch ---- - -The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's -privileged-override safety copy (objectui#5611). - -`ActionRunner.execute` classifies the object it was HANDED, and a console host -hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes -`overrideNotice` on that dispatch and two param-collection handlers read it — -yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a -key two files read as one "no reader recognizes", and prescribed promoting it to -an explicit field on `ActionDef`. That prescription is the one shape the -2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic -walked an author into a rejected design. A false warning on the product's own -privileged path — the branch that finalises an approval over approvers who have -not acted — is how a dev console gets muted. - -Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to -`actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the -fourth input to that set and the first one that is not an authored-surface -mirror. Measured before and after on the exact dispatch the bar composes: the -warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS` -grew by exactly one member, and an action carrying a real typo alongside it -still warns — naming `targt` only. - -The authored surface does not move. `overrideNotice` is still NOT declared on -`ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal -remains a compile error, and the AST-derived pin over the interface is unchanged. -Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what -an author may write — `actionKeys.pin.test.ts` now pins both halves, including -the new list's exact contents so a second member cannot arrive quietly. diff --git a/.changeset/host-only-viewtypes-tree-icon-5321.md b/.changeset/host-only-viewtypes-tree-icon-5321.md deleted file mode 100644 index 1c922a8bed..0000000000 --- a/.changeset/host-only-viewtypes-tree-icon-5321.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s -view switcher instead of the grid one, and the `tree` / `chart` view types are -recorded as host-composition-only surfaces (objectui#5321). - -`viewSwitcherSchema`'s `iconMap` carried an entry for every view type except -`tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn -with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by -adding a single key — nothing recorded that the map had to be COMPLETE, so the -next missing member went unnoticed. The map is now typed -`Record<ViewType, string>`, which is how `ViewSwitcher`'s own -`DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: -a future `ViewType` member fails `type-check` rather than silently rendering as -a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph -`DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback -stays for host props that carry an unrecognised type. Reached in practice by -the console, whose `CreateViewDialog` offers `tree` among the view types a user -can create. - -No authoring surface changes. `generateViewSchema` renders eight view types -while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of -them, so `tree` and `chart` are selectable only through the component's `views` -prop. The maintainer ruled on 2026-08-20 that both stay recorded as -host-composition-only rather than being added to those unions, following the -objectui#5097 precedent; the record now lives beside that one, with the branch -set derived from a source fence, the authored unions pinned at the type level, -and host reachability measured. diff --git a/.changeset/humanize-label-single-home-5444.md b/.changeset/humanize-label-single-home-5444.md deleted file mode 100644 index 1e4cfdd0ee..0000000000 --- a/.changeset/humanize-label-single-home-5444.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch -'@object-ui/plugin-charts': patch ---- - -The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. - -`humanizeLabel` turns a stored value into a display string when nothing else -resolves it — an option with no declared label, an object name, a chart axis -member. It existed twice, byte for byte: once in `@object-ui/fields` (read by -`plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own -renderers) and once as a deliberate local copy in `plugin-charts`' -`ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on -`@object-ui/fields`". - -Two copies of one convention is a live hazard rather than tidiness: one -dashboard can hold a chart and a grid over the same stored value, so a change -landing on one copy alone would put that value on screen under two spellings at -once. The single implementation now lives in `@object-ui/core` — the shared -ancestor both packages already depend on, so the dependency the copy existed to -avoid is still avoided and no new edge is created, and core takes no React -(objectui#4389: core-canonical logic, plugins consume). Both former sites -re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps -working unchanged. - -**Nothing rendered changes.** The surviving implementation is byte-identical to -both deleted copies, and each former call site is pinned by identity against the -core function — not by a copied output table that someone would have to remember -to edit in two places. - -The core module also writes down, for the first time, why this convention stays -distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), -which additionally splits camelCase: - -``` -input humanizeFieldKey humanizeLabel -needs_analysis Needs Analysis Needs Analysis -NeedsAnalysis Needs Analysis NeedsAnalysis <- differ -unitPrice Unit Price UnitPrice <- differ -BestCase Best Case BestCase <- differ -lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ -``` - -A field KEY is authored in the codebase and carries a machine spelling, so -splitting camelCase recovers words its author meant. A stored VALUE is arbitrary -tenant data, where a mid-token capital is not reliably a word boundary and -splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two -conventions also do not nest — on the last row each leaves alone the separator -the other rewrites. Whether they should ever converge is a separate decision -that would move rendered output in four packages at once; it is deliberately not -made here. diff --git a/.changeset/i18n-global-reset-4514.md b/.changeset/i18n-global-reset-4514.md deleted file mode 100644 index 93f3476d18..0000000000 --- a/.changeset/i18n-global-reset-4514.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Test harness only, no published behaviour change (objectui#4514). - -Mounting an `I18nProvider` in a test file left react-i18next's global -default-instance pointer on that provider's instance, so every later -provider-less render in the same file resolved through it — a failure that -surfaced hundreds of lines away, in a test nobody had touched, and passed when -run alone. `vitest.setup.base.ts` now restores the pointer after every test. - -`I18nProvider` and `useObjectTranslation()` are unchanged: the global fallback -that makes the hook provider-safe stays exactly as designed. The five source -files in this change are all `*.test.*` plus their harness. diff --git a/.changeset/i18n-guide-label-rule-5081.md b/.changeset/i18n-guide-label-rule-5081.md deleted file mode 100644 index 16ec799ad5..0000000000 --- a/.changeset/i18n-guide-label-rule-5081.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Docs + gate ledger only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. - -`skills/objectui/guides/i18n.md` attributed its label rule to `@objectstack/spec` v4 in two -places (`:117`, `:162`) while every manifest here declares `^17.0.0` and `node_modules` -carries `17.0.0` — thirteen majors, on the surface an AGENT reads before it writes a user's -project. The version number was the reported defect; measurement against the installed -package found the rule it was backing to be wrong as well, which is why neither arm of the -original fork (renumber to v17, or drop the qualifier and keep the sentence) was writable: -both would have laundered a v4-era false statement into a current one. Maintainer ruling -2026-08-20, option A: restate the rule per the installed spec, with no version qualifier. - -Measured against `@objectstack/spec` 17.0.0's published dist, `I18nLabelSchema` is a union -of a plain string and an inline locale map whose keys must match -`/^(default|[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*)$/` — a BCP-47 tag, or `default`. The guide now -states both forms, shows an inline-map example on the two keys this repo was measured to -resolve (`card.title` and `button.label`, both through `pickLocalized`), and keeps its -"don't use `{key, defaultValue}`" advice with the real reason: that key-reference vocabulary -was retired in objectstack#5055, the spec rejects the object with its own message, and if one -reaches a renderer anyway `pickLocalized` falls through to the first string value and paints -the raw translation key on screen. - -The `KNOWN_CLAIMS` entry that inventoried the fossil as `stale` is deleted in the same commit -— the downward ratchet in `scripts/__tests__/doc-version-claims.test.ts` ("no entry may -outlive the claim it excuses") turns red otherwise — and that file's header prose, which -restated the now-falsified two-branch fork, is corrected to record what the fork actually -turned out to be. diff --git a/.changeset/i18nlabel-comment-4611.md b/.changeset/i18nlabel-comment-4611.md deleted file mode 100644 index 3a243e944e..0000000000 --- a/.changeset/i18nlabel-comment-4611.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a -plain string (objectui#4611). - -The paragraph explaining why `label` / `options[].label` are inherited rather than -locally overridden justified itself with a claim about `@objectstack/spec` that was -never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects -were dropped in favour of translation files". Measured against the installed GA pin -`@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of -a string and a string-to-string record, and the schema's own doc block states two -authorized forms with "Both are real; neither is deprecated by this schema". Executed -against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted, -`{ key, defaultValue }` rejected. A reader who believed the comment would have taken a -widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one -seat having nearly done exactly that. - -The replacement describes what `I18nLabel` admits and cites the spec's own doc block -rather than restating a zod expression; where today's spelling is named it is scoped as -a measurement against 17.0.0 with its file and line, so it ages as a reading rather than -as a standing fact. The decision itself is unchanged and never depended on the false -premise — `label` flows in by reference through the spec's schema, and a local -`string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds. - -Documentation only, and the release-visible surface is the declaration file: measured -with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`), -108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every -other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha), -because the comment documents an `interface`, which is erased at emit along with its -leading comment. No behaviour changes; hover text and the shipped `.d.ts` do. diff --git a/.changeset/i18nlabel-inverted-pin-5612.md b/.changeset/i18nlabel-inverted-pin-5612.md deleted file mode 100644 index ffa052d0b1..0000000000 --- a/.changeset/i18nlabel-inverted-pin-5612.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch ---- - -The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and -`ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613). - -Both are residue of the same removed local `label` / `options[].label` override. - -The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` -that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion, -`const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed -`I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain -string is assignable under the narrow shape *and* under the wide one, so that assignment -could only ever fail if the plain-string form were removed — the opposite of the event it -was written to catch. The widening had already landed: `@objectstack/spec@17.0.0` -declares `I18nLabelSchema` as a union of a string and a string-to-string record -(`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection -it did not provide, and asserted a false premise in its own name. - -It is retargeted at what actually holds the decision up — not which single form the spec -has, but that **both** authorized forms stay assignable, on the spec type and on the -inherited `ActionParam['label']` and `options[].label`. It now fails when either form is -withdrawn, and deliberately does not fail on a further widening, since inheriting by -reference is exactly what stays correct as the authorized set moves. The comment is -rewritten against the schema's own doc block (two authorized forms, "Both are real; -neither is deprecated by this schema") instead of the false premise. Verified by -construction: against a locally built narrow `type I18nLabel = string` the new assertions -fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both -shapes. - -`ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since -the override was removed, and nothing re-exported it — and the doc paragraph that -recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard. - -No behaviour changes; the release-visible surface is the declaration file. Measured with -the package's real `tsc` build, both legs building from a cleared `dist/` and cleared -composite build info: 108 emitted files on both sides, exactly one differing — -`dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed. -Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged -sha), because the comment documents an `interface`, which is erased at emit along with -its leading comment. The deleted type import contributes no emitted delta at all, and the -rewritten test file is not part of the build. diff --git a/.changeset/inbox-actor-name-retired-5203.md b/.changeset/inbox-actor-name-retired-5203.md deleted file mode 100644 index ebb7ec8a97..0000000000 --- a/.changeset/inbox-actor-name-retired-5203.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Internal only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -Retires `InboxNotification.actor_name` (`packages/app-shell/src/layout/inboxGrouping.ts`), -which was dead at both ends. `mergeInboxRows` -(`packages/app-shell/src/hooks/sharedUserFeeds.ts`) is the single producer of every row -the bell and Home's action centre render and never mapped it; neither consumer read it; -and `sys_inbox_message` declares no actor column for it to have been mapped FROM. It was -the last declared-but-unfilled member of that interface after objectui#5190 removed the -sibling `source_object` / `source_id` pair. - -**No published type surface changes.** `InboxNotification` is not reachable from -`@object-ui/app-shell`'s public entry: neither `src/index.ts` nor `src/layout/index.ts` -re-exports it, the built `dist/index.d.ts` does not name it, and the package `exports` -map offers only `.` and `./styles.css` — no deep subpath an external consumer could -import it through. The type is internal to the package, so removing an optional member -of it is not an externally observable narrowing and nothing user-visible ships. Runtime -behaviour is unchanged in both directions: no code path produced the field and no code -path read it. - -Two pins keep it retired, in opposite directions — a TYPE PIN in -`layout/__tests__/inboxGrouping.test.ts` that fails if the field is re-declared, and a -runtime key-set pin in `hooks/__tests__/sharedInboxFeed.rowShape.test.tsx` that fails if -the producer is ever changed to spread raw `sys_inbox_message` columns through instead -of mapping them field by field. diff --git a/.changeset/inert-lifecycle-path-readout-5768.md b/.changeset/inert-lifecycle-path-readout-5768.md deleted file mode 100644 index e95f072043..0000000000 --- a/.changeset/inert-lifecycle-path-readout-5768.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`record:path` stops looking like a control it cannot be. - -The record page draws the object's lifecycle across the top from the -`stageField` role, and it drew each stage as a filled, shadowed, equal-width -pill — a segmented button group, sitting exactly where a CRM user reaches for -the stage control. Nothing was behind it. Measured in a browser on a shipped -build (HotCRM `crm_quote`, and this is generic record chrome, so every object -that declares a `stageField` has it): the segments were `role="listitem"` with -`cursor: auto` and `tabindex` null, no ancestor `button`/`tab`/`a`, and a full -pointer sequence (pointerdown → mousedown → pointerup → mouseup → click) left -the record's status untouched. Advancing a record needs the edit form or a bulk -action. Users spent two or three clicks on the path before concluding it was -decoration. - -There is no write path to connect it to, and this change does not open one: -this renderer's only channel is `useRecordContext()`, whose value exposes -`data` / `refresh` / `headerSystemActions` / `onToggleFavorite` and no -record-field mutation. Editing runs through `record:details`' -`<InlineEditProvider>` + `<InlineEditSaveBar>` (`dataSource.update(..., -{ ifMatch })`) or an action via `useActionEngine`; neither reaches this -component. - -So the promise is withdrawn rather than honoured. Each stage now renders as a -thin decorative rail segment with its label as plain text beneath it — the -vocabulary app-shell's approval step readout already uses. Gone: the per-stage -filled pill, the shadow, the ring, the bordered chip, the equal-width tap -target. Kept exactly as they were: which stage is current (`aria-current="step"` -and type weight), the travelled/untravelled distinction, the check on completed -stages, and the separated `lost`-terminal group. The accessible semantics did -not move — `role="list"` / `role="listitem"` with no tab stop was already -correct for a readout, and it stays that way. - -Three DOM attributes carry the state that colour used to be the only carrier -of, so the classification is assertable without reading CSS: -`data-stage-state` (`completed` | `current` | `upcoming`), -`data-stage-terminal` (`won` | `lost`), and `data-stage-rail` on the decorative -indicator. - -**Not in this change:** click-to-advance. A stage control that writes -`stageField` through the same permission/validation envelope an edit takes is a -separate feature with its own appetite, and folding it in here was explicitly -ruled out. diff --git a/.changeset/interfacelistpage-drop-derived-marker-title-6343.md b/.changeset/interfacelistpage-drop-derived-marker-title-6343.md deleted file mode 100644 index 02a60cf22f..0000000000 --- a/.changeset/interfacelistpage-drop-derived-marker-title-6343.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Interface-page maps: drop the derived marker-title binding, restoring the -object's own declaration as the authority - -`defaultMapFromObject` bound a `titleField` alongside `locationField`, derived -from the object's display field. That binding was added to route around a forge -in `ObjectMap` — `getMapConfig` filled an absent `titleField` with the literal -`'name'`, and the marker title was a plain `record[titleField]` read, so an -object whose display field was not `name` titled every popup `undefined`. The -forge is gone: `ObjectMap` now resolves marker titles through -`@object-ui/core#getRecordDisplayName`, the same ADR-0079 resolver the kanban, -calendar and gantt renderers already used — which is why none of them binds a -derived title either. - -What the binding did once the forge was gone was invert precedence. It reaches -the resolver as `options.titleField`, i.e. step 0 — ahead of `titleField` on the -object, ahead of the declared `nameField` pointer, and ahead of the legacy -`titleFormat` template. A field name derived by the page could therefore only -ever change the answer by out-ranking something the object itself declared; in -every other case it reproduced, at step 0, the string the resolver already -computes further down its ladder. The deriver now binds `locationField` and -nothing else, exactly like its kanban / calendar / gallery / gantt siblings. - -No authoring surface changes. An author's own `map.titleField` is untouched — it -travels as the view-level `map` block, `ListView` merges it per key, and the -resolver honours it at step 0 by design. Objects that declare nothing resolve to -the same field as before, now via the resolver's own type-aware derivation -rather than a binding forced ahead of it; objects that declare a `titleFormat` -template (or a `titleField`) now have that declaration honoured on the map, as -it already was on every other visualization. diff --git a/.changeset/issue-5373-retire-crud-schema.md b/.changeset/issue-5373-retire-crud-schema.md deleted file mode 100644 index 1643a2efca..0000000000 --- a/.changeset/issue-5373-retire-crud-schema.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor ---- - -Retire `CRUDSchema` and the `type: 'crud'` node spelling (objectui#5373, -maintainer ruling 2026-08-20, route 2) under ADR-0049 enforce-or-remove. - -`crud` had four declaration faces and no registered renderer, for the whole -life of the key: the TS interface (`packages/types/src/crud.ts`), the zod -mirror (`packages/types/src/zod/crud.zod.ts`), a dedicated branch in -`validateSchema` that affirmatively PASSED it, and `CRUDBuilder` in -`@object-ui/core`. A node spelling it painted the OBJUI-001 "Unknown component -type" panel, and `content/docs/api/schema-reference.md` published it as -reference material — so a reader (or an AI author) who copied the page got a -red panel. - -Removed from `@object-ui/types`: the `CRUDSchema` interface and its zod -mirror, the four shapes that existed only to type its keys — `CRUDOperation`, -`CRUDFilter`, `CRUDToolbar`, `CRUDPagination` and their zod mirrors and -`…SchemaType` aliases — and `CRUDSchema` as a member of `CRUDComponentSchema`, -which is what took it off the node union `AnySchema`. `ActionSchema`, -`DetailSchema` and `CRUDDialogSchema` are unchanged and remain the union's -members. - -Removed from `@object-ui/core`: `CRUDBuilder` and the `crud()` factory. - -Authoring `crud` is now REFUSED BY NAME rather than passed or silently -ignored. `validateSchema` returns an `error` with `code: 'RETIRED_TYPE'` on -`schema.type` — at any depth, since it is what `validateChildren` recurses -with — so `assertValidSchema` throws and `isValidSchema` answers `false`. The -message names the migration: `object-grid` for the record table with its -toolbar, filters, pagination and row/batch actions, `object-form` for the -create/edit form, and `detail` for the record view. `api/schema-reference.md` -is rewritten around those shapes. - -Note on blast radius: the repository itself contains zero authored `crud` -nodes and zero registrations of the key (measured on the merge base against -the doc gate's own 659-key registry derivation, which reads `register` and -`registerLazy` alike). That is an IN-REPO zero, not an npm zero — a published -consumer that imported the `CRUDSchema` type, called `crud()` / `CRUDBuilder`, -or authored `type: 'crud'` will see a compile error or a validation error -respectively. Both are the intended, loud replacement for a shape that has -never rendered. diff --git a/.changeset/issue-5389-workspace-admin-positions.md b/.changeset/issue-5389-workspace-admin-positions.md deleted file mode 100644 index 2c12bc2f6d..0000000000 --- a/.changeset/issue-5389-workspace-admin-positions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -Restore platform-admin detection for permission-set-derived administrators. - -`useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace -gating and the "Build an app" CTAs. Its third source read `user.roles`, a key -the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it -to `positions`). An administrator whose adminship comes from the -`admin_full_access` permission set — the single-tenant deployment shape, where -there is no organization member row and the server deliberately no longer -overwrites `user.role` — matched none of the three sources and read as **not an -administrator**: Setup and Studio simply disappeared for them. - -The hook now reads `user.positions[]`, the one spelling the session publishes. -Detection is restored for that path and unchanged everywhere else: an active -member row with an admin role, a stored `user.role` admin scalar, and -preview/no-auth mode all behave exactly as before, and nobody who was not an -administrator becomes one — pinned by four negative cases alongside the -positive one. - -Also corrects the now-stale documentation that described the removed spelling: -the hook's own docblock, the `roles?: string[]` declaration on the client -`AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and -two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the -comment corrections. diff --git a/.changeset/issue-5474-static-table-narrow-types.md b/.changeset/issue-5474-static-table-narrow-types.md deleted file mode 100644 index 11b8b4627f..0000000000 --- a/.changeset/issue-5474-static-table-narrow-types.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -Split the static `table` column type off the rich shared `TableColumn` -(objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = -enforced holds per renderer. - -`TableColumn` is unchanged and remains the rich shape `data-table`, -`CRUDSchema` and detail-view relations honour. The static `table` renderer's -`TableSchema.columns` now declares the new narrow `StaticTableColumn` -(`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the -keys that renderer reads). The eleven keys the static renderer never read are -retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on -`TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / -`filterable` / `resizable` / `editable` / `cell` on its columns. - -Breaking for authored metadata that wrote those keys on a `type: 'table'` -node: they were silently inert before and are now refused loudly — a tsc -error on the interface (`?: never`) and a parse rejection naming the key in -`@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: -nodes that wanted the interactive behaviour move to `type: 'data-table'` -(whose columns keep the rich `TableColumn`); right-aligned columns on the -static table use `cellClassName: 'text-right'`; alternate-row styling uses -Tailwind on `className`. diff --git a/.changeset/issue-5619-workspace-admin-unresolved.md b/.changeset/issue-5619-workspace-admin-unresolved.md deleted file mode 100644 index a6dd79f81b..0000000000 --- a/.changeset/issue-5619-workspace-admin-unresolved.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/auth': minor -'@object-ui/app-shell': minor -'@object-ui/console': minor ---- - -Stop showing an access-denied screen to a real administrator while their -adminship is still resolving. - -`useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not -arrived yet" and "resolved: not an admin" were the same answer. One of its three -sources — the active organization member row — is fetched some round trips after -the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), -so an administrator whose adminship lives only in that row rendered at least -once as a non-admin, and every gate downstream acted on it: the two marketplace -surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and -re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" -replace>` that the later flip could not undo. - -**Breaking (published API, hence `minor` per this repo's version policy):** -`useIsWorkspaceAdmin(): boolean` is replaced by -`useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old -name is removed rather than kept alongside, so a call site that ignores the -third state fails to compile instead of silently refusing an administrator. - - -const isAdmin = useIsWorkspaceAdmin(); - +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); - -`AuthProvider` gains `isMembershipResolved` on its context — the organization / -member pipeline has reached a terminal state — because `organizations`, -`activeOrganization` and `activeMember` read `[]` / `null` / `null` both before -the pipeline starts and after it finds nothing. - -No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, -so an admin the session already identifies through `positions[]` never waits on -the member row. diff --git a/.changeset/issue-5709-unconsumed-widget-option-warning.md b/.changeset/issue-5709-unconsumed-widget-option-warning.md deleted file mode 100644 index c4a5485748..0000000000 --- a/.changeset/issue-5709-unconsumed-widget-option-warning.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/sdui-parser': minor ---- - -`validateTree` now reports a dashboard widget `options` key that no renderer -consumes as a `unconsumed-widget-option` **warning** naming the consumed set -(objectui#5709 ruling). The census behind the accepted set — the spec's five -declared query keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, -`stageOrder`) plus the `description` sub-caption convention key — is -re-measured on every test run against `@objectstack/spec` and the -`plugin-dashboard` renderer sources. The check fires only on dataset-bound -shorthand widgets (the spec-legal form) hosted by `dashboard` / -`dashboard-grid` nodes, and honours the spec's per-widget -`suppressWarnings: ['unconsumed-widget-option']` escape hatch. Warning -severity only: documents keep parsing, saving and rendering. Exported for -other surfaces: `checkDashboardWidgetOptions`, `CONSUMED_WIDGET_OPTION_KEYS`, -`DASHBOARD_WIDGET_HOST_TYPES`, `UNCONSUMED_WIDGET_OPTION`. diff --git a/.changeset/issue-5821-table-column-editable-mirror.md b/.changeset/issue-5821-table-column-editable-mirror.md deleted file mode 100644 index 7f9b967e00..0000000000 --- a/.changeset/issue-5821-table-column-editable-mirror.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Add `editable` to the rich `TableColumnSchema` zod mirror. The `TableColumn` interface declares `editable?: boolean` and `data-table` honours it, but the mirror omitted the key, so a non-strict parse silently stripped it — and since the renderer treats absence as `true`, a column an author locked with `editable: false` came out of validation editable again. Columns locked with `editable: false` now stay locked through any pipeline that parses metadata via `@object-ui/types/zod` (including the CLI `validate` route). diff --git a/.changeset/issue-5881-record-highlights-header-prose.md b/.changeset/issue-5881-record-highlights-header-prose.md deleted file mode 100644 index af3ed5571c..0000000000 --- a/.changeset/issue-5881-record-highlights-header-prose.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only comment change: the module header of -`recordHighlightsInputs.spec-parity.test.ts` no longer narrates -`RecordHighlightsProps` as a strip-mode `z.object`. It now states the -pin-independent verdict and points at the file's own -`specRefusesUnknownTopLevelKeys` probe for which way the installed -`@objectstack/spec` states it. No assertion changed; nothing releases. diff --git a/.changeset/issue-5887-toplevel-strictness-probes.md b/.changeset/issue-5887-toplevel-strictness-probes.md deleted file mode 100644 index 878a97ff50..0000000000 --- a/.changeset/issue-5887-toplevel-strictness-probes.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only. The two `plugin-detail` top-level spec-parity blocks -(`recordDetailsInputs`, `recordRelatedListInputs`) now measure their OWN -schema's strictness against an undeclared top-level key, via a per-file -`specRefusesUnknownTopLevelKeys` probe and a two-arm assertion, instead of -citing the sibling `record:highlights` probe in prose — strictness is per -schema. No published behaviour changes. diff --git a/.changeset/issue-6275-validation-engine-dangling-see.md b/.changeset/issue-6275-validation-engine-dangling-see.md deleted file mode 100644 index 47ffe32e45..0000000000 --- a/.changeset/issue-6275-validation-engine-dangling-see.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Comment-only change in `@object-ui/core`: the `SimpleExpressionEvaluator` doc block -no longer points at the deleted `SECURITY_FIX_SUMMARY.md`, and states the security -rationale inline instead. Releases nothing — the class is not exported, so the doc -block reaches no `.d.ts`, and no behaviour, type or API surface changes. diff --git a/.changeset/issue-6567-hoist-internalformroute-import.md b/.changeset/issue-6567-hoist-internalformroute-import.md deleted file mode 100644 index e8d1e73a62..0000000000 --- a/.changeset/issue-6567-hoist-internalformroute-import.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Test-only: in `apps/console/src/components/FormPage.predicateScope.test.tsx`, hoist -`InternalFormRoute`'s import from a dynamic `await import()` inside the -`hop1SessionPrincipal` case to module scope. That load was costing 10204ms of the -case's 15000ms budget — it is the file's first value request for -`@object-ui/app-shell`, aliased to source — which made the file's one anti-inert -case fail under full-project parallel load. Same module, same binding, loaded -before the timed window instead of inside it. No published behaviour changes. diff --git a/.changeset/jsonc-fence-extractor-test-only.md b/.changeset/jsonc-fence-extractor-test-only.md deleted file mode 100644 index 03c7c1f195..0000000000 --- a/.changeset/jsonc-fence-extractor-test-only.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change in `@object-ui/components`: the skill-guide fence extractor in -`skill-guide-data-table-binding.test.tsx` now reads `jsonc` fences as well as -`json`, so retagging a comment-carrying guide example no longer hides it from -every assertion in that file. No published behaviour changes. diff --git a/.changeset/kanban-calendar-nav-width-converge.md b/.changeset/kanban-calendar-nav-width-converge.md deleted file mode 100644 index 7f827c27d4..0000000000 --- a/.changeset/kanban-calendar-nav-width-converge.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -`ObjectKanban` and `ObjectCalendar` internal cleanup, measured as a zero-pixel -change — the convergence of the two renderers #6305 left behind. - -Each carried the house default `min(960px, 60vw)` at **two** sites: the -`navConfig` default (`{ mode: 'drawer', width: 'min(960px, 60vw)' }`) and a -render-site `width={(navigation.width as any) ?? 'min(960px, 60vw)'}`. Both are -gone. `width` is spec-deprecated (`@deprecated [#2578 -> size]`) and -`resolveOverlayWidth` gives an explicit `width` priority OVER `size`, so -spelling it kept the deprecated branch load-bearing on the path most boards and -calendars take. With both omitted, `resolveOverlayWidth` returns `undefined` and -`RecordDetailDrawer`'s own `width` default supplies the identical -`min(960px, 60vw)`. - -The resolved overlay width is therefore unchanged on every viewport, for both a -board/calendar that declares no `navigation` and one that authors -`navigation.width` — each is now pinned by a test. The three renderers agree -again. - -No published behaviour changes. diff --git a/.changeset/khaki-rocks-shout.md b/.changeset/khaki-rocks-shout.md deleted file mode 100644 index 24665f276f..0000000000 --- a/.changeset/khaki-rocks-shout.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@object-ui/plugin-charts": patch ---- - -Draw every categorical x-axis label on short axes - -A vertical bar chart in a dashboard-width widget dropped most of its x-axis -labels — three bars drew one label, five bars drew two — leaving the bars -unnamed, with no legend to fall back on because a single-series bar chart has -none. - -The x axis applied one tick policy to time and category alike (`preserveStartEnd` -with a 48px `minTickGap`), which is right for hundreds of dates and wrong for a -band axis, where a dropped tick is an identity the reader cannot recover rather -than a sample they can interpolate. It was also keyed to the viewport rather -than the widget, so a 200px chart inside an 800px console was treated as a wide -one. - -Bar, column, line, area and combo charts now draw every label on a categorical -x axis of five buckets or fewer — rotating, and ellipsising an over-long name -rather than clipping it. Longer axes keep the existing measured thinning, and -horizontal bars are unchanged. diff --git a/.changeset/layout-default-jsdoc-7361.md b/.changeset/layout-default-jsdoc-7361.md deleted file mode 100644 index ec7cc25321..0000000000 --- a/.changeset/layout-default-jsdoc-7361.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/types': patch ---- - -The published `@default` documentation on two `layout.ts` members now matches the value the renderer actually applies. `ContainerSchema.maxWidth` documented `'lg'` while `container.tsx` applies `schema.maxWidth ?? 'xl'`, and the shared `FlexLayoutProps.align` documented `'center'` while `flex.tsx` applies `schema.align || 'start'` and `stack.tsx` applies `schema.align || 'stretch'`. The renderers are unchanged — they are the authority for what runs — so only the docblocks moved; `align` now states both consumers in prose instead of carrying a single `@default`, because one member shared by two deliberately divergent component types cannot have one correct default. diff --git a/.changeset/layout-direction-default-jsdoc-7734.md b/.changeset/layout-direction-default-jsdoc-7734.md deleted file mode 100644 index e8943954ec..0000000000 --- a/.changeset/layout-direction-default-jsdoc-7734.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/types': patch ---- - -The published `@default` documentation on `FlexLayoutProps.direction` no longer states a value that only one of its two consumers applies. The member is declared once (objectui#6151) but `flex.tsx` reads `schema.direction || 'row'` while `stack.tsx` reads `schema.direction || 'col'` ("Default to column for Stack"), so the single `@default 'row'` was correct for `flex` and wrong for `stack` — whose own `defaultProps.direction` is `'col'`. The renderers are unchanged — they are the authority for what runs — so only the docblock moved: `direction` now names both consumers in prose, the same remedy objectui#7361 applied to the sibling `align`. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays: the criterion is a DIVERGENT shared member, not a shared one. diff --git a/.changeset/layout-renderers-dom-props-5574.md b/.changeset/layout-renderers-dom-props-5574.md deleted file mode 100644 index b018f67b4b..0000000000 --- a/.changeset/layout-renderers-dom-props-5574.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/components': patch ---- - -**Behaviour change:** the `flex`, `stack`, `container` and `text` renderers no -longer forward their whole prop bag to the host element. They route it through -`toDomProps` — the same whitelist `grid` was converged on — so an authored -schema key becomes an HTML attribute only if the SDUI DOM contract declares it -one (objectui#5574). - -What this stops reaching the DOM: the renderer's own declared props, which were -consumed off `schema` to build the class list AND spread onto the element a -second time as attributes HTML does not define. Measured across -`examples/schema-catalog`, rendered through the real `SchemaRenderer`: 1194 -illegitimate attributes over 1141 nodes — `text[content]` 522, `flex[align]` -198, `flex[gap]` 193, `stack[gap]` 153, `flex[justify]` 98, `container[padding]` -14, `container[maxwidth]` 6, `flex[direction]` 5, `stack[align]` 4, -`text[value]` 1. The same probe reads 0 after, with `grid`'s 26 nodes at 0 both -times as the control. - -Nothing an author writes renders differently: every leaked key was already being -read off `schema` and applied as a class, so the markup loses attributes that -never had meaning and keeps the styling that did. `id`, `role`, `tabIndex`, -`className`, `style`, event handlers and the open `data-*` / `aria-*` families -are unchanged — including `data-obj-id` / `data-obj-type`, which now arrive -through the `data-*` family rather than by hand. - -Anything that read one of the leaked attributes off the DOM — a CSS attribute -selector such as `[gap="4"]`, or a test asserting `align` on a rendered `flex` — -must read the schema or the class instead. No `@object-ui` code did; this is -called out because the attributes were externally visible while they lasted. diff --git a/.changeset/lazy-plugin-jsdoc-compiles.md b/.changeset/lazy-plugin-jsdoc-compiles.md deleted file mode 100644 index e2dd0c0aa0..0000000000 --- a/.changeset/lazy-plugin-jsdoc-compiles.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/react": patch ---- - -Fix `createLazyPlugin`'s JSDoc example, which taught a call that does not compile. - -The `@example` block passed `() => import('@object-ui/plugin-grid')` as the -`importFn`. ObjectUI plugin packages export their components by name and have no -`default`, so that call supplies the module namespace object — rejected by the -compiler (`Property 'default' is missing`) and, at runtime, handed to -`React.lazy` as the component. The examples now unwrap the named export via the -`async` spelling, which is the form that actually type-checks. This ships in the -published `.d.ts`, so it is what editors show on hover. diff --git a/.changeset/list-user-actions-collision-5398.md b/.changeset/list-user-actions-collision-5398.md deleted file mode 100644 index 62a0d3c775..0000000000 --- a/.changeset/list-user-actions-collision-5398.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. - -`userActions` names two different blocks. On a **view** it is toolbar policy — -the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, -`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by -name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / -`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is -the only shape `listViewPredicates` can read, since its loop skips every -non-object value. - -`ListView` read the key view-first when building the `$select` projection -(`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view -carrying a perfectly legal toolbar block therefore shadowed the object's CRUD -predicates, the harvest found none, and the predicate's operand left the -projection. CEL then faults on the absent key, fails closed, and the row -Edit/Delete button disappears for everyone with nothing pointing at the -projection — objectui#3501's failure, reached with a success receipt at every -step. - -This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and -it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level -`userActions` it hands down as an object literal of two spreads, so the left -operand was `{}` at worst — never nullish. The `??` never fell through, and the -object's CRUD predicates were never consumed at all on that path, whether or -not an author wrote any toolbar policy. - -The harvest now reads the object block only. Both `userActions` read sites in -`ListView.tsx` carry a comment naming the collision, and -`__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the -two shapes, a producer that manufactures the view one, the harvest's blindness -to it, and the projection that must keep the object's operand with a toolbar -block — or an empty block — present on the view. - -Toolbar policy itself is untouched — it was never read through this path. diff --git a/.changeset/list-view-nested-aria-label-i18n-5134.md b/.changeset/list-view-nested-aria-label-i18n-5134.md deleted file mode 100644 index f330b77a6a..0000000000 --- a/.changeset/list-view-nested-aria-label-i18n-5134.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`ListView` now resolves the nested `aria.ariaLabel` against the audience's locale -instead of casting it to a string (objectui#5134). - -`@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain -string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only -read site in this repo spread it into the DOM as -`{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a -conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which -a screen reader announces as the list view's accessible name — in every locale. The -read now goes through the spec's own `resolveI18nLabel` (the resolver four other -in-repo read sites already use) against `useDisplayLocale()`. - -Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a -plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label -predates rc.6, and no measured author writes one today — but map values are legitimate -and arrive via API/import, so an imported list view carrying -`aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong -accessible name. This is the map form working as declared, not a defect users are -currently hitting. - -Behaviour on the string arm is byte-identical, including `''` (falsy before and after, -so no attribute). One edge changes for the better: a map that matches no locale used to -render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an -unnamed region beats a garbage-named one. - -The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different -vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by -`SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's -shape. diff --git a/.changeset/listview-calendar-crossfield-pin-7122.md b/.changeset/listview-calendar-crossfield-pin-7122.md deleted file mode 100644 index b642dd5a89..0000000000 --- a/.changeset/listview-calendar-crossfield-pin-7122.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: pins `@objectstack/spec` 17.3.0's list-view cross-field refusal -(offering the `calendar` visualization without a `calendar: { startDateField }` -block) through metadata-admin's real client-validation gate, on the create and -edit doors. No published behaviour changes — `@object-ui/app-shell` ships no new -or altered runtime code here. diff --git a/.changeset/listview-comment-pair-4559.md b/.changeset/listview-comment-pair-4559.md deleted file mode 100644 index 2f012823cd..0000000000 --- a/.changeset/listview-comment-pair-4559.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime -behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does -change, which is why this is a `patch` rather than an empty frontmatter. - -**objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment -block above the `sortFields` memo still called a formula field "the supported -alternative (… which sorts like any text column)". Since objectui#4294 the -`list.sortRelationalHint` string in this same file says the opposite ("Not a formula -field: it is virtual, so no column is stored for it and the server refuses to sort by -one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the -server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical -now names the remedy the hint, the server's refusal and the README already share — a -stored field that denormalizes the name onto this object, written when the source -changes. This was the last copy of the retired advice in the repo. - -**objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.** -It had drifted two declarations up, so the exported `parseSortConfig` carried two -stacked leading comments and the helper carried none. This one was not cosmetic: because -`parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into -`dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort -parser with a sentence about action labels. Moving the block removes it from the `.d.ts`; -`formatActionLabel` is module-private, so its now-correct docblock does not appear there. -It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the -comment block *attached* to a declaration — a misattributed docblock is the mechanism by -which a claim gets scored against the wrong symbol. This block carries no spec-alignment -phrase, so nothing fired today. - -No tests accompany this change and none could: both edits are comment-only, and there is -no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real -`vite build` before and after, not asserted. diff --git a/.changeset/listview-map-view-level-config-5042.md b/.changeset/listview-map-view-level-config-5042.md deleted file mode 100644 index 44ca3ccd04..0000000000 --- a/.changeset/listview-map-view-level-config-5042.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-list': minor -'@object-ui/app-shell': minor ---- - -**Behaviour change:** the spec's view-level `map` block on a list view is now read at -runtime. `ListMapConfigSchema` (objectstack#9340) has been authorable and validated since -the `@objectstack/spec` 17.1.0 pin — it flows into this repo's own `ListViewSchema` by -reference — but nothing consumed it: `ListView`'s `case 'map'` forwarded only the legacy -`schema.options.map` bag, so declaring `map: { titleField: 'title', locationField: -'location' }` on a view changed nothing and marker titles fell back to the renderer's -placeholder. - -The block now reaches `plugin-map` and drives every one of its seven reads — coordinate -extraction, marker title and description, and the initial camera. Precedence follows the -convention the sibling visualization blocks in the same file already set: the view-level -block wins over `options.map`, per key, exactly as `kanban` / `calendar` / `gallery` / -`timeline` / `gantt` each merge their spec config over the legacy bag. Both sources go -through the existing objectui#5177 key whitelist, and the branch still emits the flat -form, so `getMapConfig`'s objectui#5018 precedence rule ("neither flattener emits a `map` -key at all") stays true. - -The visualization switcher had the same gap with a sharper consequence: the capability -gate that decides which visualizations are offered also read `options.map` alone, so a -view binding its coordinates in the spec block was filtered out of its own -`appearance.allowedVisualizations` and fell back to `['grid']`. The gate now asks the same -merged config the render seam forwards, so the two cannot disagree — including for a -binding split across the two sources. - -`InterfaceListPage` (ADR-0047 interface pages) forwards the referenced view's `map` block -for the same reason. It is passed alongside the auto-derived `options.map` rather than -replacing it, so a partial authored block — `map: { titleField: 'title' }` — keeps the -derived coordinate binding instead of dropping it. - -No defaults are introduced for `zoom` / `center`: an undeclared camera stays undeclared, -so the fit-to-queried-records behaviour ruled in objectui#5000 is unchanged. diff --git a/.changeset/lookup-cell-chip-cap.md b/.changeset/lookup-cell-chip-cap.md deleted file mode 100644 index 4f1c77677a..0000000000 --- a/.changeset/lookup-cell-chip-cap.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -A multi-value lookup cell no longer grows its row without bound: `LookupCellRenderer` -now shows at most 3 chips and collapses the rest into one muted `+N` chip, the same -cap `UserCellRenderer` has always applied to its avatar stack in the very same file. - -Previously the array branch rendered EVERY referenced record as its own chip inside a -`flex-wrap` container. In a grid column that wraps to one chip per line, so a cell -referencing a large set — a production 排班计划 row referencing 60+ work objects — grew -a single row to several screens of height and blew the page layout apart. The same -uncapped rendering reached every surface that resolves through `getCellRenderer('lookup')`: -grid, related lists, gallery, kanban, report and dashboard tables, and the record detail -sections. - -The collapsed names stay reachable: the `+N` chip's `title` lists the display names of -the hidden references (resolved through the same option/label/record-name path as the -visible chips), and the record's own detail view remains the place to see the full set. -The first 3 chips keep their per-record links (#4336) and their resolution order — -nothing changes for cells with 3 or fewer references. diff --git a/.changeset/lookup-dropdown-cell-renderer-5492.md b/.changeset/lookup-dropdown-cell-renderer-5492.md deleted file mode 100644 index d43f0e19a2..0000000000 --- a/.changeset/lookup-dropdown-cell-renderer-5492.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -A lookup's inline dropdown renders its columns through the same cell renderer the browse-all picker uses, so one `lookup_columns` declaration cannot produce two answers. - -A form's lookup field offers two ways to pick a related record, and both read -the same declaration: the inline dropdown under the field, and the -"browse all records" picker behind it. The picker resolved every cell through -the type-aware cell renderer. The dropdown did not — it printed -`record[descriptionField]` verbatim into the option subtitle and concatenated -`label: String(rawValue)` into the row's `title` attribute. Measured on the -same declaration, on a real 17.1.0 deployment: - -``` -column inline dropdown (before) browse-all picker -lookup T5MsMCuwP4t_yUHq (bare FK id) the related record's name -date 2026-08-20T00:00:00.000Z (ISO) a formatted date -select pending (enum code) the authored option label -``` - -Both surfaces now call one shared module — `widgets/lookupColumnDisplay.tsx`, -which owns column normalisation, the field-descriptor enrichment from the -referenced object's schema, and the render itself. The picker's own -`renderCellContent` and `columnFieldDescriptors` are now thin calls into it, so -there is a single renderer left to drift from. The dropdown's extra columns are -rendered into the option row itself; the row's `title` keeps the full option -label, which is what a truncated label needs, instead of a raw-value dump. - -No query changed and no contract widened. `lookupColumns` entries stay bare -field names — no dot paths, no populate/expand semantics — because neither -surface's request carries populate to begin with: the picker resolves a -foreign-key id to a name client-side, in the lookup cell renderer, and the -dropdown now inherits exactly that. An unresolved reference therefore renders -what the picker renders for it, and keeps its column: a slot is dropped only -when the record holds no value for the field, decided on the raw value and -never on what the renderer makes of it, so an unresolved id can never degrade -into a silently empty column. diff --git a/.changeset/loud-lamps-shake.md b/.changeset/loud-lamps-shake.md deleted file mode 100644 index eae0d16227..0000000000 --- a/.changeset/loud-lamps-shake.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@object-ui/react": minor -"@object-ui/plugin-detail": minor -"@object-ui/plugin-grid": minor -"@object-ui/plugin-form": minor ---- - -`object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so - -The three object-bound blocks disagreed about how the data-source adapter reached -them. `object-grid` and `object-form` were registered through wrappers that read -it from `SchemaRendererProvider` context; `detail-view` was registered as the raw -component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads -only context, so the two wirings were mutually exclusive: measured with correct -keys in every cell, provider wiring gave the grid `find` 1 and the detail view -`findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. - -All three now resolve the adapter through one rule — an explicit `dataSource` -prop first, the provider context second. This is additive: `detail-view` keeps -its prop form (and direct `<DetailView dataSource={…} />` callers are untouched), -`object-form` gains a prop form it did not have, and `object-grid` no longer -throws `useSchemaContext must be used within a SchemaRendererProvider` when a -page has no provider. - -And the silence is over. A block in this family that resolves no adapter renders -a **No data source resolved** panel naming the block, the object it was about to -read, and the ancestor that injects the adapter — instead of a header-only grid, -a field-less form card, or nothing at all. The check is opt-in per block, so a -placement with inline rows, inline `customFields`, an inline record or an `api` -endpoint is untouched. - -New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, -`noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. diff --git a/.changeset/lucide-icon-record-name-gate.md b/.changeset/lucide-icon-record-name-gate.md deleted file mode 100644 index 2a2f9ca90f..0000000000 --- a/.changeset/lucide-icon-record-name-gate.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Adds `check:icon-record-names`, a repo-level gate asserting that every authored icon -name reaching a resolver that reads lucide's runtime `icons` record is a live key of -that record. lucide retires a spelling by dropping it from that record while keeping -it as a deprecated named export, so a retired name still imports, still type-checks -and still renders as a component while resolving to nothing as a string — the class -behind objectui#5586 and objectui#5622, each of which left a local pin behind. The -gate judges against the record itself rather than any list of retired spellings, and -re-discovers the resolver population on every run. - -No published behaviour changes: the touched `src/` files carry comment updates only, -and the three repaired spellings are in example schemas and the docs playground. diff --git a/.changeset/lucky-buttons-warn.md b/.changeset/lucky-buttons-warn.md deleted file mode 100644 index 739ac7445c..0000000000 --- a/.changeset/lucky-buttons-warn.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:activity` now says out loud when an unrecognised `filterMode` is folded onto `all`. - -`normalizeFilterMode` folds every value it does not recognise onto `'all'` — the widest of the four declared modes — so a near-miss like `comments-only` opened the panel on the unfiltered stream instead of the slice the author asked for, and said nothing. The fallback is kept (a dropdown handed a value with no matching item renders blank), but the fold now emits one deduped diagnostic naming the offending value and the declared modes. Nothing that renders changes. diff --git a/.changeset/lucky-donkeys-repeat.md b/.changeset/lucky-donkeys-repeat.md deleted file mode 100644 index e1a2c2679e..0000000000 --- a/.changeset/lucky-donkeys-repeat.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Tests only — no published behaviour changes. - -objectui#6598 reported that a `kind:'html'` page's `<list-view>` rendered rows but -no data columns, in all eight `columns` spellings the reporter tried. Re-measured -on the merged ref: the symptom no longer reproduces in any of the eight. The -three mechanisms behind it were fixed by objectui#6614 / PR #6669 (the braced -literal subset), PR #6679 (the unauthored-projection handoff) and objectui#6677 -(the grid's default-column derivation). This adds the matrix pin the card itself -was missing — every one of the eight spellings, end to end through the real live -registration and the real object-grid — so no future change can put any of them -back into the reported state. diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md deleted file mode 100644 index 0eb92d73fc..0000000000 --- a/.changeset/lucky-donkeys-shave.md +++ /dev/null @@ -1,94 +0,0 @@ ---- ---- - -Census only (objectui#7642): record, at each of the six sites, which contract types -the field-def bag it reads. No runtime behaviour changes — every snake leg is kept, -so this declares no release. - -The card proposed retiring `display_field` / `description_field` / `lookup_filters` / -`id_field` reads on the ground that `@objectstack/spec`'s `FieldSchema` is strict and -refuses them. Measured against the installed spec, that is true — of the AUTHORING -path. Three findings moved every site to KEEP: - -1. The SERVE path runs no parse. `ObjectStackAdapter.getObjectSchema` returns the - server document verbatim plus exactly two rewrites (`normalizeSchemaReferenceKeys`, - `applyFieldWidgetOverrides`); there is no `ObjectSchema.parse`/`safeParse` on that - path. The `resolveActionParams` site is served by a different path — - `useMetadata().objects`, filled by `client.meta.getItems('object')` in `app-shell`'s - `MetadataProvider` — and that path runs no schema parse either (the provider's only - `parse` is `JSON.parse` of its session cache; the pinned `@objectstack/client`'s - three `safeParse` calls are all event-payload schemas, none on `getItems`). A - stored pre-strict document therefore still delivers these keys to every consumer. - The legs are not unreachable. -2. Five of the six sites have NO camelCase leg. They read the refused spelling and - nothing else, so retiring it does not re-point the read to the declared spelling — - it deletes the only read of the value. The corollary is the real user-facing gap: - on fully spec-compliant metadata those five sites already ignore a configured - `displayField` / `descriptionField` / `lookupFilters` today. `idField` is NOT in - that list: measured on the pinned spec 17.2.0, `FieldSchema` refuses `idField` with - `unrecognized_keys` exactly as it refuses `id_field` — on `FieldSchema` neither - spelling of the id key is declared, so the `id_field` reads have no `FieldSchema` - spelling to gain a leg for and their only route is the ingestion choke point - (objectui#7650, option A). That is a `FieldSchema` statement only: the widget - contract `@object-ui/types` `LookupFieldMetadata` DOES declare `idField` (kept by - PR #7641 as a widget-contract key), and `LookupField` reads it off the same runtime - object finding 3 describes. -3. The object-schema field def and the widget bag are the same object at runtime. - `ObjectForm` builds its fields from `getObjectSchema` and threads each def to the - widget, `@object-ui/fields` `LookupField`. What that widget reads moved while this - census was under review, so this record is dated. At this branch's base (`1ec291c0`, - 2026-09-04) `LookupField` read `display_field` / `description_field` / `id_field` / - `lookup_filters` SNAKE-FIRST, the published `@object-ui/types` `LookupFieldMetadata` - declared all four snake members, and `content/docs/fields/lookup.mdx` documented - three of them as authorable (`description_field`, `id_field`, `lookup_filters`; - `display_field` 0 hits in all of `content/docs`, which documented `reference_field` - instead). PR #7641 (merged 2026-09-04T15:01:32Z as `351eb318`) then converged the - widget contract on the spec's camelCase. Measured on `origin/main` (`a3eb5d07`): - `LookupField` reads `displayField` / `descriptionField` / `idField` / `lookupFilters` - ONLY (its `reference_field` fallback is kept), `LookupFieldMetadata` declares the - camel members only, and `content/docs` has 0 hits for all four snake keys (controls - in the same run: `reference_field` 4, `lookupFilters` 3). On the tree this census - lands in, the split therefore runs the OTHER way: it is KEEPING these six snake - legs, while the form widget reads camel-only, that lets one stored pre-strict - document render one way in the form and another in the chart, list, filters and - action dialogs. KEEP still stands — on findings 1-2 (the serve path delivers the - stored key, and five sites have no camel leg, so retiring the read deletes the only - read) and on the ruling that refused option B and made the ingestion choke point - (option A, objectui#7650) the prerequisite for any retirement. The way to close the - split is A (canonicalise once at ingestion) plus the additive camel legs tracked on - objectui#7435, not a consumer-side deletion. - -Per site, which way the value would have flipped had the leg been retired: - -- `plugin-charts` `ObjectChart` (`id_field`, `display_field`) — bag proved to be the - object-schema def (`ds.getObjectSchema`). No camel leg: the value would have - collapsed to the constants `'id'` and `'name'` for every host, spec-compliant or not. -- `plugin-form` `deriveMasterDetail` (`display_field`) — object-schema def in-repo, - but `deriveColumns` is a public export, so external callers' bags are untraceable. - No camel leg: `col.displayField` would have become `undefined`. -- `plugin-list` `ListView`, columns branch — NOT the object-schema def. The bag is a - list-view column (`ListColumnSchema`), which refuses BOTH castings of all three keys. - A third contract, filed separately rather than half-retired. -- `plugin-list` `ListView`, object-def branch — object-schema def proved. No camel leg: - the branch's output `displayField` and `idField` (its own descriptor keys, not spec - spellings) would have gone `undefined`. -- `plugin-list` `UserFilters` — `objectDef` is a public prop typed `any` on a publicly - exported component; the bag cannot be traced past this package. No camel leg. -- `app-shell` `resolveActionParams` — the in-file provenance note is correct; this is - the object-schema def. No camel leg for any of its four reads. -- `app-shell` `ObjectFieldInspector` (`lookup_filters`) — the only camel-first site, - and it writes camel back. Its snake leg reads a stored pre-strict document, so - retiring it would show an admin an empty filter list and let a save strand the real - filters. - -One live bug found during the census and deliberately NOT filed as a card, not -addressed here: at this branch's base the designer (`ObjectFieldInspector`, -`readLookupFilters`) read `lookupFilters ?? lookup_filters` (camel first) while the -runtime `LookupField` read `lookup_filters ?? lookupFilters` (snake first), so a -document carrying both keys with different values was displayed one way and honoured -the other. It is RETIRED on `main`: PR #7641 (merged 2026-09-04T15:01:32Z, `351eb318`) -made `LookupField` read `lookupFilters` only, so both halves now honour the camel key -and there is nothing left to file. The fallback the PM recorded on objectui#7642 (a -card the moment #7641 stopped being its fix) is moot — #7641 landed. The designer's -snake leg survives as a read of a STORED pre-strict document, which is the ground of -its KEEP above, not as one side of a competing read order. diff --git a/.changeset/lucky-forms-omit-server-defaults.md b/.changeset/lucky-forms-omit-server-defaults.md deleted file mode 100644 index 2df9bd659d..0000000000 --- a/.changeset/lucky-forms-omit-server-defaults.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Console form pages no longer submit a cleared server-owned field as a blank. - -A field whose declared `defaultValue` is an instruction the server resolves per -insert (a `NOW()` / `current_user` token, or a CEL expression envelope) opens -with an empty control on a create form, and its key stays out of the payload -while nothing touches it. But a submitter who typed into that control and then -cleared it put the key back holding `''` — and `ObjectQL.applyFieldDefaults` -resolves a declared default only for a field arriving absent or null, so the -blank was stored and the declaration silently defeated. - -Such a key is now dropped from a CREATE submit on both the internal -`/forms/:name` and the anonymous `/f/:slug` route. A blank cleared from a field -with no runtime default — or with a static one — is still submitted, because -that is the user removing a value; and an edit submit is untouched, where a -cleared column is a deliberate removal. diff --git a/.changeset/lucky-pugs-shake.md b/.changeset/lucky-pugs-shake.md deleted file mode 100644 index 71af6fb8c7..0000000000 --- a/.changeset/lucky-pugs-shake.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`ui:dropdown-menu` now resolves a menu item's authored `icon` to a glyph instead of drawing the name as raw text. - -Both arms of the item renderer — `DropdownMenuItem` and `DropdownMenuSubTrigger` — rendered `icon` straight into a text node, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew the literal word `copy` beside its label. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. - -The `components-overlay-dropdown-menu/with-icons` catalog fixture declared the retired lucide spelling `edit`, which is absent from lucide's runtime `icons` record and would therefore have drawn no glyph; it now declares `square-pen`, the live key the retired export resolves to by identity. diff --git a/.changeset/map-marker-title-binding.md b/.changeset/map-marker-title-binding.md deleted file mode 100644 index aa8627f38f..0000000000 --- a/.changeset/map-marker-title-binding.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Interface-page maps: derive a marker-title binding from the object's display field - -An ADR-0047 interface page that whitelists `map` derives its map binding with -`defaultMapFromObject`, which bound only `locationField`. With no `titleField` -reaching `ObjectMap`, `getMapConfig` filled the gap with the literal `'name'` -and the marker title is a plain `record[titleField]` read — so on any object -whose display field is not `name` (for example one keyed by `title`), every -marker popup titled itself `undefined`. - -The derivation now also binds the object's display field, resolved with the -field-name half of ADR-0079's precedence: the declared `nameField` (and its -`displayNameField` / `NAME_FIELD_KEY` aliases), otherwise the shared -`deriveTitleField` scan from `@object-ui/core` — the same ranking the kanban, -calendar and gantt renderers resolve titles through, so a map and a board over -one object agree on what a record is called. When nothing resolves the key is -omitted rather than defaulted. A hand-declared `map` block still wins per key. diff --git a/.changeset/marketplace-catalog-runtime-before-admin-5557.md b/.changeset/marketplace-catalog-runtime-before-admin-5557.md deleted file mode 100644 index 69a66da580..0000000000 --- a/.changeset/marketplace-catalog-runtime-before-admin-5557.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace **catalog** page now tells a non-admin that the runtime has no -marketplace, instead of telling them they lack permission (objectui#5557). - -`MarketplacePage` ordered its two early returns admin-first, so on a runtime that -mounts no marketplace at all (`features.marketplace: false` — an `OS_CLOUD_URL=off` -deployment, the EE deploy template's factory default) an unprivileged member got -"access denied" for a surface that exists for nobody. That answer sends them to -ask an administrator for a grant that would not help them, and it left the -informational disabled state built in objectui#5504 unreachable for every -non-admin. The runtime check now answers first, because "this deployment has no -marketplace" is true regardless of who is asking. - -This restores the sibling-page invariant for the one class of viewer it still -failed for: `MarketplacePackagePage` was reordered the same way in objectui#5533, -so on a marketplace-off runtime the catalog page and the package detail page now -give a non-admin the same kind of answer. - -Scope, deliberately narrow: - -- **Admin-first ordering stays correct where a marketplace exists.** On a runtime - with `features.marketplace: true`, a non-admin still gets `MarketplaceAccessDenied` - — the catalog is an install surface, and a member who cannot install has nothing - to do with it. That boundary is pinned by an explicit test, not left to prose: - without it, a change that simply dropped the admin check would look correct. -- Nothing an admin sees changes, on either kind of runtime. -- No new i18n keys, and no change to `MarketplaceAccessDenied` or - `MarketplaceDisabled` themselves — only which of the two the page reaches for, - and in which order it decides. -- The disabled state is still the server's own answer (`features.marketplace`), - never inferred from a failed request, and it still fails open. diff --git a/.changeset/marketplace-detail-admin-guard-order-5583.md b/.changeset/marketplace-detail-admin-guard-order-5583.md deleted file mode 100644 index b50d522485..0000000000 --- a/.changeset/marketplace-detail-admin-guard-order-5583.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace package detail page decides "you are not an admin" before it fetches, -instead of after the load has already failed. - -`MarketplacePackagePage` ordered its early returns with the `!isAdmin` guard *after* -both the loading branch and the `error || !data` branch, and gated its two fetch -effects on `features.marketplace` alone. On a runtime that mounts a marketplace, a -non-admin who opened a package URL was therefore walked through the fetch and the -skeleton, and — when the load failed — was handed the destructive "Failed to load -package" card carrying the server's own error message. Whether that viewer was -refused or handed a diagnosis about a surface they are not allowed to use came down -to whether an unrelated request happened to succeed. - -The guard now sits ahead of both branches, and `getMarketplacePackage` and -`getCloudInstallationInfo` are gated on `isAdmin` as well, so the page stops issuing -requests on behalf of a viewer it has already decided to turn away. That is the -discipline objectui#5533 established on this same page for `features.marketplace`, -applied to the other predicate that decides the same thing. It is also the ordering -`MarketplacePage` carries after objectui#5557, so the two sibling pages now answer one -runtime the same way for every viewer. The server remains the authority on what a -non-admin may fetch; this only stops the client doing work it would discard. - -Unchanged for an admin, deliberately and under test: a failing load still produces the -destructive card with the server's message intact, and a successful one still renders -the package. A "fix" that hoisted the refusal unconditionally, or that deleted the -failure branch, would satisfy every non-admin assertion and fail those two. - -`loading` stays seeded from `marketplaceEnabled` alone rather than from -`marketplaceEnabled && isAdmin`. `isAdmin` reads `activeMember`, which `AuthProvider` -resolves asynchronously *after* the session settles, so an admin whose role comes from -the org member row renders once as a non-admin before the flag flips. Seeding `false` -there would leave that first admin render with `loading: false` and no data — the -destructive card, painted for a frame before the effect could raise the flag again. -`MarketplacePackagePage.guardOrder.test.tsx` pins the flip case for that reason, along -with the ordering, the skipped requests, and the marketplace-off boundary the guard -must not jump above. diff --git a/.changeset/marketplace-detail-disabled-state-5533.md b/.changeset/marketplace-detail-disabled-state-5533.md deleted file mode 100644 index 672d7f993a..0000000000 --- a/.changeset/marketplace-detail-disabled-state-5533.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace **package detail** page now states that the marketplace is turned -off, instead of red-erroring, on a runtime that has none (objectui#5533). - -`MarketplacePackagePage` takes the same `isMarketplaceEnabled()` early return its -sibling catalog page took in objectui#5504, rendering the informational -`MarketplaceDisabled` state when the server reports `features.marketplace: false` -(an `OS_CLOUD_URL=off` deployment — the EE template's factory default). Until now -the same runtime answered a bookmarked or pasted package URL with a destructive -"Failed to load package / Not found." card, so the two sibling pages reached -opposite conclusions about one runtime: the catalog called it configuration, the -detail page called it a failure. - -Both requests the page fires for its own view are skipped in that state — the -package fetch and the cloud-installation probe — rather than fired and discarded: -a discarded request still reaches the server and can race the destructive card -onto the screen before the disabled state settles. - -Scope, deliberately narrow: - -- **Not** a "swallow all errors" change. With `features.marketplace: true` the page - behaves exactly as before, and a package that genuinely is not there still - renders the destructive card carrying the server's own message. The flag is the - runtime's own answer, never inferred from the shape of a failure, and it fails - open — a runtime that answers nothing keeps its detail page. -- The `installLocal` surfaces are untouched. That is a different capability flag, - and an air-gapped `OS_CLOUD_URL=off` runtime still has a working install-local - path. -- No new i18n keys: `marketplace.disabled.*` and `marketplace.action.backHome` - already ship in all ten locale packs. diff --git a/.changeset/marketplace-disabled-state-5504.md b/.changeset/marketplace-disabled-state-5504.md deleted file mode 100644 index 4462a09136..0000000000 --- a/.changeset/marketplace-disabled-state-5504.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a -first-class disabled state, and the load-failure hint describes the control plane -the runtime was actually pointed at (objectui#5504). - -`apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory -default, so a stock self-hosted stack has no marketplace at all. The Console still -recommended one: Home led with "Start with a template" and "Browse App -Marketplace", and the click landed on a red **Failed to load marketplace / Not -found** card whose hint claimed this runtime "points at the public ObjectStack -cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for -exactly the deployment reading them — the operator had not left the default, and -the advice pointed back at the template that told them to set `off`. "Marketplace -disabled by configuration" is a configuration conclusion, not a load failure. - -- `isMarketplaceEnabled()` (`runtime-config`) reads the server's own - `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the - serving app's route table (objectstack#8356). It is never inferred from the shape - of a failed request: a control plane that is merely DOWN leaves the flag `true`, - so an outage still renders as an outage. Unknown fails OPEN. -- The marketplace page renders an informational "App Marketplace is turned off" - state — muted, not `destructive` — and issues no request it knows will 404. -- Home's "Start with a template" cover greys out with a visible localized reason, - and the "Browse App Marketplace" shortcut is withheld, exactly as they already - are for the `manage_metadata` capability gate. -- `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the - configured control plane) and `failedHintSameOrigin`. The "points at the public - cloud by default" sentence is gone: it was rendered unconditionally, including on - every runtime whose operator had overridden `OS_CLOUD_URL`. - -All ten locale packs carry the new keys. diff --git a/.changeset/master-detail-formtype-vocabulary.md b/.changeset/master-detail-formtype-vocabulary.md deleted file mode 100644 index 00f7c1d493..0000000000 --- a/.changeset/master-detail-formtype-vocabulary.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`object-master-detail-form` declares `formType` as a closed vocabulary instead of a bare `string`. - -The block declared `formType` as `type: 'string'` while the sibling `object-form` declared the -same key as an `enum`, and both funnel into the renderer that switches on those variant names. A -value outside the vocabulary therefore matched no branch and fell through to the flat field list -with no diagnostic — measured, a `formType` of `'wizzard'` renders the parent half with its -authored sections silently gone. - -The declared set is `simple | tabbed`, measured against the master-detail composition rather than -copied from the sibling's six: `drawer` and `modal` host the parent half in a portal dialog outside -the master-detail container, so its single bottom Save bar has no form to submit; `wizard` mounts -only the current step's fields and turns that Save bar into a `Next`; `split` renders inline but -persists through `dataSource.create` instead of the atomic batch. - -Authoring-surface only. The manifest, the JSX-page compiler and the save gate now report an -out-of-vocabulary value as `invalid-enum`; rejection at publish time remains `@objectstack/spec`'s. diff --git a/.changeset/max-tool-roundtrips-deprecate-5605.md b/.changeset/max-tool-roundtrips-deprecate-5605.md deleted file mode 100644 index 895bfcddfe..0000000000 --- a/.changeset/max-tool-roundtrips-deprecate-5605.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/types': patch ---- - -`maxToolRoundtrips` on `ChatbotSchema` is deprecated: it is inert, and an author -who sets it is now told so instead of being left believing the documented cap -applies (objectui#5605). - -The key was declared authorable in `@object-ui/types` (interface and zod, with a -description), threaded from the authored document through the chatbot renderer at -three call sites, accepted by `useObjectChat`, given a default — and then dropped. -Measuring the installed chat runtime says it cannot be honoured from here rather -than that someone forgot to wire it: `@ai-sdk/react`'s `useChat` takes `ChatInit` -plus throttle/resume, and `ChatInit` declares exactly one loop control — the -boolean predicate `sendAutomaticallyWhen` — and no numeric cap under any -spelling. The numeric knob was removed from `useChat` in a major, and its -successor was renamed through `continueUntil` to `stopWhen` / `stepCountIs`, -which the installed `ai` package declares only on `generateText`, `streamText` -and the tool-loop agent settings — all server-side. ObjectUI is backend-agnostic, -so it owns no server loop to cap either, and putting the number in the request -body would only move the same dead key one hop onto a wire contract no backend -reads. - -This is stage one of a two-stage retirement, so nothing an author already wrote -breaks: the key still parses, still carries its declared shape, and the renderer -still threads it. What changes is that it is now marked `@deprecated` in the -interface, the zod description and the docs, and that authoring it logs a -one-time notice naming the knob that does work — `planning.maxIterations` on the -agent. A follow-up removes the declaration once this deprecation has shipped in a -release. diff --git a/.changeset/menu-icon-test-phantom-value.md b/.changeset/menu-icon-test-phantom-value.md deleted file mode 100644 index ca89894e20..0000000000 --- a/.changeset/menu-icon-test-phantom-value.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: the two overlay-menu icon suites (`context-menu-item-icon`, -`dropdown-menu-item-icon`) no longer transcribe the undeclared `value` key from -the catalog fixtures objectui#7072 cleaned, and each block now records why the -key is absent. No published source and no rendered behaviour changes. diff --git a/.changeset/metadata-client-actor-retired-4834.md b/.changeset/metadata-client-actor-retired-4834.md deleted file mode 100644 index 2aa8de93c1..0000000000 --- a/.changeset/metadata-client-actor-retired-4834.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -**Breaking (published surface):** remove `options.actor` from `MetadataClient`'s -`save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor` -request header. - -The server stopped honouring that header. objectstack#7941 ruled that the -recorded actor is the identity the request was authorized as, and removed the -header limb from the `/meta` write resolver — attribution cannot drift from -authorization. The option therefore typed cleanly, sent a header, and could not -influence the audit or history row it appeared to address: a false affordance -that promised attribution and silently failed to deliver it. - -Three declarations go: `MetadataClientSaveOptions.actor` (inherited by -`MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`), -and the inline `{ actor?: string }` on each of `publish` and `rollback`. -`MetadataAuditEntry.actor` is unaffected — that is the server's read-back of -who acted, and it remains the way to see attribution. - -Marked `minor` rather than `major` per this repo's version-alignment policy -(the fixed group's major tracks `@objectstack`, and `major` in a changeset -would drag all 39 packages off that cadence). - -No caller in this repo passed `actor`; the census found the only in-repo -occurrence was the client's own unit test. Callers outside this repo that still -pass it are unaffected at runtime beyond losing a header the server already -ignored — the property is dropped rather than forwarded, pinned by -`metadata-actor-retired-4834.pin.test.ts`. diff --git a/.changeset/metadata-lock-state-5024.md b/.changeset/metadata-lock-state-5024.md deleted file mode 100644 index 785458cbfb..0000000000 --- a/.changeset/metadata-lock-state-5024.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/data-objectstack': patch -'@object-ui/app-shell': patch ---- - -The metadata lock banner can no longer render an amber, padlocked box with no -title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three -times (objectui#5024). - -`MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four -states out by hand, 42 lines apart in one file, compared by no gate. They are now -one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s -`z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies -were restating a schema rather than filling a gap. - -The user-visible half is the banner. Its title was three independent `&&` branches -with no fallback, while the switch that opens the banner is true for any non-`none` -value — so a lock state outside the four opened the box and left the headline -empty. That is reachable without a fifth state ever being added here: -`MetadataClient.layered()` casts the wire value through unchecked, so a newer -server reaches this banner as-is. Measured, not assumed — feeding `no-publish` -through the page rendered the padlock, the border and an empty title. The title is -now a keyed lookup with a loud fallback that names the unrecognised token, so a -fifth state fails `type-check` here and, if one arrives from a server anyway, the -operator reads a sentence instead of a blank box. diff --git a/.changeset/nav-selection-root-keys-edge-5600.md b/.changeset/nav-selection-root-keys-edge-5600.md deleted file mode 100644 index 5fd1bb1b3c..0000000000 --- a/.changeset/nav-selection-root-keys-edge-5600.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Internal layering fix, behaviour-unchanged: publishes nothing, declared explicitly -with an empty frontmatter rather than left undeclared. - -Inverts the dependency edge objectui#5600 found: `nav-selection.ts` — otherwise pure -URL-parsing plumbing (`parseSurfaceParam` / `formatSurfaceParam` / `parseNavSelParam` -and friends) — statically imported `APP_NAV_ROOT_KEYS` from the 510-line -`AppNavInspector.tsx`, so every consumer of the URL helpers (including two small -utilities, `console/ai/artifactStudioPath.ts` and, transitively via -`useSurfaceDeepLink.ts`, the studio-design surface) pulled a React inspector -component into their static import graph for one array of root keys. - -`APP_NAV_ROOT_KEYS` (`['nav', 'navigation', 'tabs', 'items', 'menu']`) is a plain -string-literal array — it describes nav SHAPE, not the inspector — so it now lives in -`nav-selection.ts` instead. `AppNavInspector.tsx` never read the array itself, only -re-exported it (`export const APP_NAV_ROOT_KEYS = ROOT_KEYS;` as its last line), so -the fix is a straight move with no back-import needed on that side. - -Verified structurally rather than by reading the import line: a BFS over static -(non-type, non-dynamic) relative imports from `nav-selection.ts` and the other -non-test importers no longer reaches `AppNavInspector.tsx` (158 files visited, target -not found), where the same walk over the pre-fix tree found it in 6 files via -`nav-selection.ts -> AppNavInspector.tsx` directly. diff --git a/.changeset/nervous-donkeys-tap.md b/.changeset/nervous-donkeys-tap.md deleted file mode 100644 index 31a9d1b170..0000000000 --- a/.changeset/nervous-donkeys-tap.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Studio workbench rails converge on an AI copilot apply without a page reload (objectui#7255) - -The copilot is the right dock of the Studio document and already announces -every authoring turn on the assistant bus (`emitMetadataRefresh`), but the -four pillar rails never subscribed: a just-applied object or nav item stayed -invisible until the author reloaded the page. The Interfaces / Data / -Automations / Access rails now list that pulse as a load dependency and refetch -their package-scoped reads in place. The Interfaces rail holds the pulse while -the nav editor has unsaved edits (its load rehydrates the nav edit buffer) and -releases it once they settle, and re-reading the same package no longer flaps -the app status back through `loading`. diff --git a/.changeset/network-escape-batch1.md b/.changeset/network-escape-batch1.md deleted file mode 100644 index 7473843dc6..0000000000 --- a/.changeset/network-escape-batch1.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: the five network-escape files in batch 1 of objectui#7307 now serve their `/api/v1/security/explain` and `/api/v1/meta/object/<name>` probes from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. No published behaviour changes — no product source is touched. diff --git a/.changeset/network-escape-batch2.md b/.changeset/network-escape-batch2.md deleted file mode 100644 index f80e7f7caa..0000000000 --- a/.changeset/network-escape-batch2.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: the four network-escape files in batch 2 of objectui#7307 (three in `plugin-kanban`, one in `plugin-gantt`) now serve their `/api/v1/security/explain` probe from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. No published behaviour changes — no product source is touched. diff --git a/.changeset/network-escape-batch3.md b/.changeset/network-escape-batch3.md deleted file mode 100644 index f30fed98fe..0000000000 --- a/.changeset/network-escape-batch3.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: the four `plugin-detail` network-escape files in batch 3 of objectui#7307 now serve their probes from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. One of the four also reached `/api/task/42` through `DetailView`'s `api` branch, so its router serves that route too. No published behaviour changes — no product source is touched. diff --git a/.changeset/network-escape-batch4.md b/.changeset/network-escape-batch4.md deleted file mode 100644 index aa1dd6a38d..0000000000 --- a/.changeset/network-escape-batch4.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change: the four `app-shell` `console/home` network-escape files in batch 4 of -objectui#7307 now serve their `/api/v1/meta/_drafts` probe from a recording double instead -of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. The -double answers an empty draft ledger, which is what `PendingDraftsBanner` already rendered -from the failed read, so no assertion moves. No published behaviour changes — no product -source is touched. diff --git a/.changeset/network-escapes-batch5.md b/.changeset/network-escapes-batch5.md deleted file mode 100644 index 1967c76f96..0000000000 --- a/.changeset/network-escapes-batch5.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only (objectui#7307 batch 5): three app-shell suites now serve their -`GET /api/v1/meta/object`, `GET /api/v1/automation/_status` and -`/api/v1/ai/conversations` probes from recording doubles instead of a real -socket, and their three rows leave the network-escape ledger. No published -runtime code changes, so nothing to release. diff --git a/.changeset/network-escapes-batch6.md b/.changeset/network-escapes-batch6.md deleted file mode 100644 index 40594bf6f0..0000000000 --- a/.changeset/network-escapes-batch6.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Test-only (objectui#7307 batch 6, the last): `FlowNodeInspector.specKeys.test.tsx` -now serves its `GET /api/v1/meta/object` probe from a recording double instead of -a real socket, which empties the network-escape burn-down ledger. The ledger is -therefore RETIRED with it — the guard's `KNOWN_ESCAPES` set, its attributed-stderr -branch for listed files and its known/unknown split are gone, exactly as the -ledger's own non-vacuity floor prescribed for reaching zero, and -`scripts/__tests__/network-escape-ledger.test.ts` now pins that the machinery -stays gone. The standing guard from objectui#6640 is untouched and now covers -every file: any test that reaches a real socket is red on its first run. No -published runtime code changes, so nothing to release. diff --git a/.changeset/node-gate-data-predicate-report-5687.md b/.changeset/node-gate-data-predicate-report-5687.md deleted file mode 100644 index 5465efc0e4..0000000000 --- a/.changeset/node-gate-data-predicate-report-5687.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`SchemaRenderer`'s node visibility gate now emits a dev-only warning when a -predicate reads `data.*` and the data-source adapter cannot answer that read, so -an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the -constant they authored instead of shipping it (objectui#5687). - -At the node tier `data` is the data-source **adapter** — the object `${data.total}` -in a props bag resolves against — and it has never been the row. A predicate -written with the deprecated `data.*` spelling therefore resolves `undefined == -'draft'`, which is a perfectly good `false`: the block is hidden on every row, and -because it does not throw, the unresolvable-predicate reporter added for -objectui#5454 never fired. Measured on this base, the same predicate written as a -`{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether -an author heard about the identical mistake depended on which dialect they happened -to write it in, which is the arbitrariness objectui#5454 existed to remove. - -**No verdict changes and no interpolation changes** (maintainer ruling, -2026-08-22, option A). The node tier keeps its documented `data` = adapter -semantics; objectui#5330's row binding does not extend here. The evaluator's answer -is returned exactly as computed and `${data.*}` interpolation is untouched — only -the silence moved. - -The report fires on a `data.*` read the bound adapter answers with `undefined`, not -on the spelling. A genuine adapter read stays silent (`data.total > 0` against an -adapter carrying `total`), a canonical `record.*` predicate stays silent, and a -correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per -node type + key + predicate source — the same module, Set and lifecycle as the -objectui#5454 reporter. - -This loudness is temporary by design: it dissolves when objectui#5330's deprecation -window for the `data.*` row spelling closes. diff --git a/.changeset/normalise-client-error-user-message-5901.md b/.changeset/normalise-client-error-user-message-5901.md deleted file mode 100644 index d6739f5405..0000000000 --- a/.changeset/normalise-client-error-user-message-5901.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -Preserve the producer's `userMessage` marking when `normaliseClientError` re-wraps a refusal. - -`ApiErrorSchema.userMessage` (objectstack#9934) is the opt-in channel an application author -sets at throw time to say "this text is for the end user", and the contract states it -status-agnostic — any refusal status may carry it. Both of the shapes this adapter re-wraps -into typed errors dropped the marking: a hook that refused a write with `VALIDATION_FAILED` -or `CONCURRENT_UPDATE` and marked its own sentence had that sentence discarded at the -adapter boundary, before any surface could render it. Nothing threw and the typed error was -otherwise correct, so the only symptom was the user reading a generic string instead of the -sentence their administrator wrote. - -The marking now rides both re-wraps, in the form the shared reader (`declaredUserMessage`) -already looks for: on the details bag for `DataApiValidationError`, exactly the way `fields` -already survives, and on a new readonly `userMessage` member for `ConcurrentUpdateError`, -which has no details bag. Unmarked refusals are untouched — they carry no key and still read -as `null`, so nothing a producer did not opt into can reach a user. diff --git a/.changeset/notready-t-interpolates.md b/.changeset/notready-t-interpolates.md deleted file mode 100644 index 5c806e4ec1..0000000000 --- a/.changeset/notready-t-interpolates.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -fix(i18n): `useObjectTranslation`'s provider-less `t` now interpolates its inline `defaultValue` - -With no `I18nProvider` mounted, react-i18next hands back its not-ready `t`, which -returns `options.defaultValue` **verbatim** — so an inline default written -`'Deleted {{count}} rows'` reached the user with the braces intact. 68 inline -defaults across 24 files rendered through that path on any host that embeds an -ObjectUI component without a provider, which is the configuration -`createSafeTranslation` exists for. - -`useObjectTranslation` now runs its not-ready result through the same one -interpolator `createSafeTranslation`'s `fallbackT` uses, so both provider-less -renderers fill exactly the `{{name}}` spelling the copy is already gated to. The -ready path is untouched: with a provider, i18next's own `t` is returned by -reference and nothing is interpolated twice. Pre-interpolated template-literal -defaults (`` `Deleted ${n} rows` ``) stay correct — they have no holes left to -fill — so no call site changes. diff --git a/.changeset/npm-presence-publish-predicate-5442.md b/.changeset/npm-presence-publish-predicate-5442.md deleted file mode 100644 index c77030eafe..0000000000 --- a/.changeset/npm-presence-publish-predicate-5442.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -`.github/workflows/changeset-release.yml` keys its publish lane on **"is the version this -commit declares already on npm?"** instead of on **"are changesets pending?"** -(objectui#5442, maintainer ruling 2026-08-22, Option B). - -The two predicates read as interchangeable and come apart exactly where it costs a -release. The version PR is cut from `main` at T and merged at T+n; `main` takes ~18 -merges a working day and the merge does not remove the changesets that landed in -between. Those belong to the *next* version, but keyed on them the version this commit -just bumped to is skipped, and the next version PR bumps straight past it. Measured on -`bc21c704b`: of the 90 versions `packages/core/CHANGELOG.md` declares, 16 never reached -npm, and the repository said `17.6.0` while `dist-tags.latest` said `17.5.0`. - -The npm predicate is also cheaper than the one it replaces rather than a trade against -it: an ordinary landing does not move the manifest version, so it answers "already -published" and the expensive job is skipped. - -Alongside it, a **loud check**. The defect was never a red run — run 3370 on `cfeb378b5` -completed `success` having published nothing — so the publish lane now reads the registry -back and fails if the version it exists to ship is still absent. A repo/npm divergence is -a failing run, not something an audit finds 16 versions later. - -The refresh lane is untouched and still cannot publish: no `publish:` input and no npm -credentials in its `env:`, the double denial that keeps a scheduled tick from releasing -something nobody merged. diff --git a/.changeset/object-schema-metadata-derive-from-spec.md b/.changeset/object-schema-metadata-derive-from-spec.md deleted file mode 100644 index 501ecb2388..0000000000 --- a/.changeset/object-schema-metadata-derive-from-spec.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/app-shell': patch -'@object-ui/react': patch ---- - -`ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject` -instead of being a hand-written copy (objectui#5362; maintainer ruling -2026-08-20: the object document type belongs to the spec). - -What changes on the published type surface: - -- **Gained:** the full spec object-document surface, including the three keys - the runtime already reads but the old interface rejected as excess - properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`, - `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of - the spec document). -- **Removed:** nine members the old interface declared that no objectui - runtime code reads and the spec document does not know: `extends`, - `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling - is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`. - `ObjectTrigger` and `ObjectRelationship` remain exported unchanged. -- **Kept:** `editMode` — the one measured client-side member the runtime reads - (`recordFormNavigation` / `AppContent`) — now declared on the new - `ObjectSchemaClientExtensions` interface, which the derivation intersects. - Note the spec's strict parse rejects `editMode` on published documents - (`unrecognized_keys`); it is a client-type member only. - -Spelling settlement: `listViews` (camelCase) is canonical — `list_views` -appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in -`@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling -READ fallback for stored pre-settlement documents (that stock has never been -censused — objectstack#7917); the CRUD guide and its pinned transcription now -author the canonical spelling. diff --git a/.changeset/object-view-unmirrored-keys-7779.md b/.changeset/object-view-unmirrored-keys-7779.md deleted file mode 100644 index 65ab787d01..0000000000 --- a/.changeset/object-view-unmirrored-keys-7779.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**Breaking for authored metadata:** `ObjectViewSchema.viewTabBar` is RETIRED -(objectui#7779, maintainer ruling B of 2026-09-06; ADR-0049 enforce-or-remove). -An `object-view` node that authors `viewTabBar` no longer validates: the parse -fails loudly on the `viewTabBar` path with the explanation in the message, and -the TS member is a `?: never` tombstone so the same document is refused at -compile time. Nothing ever read the key off the node — the tab-bar UX config -(`ViewTabBarConfig`, still exported) is the `config` prop of the `ViewTabBar` -component, composed by the host, not authored metadata. Remove the key. - -In the same change, eight `ObjectViewSchema` keys the TypeScript interface -declared and the Zod mirror never did are now ENFORCED. Until now a document -authoring any of them passed the validator unexamined through `BaseSchema`'s -passthrough while the published type invited the author to write it — declared -but not enforced (objectui#7279's `UnmirroredDeclared` reading). Each key now -admits exactly what the declaration promises and refuses a wrong-typed value at -its own path; a correctly typed document is untouched. - -| key | disposition | how | -| --- | --- | --- | -| `navigation` | mirrored | the spec's `ListViewSchema.navigation` slot by reference (`NavigationConfigSchema`, optional) | -| `searchableFields` | mirrored | the spec's `ListViewSchema.searchableFields` slot by reference (`string[]`, optional) | -| `filterableFields` | mirrored | the spec's `ListViewSchema.filterableFields` slot by reference (`string[]`, optional; the spec marks it a legacy shorthand for `userFilters.fields`) | -| `allowCreateView` | mirrored | the sibling `ViewSwitcherSchema.allowCreateView` slot by reference — the renderer forwards the value verbatim into the `view-switcher` node it composes | -| `viewActions` | mirrored | the sibling `ViewSwitcherSchema.viewActions` slot by reference, for the same reason | -| `defaultViewType` | mirrored | local literal: the declaration's seven-value union (`grid`, `kanban`, `gallery`, `calendar`, `timeline`, `gantt`, `map`) — read as `schema.defaultViewType \|\| 'grid'` | -| `defaultListView` | mirrored | local literal: `string` — read as `namedListViews?.[schema.defaultListView]` | -| `showViewSwitcher` | mirrored | local literal: `boolean` — read as `schema.showViewSwitcher === true` | -| `viewTabBar` | RETIRED | `?: never` + `retirementTombstone()` — zero reads on the node | -| `listViews` | unchanged (still unmirrored) | see below | - -**What was measured.** Every reading was taken on the `object-view` node -renderer (`packages/plugin-view/src/ObjectView.tsx`, registered by -`plugin-view/src/index.tsx`) with `schema.objectName` / `schema.layout` as the -positive controls of the same `schema.KEY` query, so each zero is a reading; -the repo-wide census of `viewTabBar` finds the key in no source file outside -`@object-ui/types` (two doc tables listed it as authorable and are corrected -here). The spec side was read through the installed pin -(`@objectstack/spec@17.2.0`, `ui` entry, 117 exported object schemas walked; -control keys `objectName` / `columns` / `navigation` / `listViews` hit): the -three spec-modelled keys are optional slots on `ListViewSchema` and -`ObjectListViewSchema`; the six local keys have no spec slot anywhere. - -**`listViews` stays unmirrored, on the ruling's own fallback clause.** The -declaration's value is the local `NamedListView` — **47 declared top-level -members**, of which the renderer reads six: `label`, `type`, `columns`, -`filter`, `sort`, `options`. It reads a seventh key off a named view, `data`, -but `data` is **not a declared member of `NamedListView` at all**: it reaches -the renderer through an `as any` cast on the named-view config in -`packages/plugin-view/src/ObjectView.tsx`, so it never was one of the declared -members a mirror would have to carry. The spec's `ViewSchema.listViews` is a -record of the STRICT `ObjectListViewSchema`, which requires `columns` and -refuses `options`, ObjectQL tuple filters and `default` — that is, it refuses -the named views this package's own README and -`content/docs/api/schema-reference.md` teach (`{ label: 'All Users' }` fails at -`columns`; `filter: [["owner", "=", "..."]]` fails at `filter.0`). Mirroring the -spec value would lose documented behaviour; mirroring the local value would -enforce **41 unread members** (47 declared, minus the 6 that are both declared -and read) into the contract — the -very thing ruling B refused for the six local keys. The key therefore stays in -the parity ledger with that measurement, pinned, until the maintainer decides its -value type. It is not papered over with `z.any()`. - -**Who is affected:** an author who wrote `viewTabBar` on an `object-view` node -(remove it), or who wrote a wrong-typed value for one of the eight keys — e.g. -`defaultViewType: 'tree'` (host-composition-only, objectui#5321), -`navigation: 'page'` (write `navigation: { mode: 'page' }`), -`searchableFields: 'name'` (write an array), `viewActions: 'share'` (write -`[{ type: 'share' }]`). Such documents used to pass validation and render with -the key ignored; they now fail at the key with the reason. - -**Who is NOT affected:** every correctly typed document, and every document -that never wrote these keys — `absent` stays valid on all nine. No renderer -changed. The parity ledger (`zod-mirror-parity.test.ts`) records the move: -`UnmirroredDeclared` 14 entries / 96 keys to 14 / 87, the `ObjectViewSchema` -entry re-derived into the SPEC-DERIVED half because the mirror now references -the spec in code. - -Graded `minor`, not `patch`: this narrows the accepted input set, which is -breaking for any author who wrote the tolerated spellings. It is not `major` -per this repo's fixed-group convention (objectui's own breaking changes ship as -`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, -mechanically enforced by `scripts/check-changeset-no-major.mjs`). diff --git a/.changeset/objectchart-fieldoptionlabel-ref-5587.md b/.changeset/objectchart-fieldoptionlabel-ref-5587.md deleted file mode 100644 index 4b61e3f15d..0000000000 --- a/.changeset/objectchart-fieldoptionlabel-ref-5587.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -`ObjectChart` now depends on the `fieldOptionLabel` resolver directly instead of -holding it behind a ref, so a chart re-resolves its groupBy option labels when -the resolver genuinely changes (objectui#5587). - -The ref existed for a reason that no longer holds. `useSafeFieldLabel()` returned -a fresh object on every render outside an i18next provider, so a direct -dependency made `fetchData`'s `useCallback` identity fresh on every render, and -the effect that depends on `fetchData` refetched on every render — an unbounded -loop. `ObjectChart` worked around that locally with `fieldOptionLabelRef` plus a -`useEffect` keeping it current. `useObjectLabel`'s memo now holds with or without -an i18next instance bound (objectui#5564), so the resolver's identity is stable -on both paths and the indirection buys nothing. - -It did cost something, and that is the user-visible half: a ref-hidden dependency -meant `fetchData` did NOT re-run when the resolver changed. A chart mounted -before its `I18nProvider`, or rendered across a language switch, kept serving -groupBy labels resolved by the old resolver until some unrelated dependency -(object name, filter, aggregate) happened to move. It now refetches once on that -transition and shows labels in the active language. - -Pinned by `ObjectChart.fieldOptionLabelRefetch.test.tsx`, which counts fetches -across forced re-renders both outside and inside a provider. Reverting -`useObjectLabel.ts` to its pre-objectui#5564 state turns the no-provider case red -(2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so -the removal is pinned to the fix that unlocked it rather than to a comment. diff --git a/.changeset/objectmap-marker-title-resolver-5953.md b/.changeset/objectmap-marker-title-resolver-5953.md deleted file mode 100644 index fa48d3279b..0000000000 --- a/.changeset/objectmap-marker-title-resolver-5953.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-map': patch ---- - -`ObjectMap` now resolves marker titles through `@object-ui/core`'s -`getRecordDisplayName` (ADR-0079), instead of reading a hard-coded `'name'` key. - -`getMapConfig` used to fill an absent title binding with the string literal -`'name'`, and the marker transform then did a bare `record[titleField]` read. For -every object whose display field is not literally `name`, that read was -`undefined`, so each marker popup titled itself `undefined`. The literal is gone -from both branches that carried it, and the read site hands the decision to the -same resolver `ObjectKanban`, `ObjectCalendar` and `ObjectGantt` already title -their items through — `ObjectMap` was the fourth renderer and the only one still -outside it. - -An authored `map.titleField` keeps winning outright: it is passed through as the -resolver's explicit `titleField` option, which it checks first. What is new -underneath is everything a static field-name binding cannot express — the -object's declared `nameField`, its deprecated `displayNameField` alias, a legacy -`titleFormat` template, type-aware field derivation, and a name-ish probe over -the record's own keys for the inline-data case where no object definition is -fetched at all. - -Records with no resolvable name now read `Record #<id>` rather than `undefined` -or a uniform `Marker`; the `Marker` placeholder is kept only for a record that -carries no id either. No authoring surface changes and no new map config keys. diff --git a/.changeset/objectmap-schema-data-shorthand-5305.md b/.changeset/objectmap-schema-data-shorthand-5305.md deleted file mode 100644 index c21345fce8..0000000000 --- a/.changeset/objectmap-schema-data-shorthand-5305.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-map': patch ---- - -`ObjectMap` reads `schema.data` in one place again, so an array-shorthand map stops -making a metadata request it never uses. - -The fetch effect carried a second short-circuit beside the `props.data` one -objectui#5003 fixed: it read `schema.data` directly and tested whether that value was -itself an array. eslint reported it as `missing dependency: schema.data` — the last -`react-hooks/exhaustive-deps` warning on that effect. - -The dependency was never actually missing. `getDataConfig(schema)` already returns -`schema.data` verbatim, and the result is memoized on `JSON.stringify(rawDataConfig)` -into `dataConfig`, which **is** one of the effect's declared dependencies. The authored -rows therefore reached the effect before this change; the direct read was a duplicate of -an already-threaded value, which is why neither adding a dependency nor deleting the -branch was right. - -The array handling moved into `getDataConfig`, where `ObjectGrid`'s own `getDataConfig` -already pins the same normalization (`"Check if data is an array (shorthand format)"`). -Same rows render, and the effect now reads only `dataConfig`. - -One behavioural consequence, and it is the point: an array under `data` now yields -`provider: 'value'`, so `hasInlineData` is true and the sibling effect no longer calls -`dataSource.getObjectSchema()` for it. That request's only read site is -`buildExpandFields()` inside the object-provider fetch branch, which an inline schema -never reaches — so the call was pure waste, and the shorthand now behaves exactly like -the declared `{ provider: 'value', items }` form it is shorthand for. - -Deleting the branch instead was measured, not assumed: with no producer-side handling, -an array-shorthand map renders `Error: DataSource required for object/api providers` -rather than its markers. The shorthand is a live convention in six sibling blocks -(`ObjectGrid`, `ListView`, `ObjectTree`, `ObjectChart`, `ObjectDataTable`, -`calendar-view-renderer`), so `object-map` would have become the one block in the family -that answers it with an error box. diff --git a/.changeset/objectpivottable-i18n-resolver-refs-5625.md b/.changeset/objectpivottable-i18n-resolver-refs-5625.md deleted file mode 100644 index b5f4cc6308..0000000000 --- a/.changeset/objectpivottable-i18n-resolver-refs-5625.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel` -resolvers directly instead of holding them behind refs, so a pivot re-derives -its header and option labels when the resolver genuinely changes -(objectui#5625). - -The refs existed for a reason that no longer holds. `useSafeFieldLabel()` -returned a fresh object on every render outside an i18next provider, so a direct -dependency would have re-run the metadata-derivation effect on every render — -and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly -built objects, so each run scheduled the next one: an unbounded derive loop. -`ObjectPivotTable` worked around that locally with `fieldLabelRef` / -`fieldOptionLabelRef` plus a `useEffect` keeping them current. -`useObjectLabel`'s memo now holds with or without an i18next instance bound -(objectui#5564), so both resolvers have a stable identity on both paths and the -indirection buys nothing. - -It did cost something, and that is the user-visible half: a ref-hidden -dependency meant the derivation did NOT re-run when the resolver changed. A -pivot mounted before its `I18nProvider`, or rendered across a language switch, -kept showing its top-left header label and its select-option cell labels as -resolved by the old resolver — until some unrelated dependency (the data source, -the object name) happened to move. It now re-derives once on that transition and -renders in the active language. - -This is the same removal objectui#5587 made in `ObjectChart`, one package over. - -Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations -across forced re-renders both outside and inside a provider, checks that the two -derived state maps settle, and asserts the language-switch re-derivation. -Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the -no-provider case red (derivations in the dozens instead of 1), so the removal is -pinned to the fix that unlocked it rather than to a comment. diff --git a/.changeset/objectview-overlay-pair-completeness-5025.md b/.changeset/objectview-overlay-pair-completeness-5025.md deleted file mode 100644 index 85273d19a3..0000000000 --- a/.changeset/objectview-overlay-pair-completeness-5025.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`sanitizeViewOverride` now strips a half-filled `between` from a stored view -overlay, instead of handing it back to the merge (objectui#5025, -objectstack#8815). - -The overlay recovery pass asked "is this filter row filled in?" with the -shape-blind predicate objectstack#8815 retired — `value == null || value === '' -|| (Array.isArray(value) && value.length === 0)`. That is correct for `scalar` -and `list` and blind to `pair`: a `between` carrying one bound is -`['2024-01-01', '']`, an array of length 2, so the pass read it as a real -condition and kept it. The two write paths (`plugin-list`'s `ListView` and -app-shell's `viewFilterFold`) were converted to the builder's arity-aware -`isFilterValueComplete`; this read path was the third verbatim copy, and the one -whose job was to clean up exactly the rows the other two used to write. Until -now a stored half-range survived the pass, reached the query, and the server -refused the whole view (`400 INVALID_FILTER`) for every user on every later -read. - -The pass now delegates to the same `isFilterValueComplete` the write paths use, -on both of the at-rest shapes it handles (the spec `ViewFilterRule` object and -the legacy runtime triple). A complete range — bounds of `0` and `false` -included — is untouched, and the `scalar` and `list` families read exactly as -before, since the retired predicate was already right for them. One shape is -newly stripped beyond the half-filled array: a `between` whose value is a bare -scalar, which `ViewFilterRuleSchema` itself refuses. - -Which operators want no value at all is unchanged and still answered by -app-shell's `VALUELESS_FILTER_OPERATORS` — that set is already derived from the -builder's `VALUELESS_FILTER_BUILDER_OPERATORS` rather than being a private copy, -and its parity is pinned. diff --git a/.changeset/objectview-sort-sink-4869.md b/.changeset/objectview-sort-sink-4869.md deleted file mode 100644 index 9408f95811..0000000000 --- a/.changeset/objectview-sort-sink-4869.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -**Bug fix:** a non-grid `object-view` (calendar / kanban / gallery / timeline) -whose sort came from the deprecated `table.defaultSort` no longer comes back -empty. `ObjectGridSchema.defaultSort` is declared a single `{ field, order }` -object, and `ObjectView`'s own fetch handed it to `$orderby` verbatim — where -the ObjectStack adapter reads it as an `$orderby` map and folds it with -`Object.entries`, so the request went out sorting by two columns literally -named `field` and `order`. The server answers `400 INVALID_SORT`, `ObjectView` -swallows the error, and the view rendered with no records — while the *same* -metadata sorted correctly as a grid, because `ObjectGrid` lowers that pair -before using it (objectui#4869, maintainer ruling 2026-08-22). - -`ObjectView` now performs the same legacy-to-canonical lowering `ObjectGrid` -already does (`sort ?? (defaultSort ? [defaultSort] : undefined)`) and routes -the whole chain — named view sort, `views` prop sort, `table.sort`, -`table.defaultSort` — through the shared `convertSortToQueryParams` sink. This -was the last object-bound read site sending an authored sort to `$orderby` -unlowered; every other block (gantt, map, calendar, timeline, -`record:line_items`) already did, so an adapter that implements `find` itself -now receives one normalized `Record<field, direction>` from all of them instead -of two different dialects. - -Precedence is unchanged, and both spellings of the pair keep working. The -shared sink was deliberately **not** widened to accept a bare `{ field, order }`: -its input slot also legitimately carries `$orderby`'s own map, in which -`{ field: 'desc' }` is a valid ordering by a column named `field`, so widening -it would make one shared function guess. - -Two visible shape changes on the wire, both semantically identical to before: a -string `table.sort` such as `'name desc'` now serializes as `-name` rather than -riding through as `name desc`, and a `SortConfig[]` arrives as a map rather than -as an array. diff --git a/.changeset/olive-cameras-repeat.md b/.changeset/olive-cameras-repeat.md deleted file mode 100644 index 6688e0b3d6..0000000000 --- a/.changeset/olive-cameras-repeat.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/plugin-map": patch ---- - -`ObjectMap`: make the marker `useMemo` actually memoize. - -`getMapConfig(schema)` ran unmemoized in the render body, so `mapConfig` carried -a fresh object identity on every render. The marker transform names `mapConfig` -in its dependency array, so it recomputed on every single render — walking every -record through `extractCoordinates` and the display-name resolver, and re-running -`ObjectMapConfigSchema.safeParse` on each pass — while declaring that it does not. -The invalidation cascaded on into `filteredMarkers`, `clusteredData`, -`markerBounds` and `initialViewState`. - -`mapConfig` is now memoized on `schema`, the one value `getMapConfig` reads. -Behaviour is unchanged; the config is still rebuilt whenever the schema -genuinely changes. diff --git a/.changeset/olive-crabs-shave.md b/.changeset/olive-crabs-shave.md deleted file mode 100644 index 6d6476f633..0000000000 --- a/.changeset/olive-crabs-shave.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Every envelope reader on the package surfaces now renders a producer-marked -`error.userMessage`, and the Studio package list opens the failure body at all. - -`fetchPackages` — the `/api/v1/packages` read behind the Studio switcher, the writability -courtesy gate, the namespace lookup and the builder landing page — answered a refusal with -`HTTP <status>` and never opened the body, so `message`, `code` and `userMessage` were -discarded together: a 403 whose envelope said `Reading packages requires the studio.access -or setup.access capability.` reached the author as four characters. `apiJson` on the -package admin page read `error.message` and never the mark, and rendered no code. -`duplicatePackage` read `error.message` alone. - -All three now ask one shared rule (`readEnvelopeFailureText`): a producer-marked -`userMessage` outranks the diagnostic `message` at any status — presence of the field is -the producer's marking, and a consumer that sees it renders it verbatim — with `error.code` -appended to whichever prose won. An unmarked refusal is unchanged and still renders its -diagnostic; a body carrying no prose still falls back to each reader's own status text. diff --git a/.changeset/olive-donkeys-invent.md b/.changeset/olive-donkeys-invent.md deleted file mode 100644 index b3139d133f..0000000000 --- a/.changeset/olive-donkeys-invent.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/app-shell": patch ---- - -ObjectView no longer forwards `chart.config` onto the `object-chart` node it builds. - -`@objectstack/spec`'s `ListChartConfigSchema` is a `strictObject` declaring exactly -`chartType` / `dataset` / `dimensions` / `values`, and objectui binds it by reference, so -`config` is refused by name (`unrecognized_keys ["config"]`) — by the client schema and by -the same schema the platform's metadata write door parses every save through. The rung was -therefore a channel no conforming author could feed, on both the dataset and the legacy -branch, and it landed on a node that does not declare `config` either. - -For every spec-conforming view this is a no-op. A non-conforming row that somehow carried -the key degrades rather than breaks: `ChartRenderer` generates a container config from -`series` plus a positional palette when none is present, so the chart still renders, with -series-derived labels and default colours. diff --git a/.changeset/olive-donkeys-scream.md b/.changeset/olive-donkeys-scream.md deleted file mode 100644 index 36ffa1eb22..0000000000 --- a/.changeset/olive-donkeys-scream.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Pin the form-view option element's two narrowings (objectui#6263): a per-option `default` -written on a metadata-admin form field stays refused at the type level, and the re-pointed -`visibleWhen` still refuses the spec's `dialect`-only envelope. Type-level pins plus a -tombstone comment recording the 2026-08-28 ruling; no published behaviour changes. diff --git a/.changeset/olive-donkeys-shave.md b/.changeset/olive-donkeys-shave.md deleted file mode 100644 index 6e765665f9..0000000000 --- a/.changeset/olive-donkeys-shave.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846). - -The flat branch now requests the `[]` marginal grouping — the same server-side -totals machinery the cross-tab has used since framework#1753 — and renders the -answer under the console's existing `dashboard.total` label. Totals are computed -by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/ -`max` reports the whole-set aggregate); nothing is recombined client-side. The -row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null -bucket, and never joins the drillable rows. - -No footer is rendered when the executor answers no `[]` grouping, and none is -requested for a table truncated by `options.limit`, whose visible rows a -whole-set total could not be reconciled against. diff --git a/.changeset/olive-donkeys-smile.md b/.changeset/olive-donkeys-smile.md deleted file mode 100644 index e643a315e7..0000000000 --- a/.changeset/olive-donkeys-smile.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/plugin-charts": minor ---- - -Dashboard chart widgets no longer render as a blank area when their height class -resolves to `auto`. - -`ChartContainer`'s min-size fallback was applied only to the wrapper `div`. -Recharts measures its own `width:100%;height:100%` size-detector element, and a -percentage height never resolves against an ancestor's `min-height`, so the -wrapper obediently grew to 280px while the measured element stayed at 0 — and -Recharts renders no children at all for a non-positive box. The result was a -widget card with its title over an empty chart area: no marks, no refusal, no -empty state, and permanent, because a box that never changes fires no resize. -The floor is now applied to the measured element as well, under the same -condition, so an author's explicit height still wins. diff --git a/.changeset/olive-donkeys-smoke.md b/.changeset/olive-donkeys-smoke.md deleted file mode 100644 index 890c2a7f3d..0000000000 --- a/.changeset/olive-donkeys-smoke.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Docs-only: correct the stale docstring on the Studio Data-pillar grid-columns pin -(`StudioDesignSurface.gridColumns.test.tsx`). Since cloud#1652 added the -`publishedFieldNames` filter to `gridColumns`, the second case exercises -identity-churn liveness rather than a real column change, and its prose said the -opposite. Comment and test title only — no assertion, no source, no published -behaviour changes. objectui#6729. diff --git a/.changeset/olive-mails-repeat.md b/.changeset/olive-mails-repeat.md deleted file mode 100644 index 488ed71f25..0000000000 --- a/.changeset/olive-mails-repeat.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -**Bug — a `code`/`text` value whose text is JSON rendered as the literal `[Object]`.** `coerceToSafeValue` classified strings by SHAPE: any string starting `{`/`[` and ending `}`/`]` was `JSON.parse`d and the result run through the reference-label extraction (`name || label || externalId || id || _id || '[Object]'`), which answers the placeholder for an object carrying none of those keys. Every text-like cell reaches that helper — `text`, `textarea`, `code`, `time`, `auto_number` and `qrcode` all register to `TextCellRenderer` — so a stored `{"ok": true}` displayed as `[Object]`, and `[1, 2, 3]` in a text field displayed as `1, 2, 3`. - -A string is now returned verbatim, whatever its shape. The reference case the parse was written for (an unresolved external-id reference arriving as `'{"externalId":"…"}'`) belongs to reference-TYPED columns and is already handled there: `LookupCellRenderer` carries its own JSON-string branch, which resolves the label through the referenced object's schema and links to the record — neither of which the type-blind helper could do. The behaviour is scoped to the column type that owns it, not dropped. Object and array VALUES still coerce, so React error #310 stays fixed. diff --git a/.changeset/olive-moons-reclassify.md b/.changeset/olive-moons-reclassify.md deleted file mode 100644 index c01ea206f0..0000000000 --- a/.changeset/olive-moons-reclassify.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only. The zod-mirror parity guard's declared-but-unmirrored ledger is split by -remedy: 23 callback-shaped keys move out of `UnmirroredDeclared` (121 to 98 keys) into -a separately pinned `RuntimeOnlyDeclared`, reconciled against the same measurement -through the union of both ledgers. No published type, no mirror and no runtime -behaviour changes; nothing releases. diff --git a/.changeset/olive-moons-shave.md b/.changeset/olive-moons-shave.md deleted file mode 100644 index 507ad25d28..0000000000 --- a/.changeset/olive-moons-shave.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Internal test-support change, no user-visible behaviour (objectui#6924). - -The 17 hand-written `(Schema as { options?: readonly string[] }).options` casts -across 16 spec-parity test files converge onto `@object-ui/test-support`'s -`enumOptions(node)` — a sibling entry point onto the wrapper walk -`shapeEnumOptions` already carried, which now delegates to it rather than -holding a second copy. Only test files, the private (never-published) -`@object-ui/test-support`, and `devDependencies` edges change; no package's -shipped `dist/` and no public type moves. diff --git a/.changeset/olive-pears-invent.md b/.changeset/olive-pears-invent.md deleted file mode 100644 index 8c303f8953..0000000000 --- a/.changeset/olive-pears-invent.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Declare the batch write-warning's unattributed-strip placeholder in -`@object-ui/data-objectstack`, and pin what it is (objectui#7160). - -`notifyBatchDroppedFields` resolves the object a cross-object strip is about -from the wire entry's `object`, else from the operation its `index` addresses. -When neither channel answers, it wrote a bare `''` — a value satisfying the -spec's required `object: string` while naming no object at all. That literal is -now the named, documented `UNATTRIBUTED_STRIP_OBJECT`, carrying the reachability -argument, why such a strip is still emitted rather than refused, and why the -placeholder must stay falsy. A stale comment claiming an unattributable strip -"reads as an update" is corrected to what the code does (it lands on `create`, -tracked as objectui#7170). - -No published behaviour changes and no released surface moves: the value is -unchanged, and the new boundary suite passes identically against the pre-change -source. The emitted `dist/index.d.ts` is byte-identical at both shas, with a -live control (one temporary exported const) proving the comparison detects a -real surface change. - -`node scripts/check-changeset-presence.mjs` on this tree, verbatim: - -> ✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/olive-pears-invent.md. -> Every one of them has an EMPTY frontmatter — declared as releasing nothing, which -> is the explicit exemption and a complete answer to this gate. diff --git a/.changeset/olive-pugs-shave.md b/.changeset/olive-pugs-shave.md deleted file mode 100644 index 18c839a639..0000000000 --- a/.changeset/olive-pugs-shave.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/components': patch ---- - -Stop the record header title from collapsing when the action tail is wide - -The `page:header` record header laid the title column and the action tail out -in a `flex-nowrap` row. The tail is `shrink-0`, so the title column — the only -flexible item — absorbed the entire width deficit. On a record carrying three -labelled `record_header` actions plus the `⋯` and `⟳` chrome, a 799px viewport -left the title column 29.5px of a 687px header and the `h1` rendered as a -single character and an ellipsis, while the breadcrumb above it still showed -the full record name. - -The title column now carries a 12rem floor and the header may wrap at `sm` and -up, so a tail that cannot fit alongside a readable title drops to its own line -instead of starving it. Below `sm` the header is already a column and is -unchanged. Headers whose tail already fits keep their single-line layout. diff --git a/.changeset/olive-pugs-sing.md b/.changeset/olive-pugs-sing.md deleted file mode 100644 index 0ebe368860..0000000000 --- a/.changeset/olive-pugs-sing.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -Dashboard record fields: percent columns now render through the one percent -scaling decision instead of a second, drifted copy of it. - -`renderFieldValue`'s `%`-format branch normalised the value itself before -calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`, -then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`, -which `@object-ui/core` documents as the single source of truth for percent -display scaling, so the branch was re-deciding what core owns — and its copy had -drifted from it in three measured ways: - -- `(value / 100) * 100` is not value-preserving in binary floating point, - re-introducing one call frame upstream the round trip that was removed from - inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000 - values change bit pattern and 1,108 rendered strings move, every one a - last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is - `1.61%`. -- A stored fraction below `0.01` was scaled twice — the local `* 100` put it - back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered - `50.00%`. -- The local test was `value > 1` rather than core's symmetric `|value| < 1`, so - a negative already in percentage points took the fraction arm: `-5` rendered - `-500.00%`. - -The branch now hands the raw stored value to `formatPercent` — the identical -call the list-view percent cell already makes for an ordinary percent column — -so a percent reads the same as a record field, as a grid cell and as a dashboard -measure. Output moves where it was wrong: values at or above 1 whose round trip -lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly -`1`, which is one percentage point by core's convention and now renders -`1.00%` at two decimals, where the local `value > 1` test had made it -`100.00%`. diff --git a/.changeset/one-origin-dev-configs-5745.md b/.changeset/one-origin-dev-configs-5745.md deleted file mode 100644 index a9ed98970c..0000000000 --- a/.changeset/one-origin-dev-configs-5745.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Dev/build configuration only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. No package `src/` is touched. The -files changed are `apps/console/.env.development`, `examples/console-starter/.env.*` -and `examples/console-starter/vite.config.ts`; `.env.development` is not part of -`@object-ui/console`'s published artifact (`files: ["dist"]`, built from -`.env.production`, which is untouched), and the example is private and on the -changeset `ignore` list. - -Converge the dev stacks on one origin: empty `VITE_SERVER_URL` and let the Vite -proxy do the split-host hop. - -Both dev env files pointed `VITE_SERVER_URL` at `http://localhost:3000` while the -page was served from `:5180` (console) and `:5173` (starter). Every client in these -apps coalesces an unset value to `''` and then builds a relative `/api/...` URL, so -an empty value routes same-origin through the dev proxy instead. `console-starter` -had no `server` block at all, so it gets the same one-stanza `/api` proxy -(`DEV_PROXY_TARGET` or `http://localhost:3000`) that `apps/console` already carries; -no port is pinned, because its README documents the example on Vite's default 5173. - -This is the ruled prerequisite for the `sameOriginOnly` action-runtime default. Under -that default a non-empty `VITE_SERVER_URL` makes every relative-target `type: 'api'` -action resolve cross-origin and be fetched bare — no `Authorization`, no -`X-Tenant-ID`, no `Accept-Language` — i.e. a 401 for the standard `pnpm dev` stack. - -`examples/console-starter/.env.production` is emptied too. Its committed -`https://demo.objectstack.ai` was not a deliberate choice for the example: it is the -same pre-convergence value the sibling console carried until 2026-05-24, when -`c351c9604` ("default published SPA to same-origin") cleared it there for CORS-blank --page reasons and touched only `apps/console/.env.production`. The starter was left -behind by that commit, and as a fork-ready scaffold its committed value is the -production origin every fork inherits. Split-origin deployments inject -`VITE_SERVER_URL` at build time, the way the console's own `.env.production` already -documents. diff --git a/.changeset/opennativepicker-coverage-7570.md b/.changeset/opennativepicker-coverage-7570.md deleted file mode 100644 index d615beca89..0000000000 --- a/.changeset/opennativepicker-coverage-7570.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: pin the `openNativePicker` click path through `DateField` — that -clicking the field invokes the helper on its own input, and that a host-supplied -`onClick` arriving through DOM props is not swallowed (objectui#7570). No published -behaviour changes. diff --git a/.changeset/overlay-slot-requiredness-7106.md b/.changeset/overlay-slot-requiredness-7106.md deleted file mode 100644 index ff0088abf8..0000000000 --- a/.changeset/overlay-slot-requiredness-7106.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Docs-only fix: `alert-dialog.mdx` and `sheet.mdx` published `trigger`/`content` -node slots as required; the shipped `AlertDialogSchema`/`SheetSchema` -declarations (and their Zod mirrors) already declare them optional. No -declaration changes; test-only update to the fixture pin that records this -class of divergence. diff --git a/.changeset/owd-width-single-home-5477.md b/.changeset/owd-width-single-home-5477.md deleted file mode 100644 index 10aa33c86c..0000000000 --- a/.changeset/owd-width-single-home-5477.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ObjectSettingsPanel` now calls `isExternalWider` from `owd-sharing.ts` instead of -re-declaring `OWD_WIDTH` and the ADR-0090 D11 width comparison inline (objectui#5477). - -`owd-sharing.ts` states its own purpose — it is "the single home" for the pieces the -per-object Settings tab and the package-level OWD overview must agree on, the D11 -"external ≤ internal" comparison among them — and the Settings tab was the one surface -not calling it. The two implementations were verified equivalent before the swap, over -the full domain of both dials plus `undefined`, the rejected legacy aliases and -prototype-chain keys: 144 input pairs, zero disagreements. The module's extra `!!` -truthiness guards are redundant at this call site, which already normalizes both dials -to `''`, and `'' in OWD_WIDTH` is false regardless. So no author-visible verdict -changes — the same three pairs warn, and the same twenty-two stay calm. - -The substantive half is the pin that keeps it that way. `ObjectSettingsPanel.owdAgreement.test.tsx` -drives BOTH surfaces over the full 5×5 cross-product of the values their dials offer and -requires all three legs — the Settings tab's rendered warning, the overview's per-row -error, and `isExternalWider` itself — to agree on every pair, with the violating pairs -pinned by name so the sweep cannot pass vacuously. Re-inlining a drifted copy into either -surface, swapping the `(internal, external)` argument order, or refining D11 in only one -place now fails a test instead of silently leaving the authoring surface enforcing the -old rule. diff --git a/.changeset/page-source-tailwind-framing-5461.md b/.changeset/page-source-tailwind-framing-5461.md deleted file mode 100644 index 4762675119..0000000000 --- a/.changeset/page-source-tailwind-framing-5461.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted. - -This is a published type surface: the TSDoc ships in `@object-ui/types`'s built -`.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'` -page is "constrained JSX/HTML + Tailwind" — and it links -`content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to -say the opposite. Shipped type documentation was contradicting the guide it points -readers to. - -ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that -framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind -is compiled at build time by scanning the console's own `src`, and there is no -safelist — so an authored utility class produces CSS only by coincidence, when -objectui already ships that exact class, and otherwise produces nothing with no -error anywhere. That is the ADR-0065 failure mode verbatim ("works only by -coincidence"), and it is how a modal's `bg-black/50` backdrop reached production -fully transparent. - -The tiers themselves are unchanged, and every load-bearing claim in the TSDoc -survives verbatim — parse-never-execute and untrusted-author safety for `html`, -the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the -`react-pages` host capability for `react`, the ADR-0080 citation and the guide -link. Only the styling conclusion changes, to the primitive each tier actually has: - -| `kind` | Style with | -|---|---| -| `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows -light/dark and whatever theme the deployment installs. The TSDoc now also names the -rule that reports a violation — `page-source-className-tailwind`, shipped in -`@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by -`os validate` as a warning on both tiers. - -No behaviour change, and the accepted `kind` set is untouched. - -`packages/components/src/renderers/layout/react-page.tsx` carries the same -correction on its two source comments (the injected-scope note and -`buildComponentScope`), and gains the styling note the file was missing. Those are -internal comments — they do not project into any `.d.ts` and change no export — so -they get no entry of their own; there is nothing an `@object-ui/components` -consumer could read in a CHANGELOG and act on. diff --git a/.changeset/page-source-tailwind-prose-retraction-5469.md b/.changeset/page-source-tailwind-prose-retraction-5469.md deleted file mode 100644 index f125f38941..0000000000 --- a/.changeset/page-source-tailwind-prose-retraction-5469.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/react-runtime': patch -'@object-ui/sdui-parser': patch -'@object-ui/components': patch ---- - -Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's -`source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, -Accepted) retracted. objectui#5461 corrected three sites; a multiline census -found eight more, in three spellings a line-oriented grep could not see. - -A page's `source` is *runtime metadata*. The console's Tailwind is compiled at -build time by scanning the console's own `src`, and there is no safelist, so it -never sees your page: an authored utility class produces CSS only by coincidence -(when objectui already ships that exact class) and otherwise produces nothing, -with no error anywhere. That is the ADR-0065 "works only by coincidence" failure -mode, and it is how a modal's `bg-black/50` backdrop reached production fully -transparent. `os validate` reports it as `page-source-className-tailwind`, a -warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. - -The tiers themselves are unchanged and every load-bearing claim survives — -parse-never-execute, the untrusted-author safety argument for `html`, and the -deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording -`content/docs/guide/react-pages.md` §Styling already uses: - -| `kind` | Style with | -|---|---| -| `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`. - -Why each package has an entry — each was measured against its built artefact, not -assumed: - -- **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes - `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is - the paragraph that routes untrusted-author work to the `html` tier, and it - carried the retracted framing line-wrapped across `:17-18`. It also gains the - §Styling section it was missing — the absence is why the framing survived here. -- **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects - verbatim into the published `dist/types.d.ts`. -- **`@object-ui/components`**: the corrected header of - `src/renderers/basic/html-elements.tsx` projects verbatim into the published - `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm - comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a - function body) and is included here only because the same package already owes - an entry. - -No behaviour change: this is prose only. `CHANGELOG.md` occurrences are -deliberately untouched — immutable release history. diff --git a/.changeset/palette-canonical-discussion-5495.md b/.changeset/palette-canonical-discussion-5495.md deleted file mode 100644 index 18563f664d..0000000000 --- a/.changeset/palette-canonical-discussion-5495.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -**The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.** - -The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495. - -**The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered". - -One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one. diff --git a/.changeset/palette-exclusion-reason-truthfulness.md b/.changeset/palette-exclusion-reason-truthfulness.md deleted file mode 100644 index ad1bcf1077..0000000000 --- a/.changeset/palette-exclusion-reason-truthfulness.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Corrects two `PALETTE_EXCLUSIONS` reason strings in the Studio page-palette ledger that -claimed "no renderer" for `element:text_input` and `element:record_picker`, both of which -have registered renderers, and pins the class with a test. The exclusions themselves are -unchanged decisions and no published behaviour moves: the reason strings are developer-facing -ledger prose, read by no runtime code path. diff --git a/.changeset/param-dialog-title-fallback-4282.md b/.changeset/param-dialog-title-fallback-4282.md deleted file mode 100644 index 41c71b6810..0000000000 --- a/.changeset/param-dialog-title-fallback-4282.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's param-collection dialog now titles itself from `action.label` alone — -the unreachable `|| action?.title` fallback beside it is removed (objectui#4282). - -`title` is declared on no action surface in the ecosystem: it is absent from -`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from -`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` -inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` -`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, -`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand -side of that `||` could not be reached by authored metadata: a fallback that cannot -fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. -Nothing a user hits changes; the line now reads exactly one key, matching the -`description` line directly below it. - -`useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader so the alias cannot -be reinstated silently: an action carrying `title` and no `label` must open an untitled -dialog rather than a dialog named by a key no producer sets. diff --git a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md b/.changeset/paramtofield-reference-rule-derives-from-core-5312.md deleted file mode 100644 index a3aa770c31..0000000000 --- a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ActionParamDialog`'s "this param carries a reference target" rule now derives from the shared reference-field family instead of the last private copy of it. - -`packages/app-shell/src/utils/paramToField.ts` restated the rule inline as -`LOOKUP_WIDGET_TYPES.has(type) || type === 'user'` — the fourth and last -hand-maintained answer to one question ("does this widget resolve a foreign key, -so hand it `reference_to` / `display_field` / the rest of the picker config"). -The other three converged on `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` in -objectui#4770 / #4790 / #4815; this face is now the fourth. - -No reachable behaviour change. The shared set is one member wider (`tree`), and -that member can never be a widget key on this surface: it is absent from -`fields`' widget map and `mapFieldTypeToFormType` sends it to `field:lookup`, so -every key the rule tests arrives as `lookup`. Both halves are pinned, so -registering a real `tree` widget surfaces the change instead of shipping it -silently. - -The module's second rule — which widget keys degrade to a text input for want of -a declared `referenceTo` — is a different set over overlapping types (`user` -defaults its target to `sys_user` and must never degrade) and was deliberately -left un-merged, matching the same split the plugin-grid twin keeps. - -Also retires a comment that claimed the disjunction "moves in lockstep with -plugin-grid's `bulkParamToField` twin — the two param faces are never split". -Measured on the tip before this change, that was false in both senses: the twin -had read core's Set since objectui#4815 while this line read a private literal, -so the two shared nothing and no gate could report a split; and the two member -sets already differed, by `tree`. Lockstep now holds mechanically — the pin is -on object identity (a spy on core's `has`), so a member-identical private copy -fails where a value check would pass. diff --git a/.changeset/parity-gate-lazy-registrations.md b/.changeset/parity-gate-lazy-registrations.md deleted file mode 100644 index c6a27a5a2c..0000000000 --- a/.changeset/parity-gate-lazy-registrations.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change to the console's registry/spec parity gate (`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`): it now loads the two spec-carried plugin blocks the console registers lazily, and asserts its own coverage so a block can no longer fall out of the judged population silently. Also adds one member pin next to the renderer it constrains (`packages/plugin-kanban/src/__tests__/ObjectKanban.filterMembersReachTheWire-8176.test.tsx`), so the `filter` key objectui#8186 declared is answered with a pin rather than with an exemption. No published behaviour changes — no renderer, registration or runtime file is touched. diff --git a/.changeset/paywall-moment-exit-and-failure-state-7253.md b/.changeset/paywall-moment-exit-and-failure-state-7253.md deleted file mode 100644 index 83fe84d5f9..0000000000 --- a/.changeset/paywall-moment-exit-and-failure-state-7253.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-chatbot': minor ---- - -Fix the three breaks at the AI paywall moment (#7253), measured on a free plan's -second build iteration when the cloud guardrail refuses "Confirm changes" with a -429 `AI_DESIGN_QUOTA_EXHAUSTED`. - -- **The upgrade CTA no longer opens a 404.** It used to open a client-composed - `${cloudBase}/apps/cloud-control/sys_environment`, which guessed the control - plane's console mount, app slug and route — all three wrong — and landed on - the API's `ENDPOINT_NOT_FOUND` JSON. `cloudInstallDeepLink` / - `cloudPricingDeepLink` are replaced by `cloudConsoleUrl()`, the - runtime-supplied cloud origin with no path appended; the control plane's own - root redirect decides the landing page. The former - `|| 'https://cloud.objectos.app'` default is gone: a runtime with no upstream - cloud now renders no upgrade link at all rather than pointing a self-hosted - user at the vendor's SaaS. -- **The confirm card gets an explicit failure state.** A quota refusal parks the - card on "not applied" with the server's own next step (reset tomorrow / - upgrade) plus the upgrade action, instead of silently rolling back to - "Confirm / Adjust" as though the click had never happened. Transient failures - (offline, per-minute rate limit) still roll back, because retrying is the - right next step there. -- **The composer is no longer refilled with an already-delivered message.** - Only text typed into the composer is restorable now; card-driven sends - (confirm, approve, suggestion chips) send canned text the user never typed and - no longer leave the previous prompt staged as if it needed resending. diff --git a/.changeset/pending-drafts-bar-locale-parity-5705.md b/.changeset/pending-drafts-bar-locale-parity-5705.md deleted file mode 100644 index 6bafb84358..0000000000 --- a/.changeset/pending-drafts-bar-locale-parity-5705.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings — -now exists in all ten locale packs. It previously existed only in `en` and `zh`, so -`ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and -`all-locales-key-parity` failed on `main` (objectui#5705). - -The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was -titled for the locale packs but reached only `zh`, so eight packs × five keys stayed -missing and the parity assertion — which carries no allowlist — was red on `main` and -on every PR whose diff touched source. Source-free diffs skip the shard that runs it, -which is why the breakage survived several merges. - -Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`, -`ru` puts the number last (`…: {{count}}`) as it already does for the sibling -`home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}` -because that value is a runtime message rather than a single token — both choices -carry an in-pack note. Terminology is taken from each pack's existing publish-bar -vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read -alike. - -Both interpolations survive verbatim in every pack — `{{count}}` in `count` and -`{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated -packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a -different node and is untouched. diff --git a/.changeset/pending-drafts-i18n-keys.md b/.changeset/pending-drafts-i18n-keys.md deleted file mode 100644 index 84dab23b48..0000000000 --- a/.changeset/pending-drafts-i18n-keys.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/app-shell': patch ---- - -Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. diff --git a/.changeset/per-user-active-org-and-session-user-purge-5664.md b/.changeset/per-user-active-org-and-session-user-purge-5664.md deleted file mode 100644 index 45bdd142c8..0000000000 --- a/.changeset/per-user-active-org-and-session-user-purge-5664.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@object-ui/auth': patch -'@object-ui/console': patch ---- - -The active organization id is now stored per user, and a change of session user drops the -previous user's client state wholesale (objectui#5664). - -`auth-active-organization-id` was a single un-namespaced `localStorage` key while its -siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, -`flow-palette-recents:u:`). On a browser handed from one account to another — a shared -machine, a kiosk, a handover, a support session — the arriving user's console read the -PREVIOUS user's organization id. The header workspace chip rendered the previous user's -workspace for a user whose `organization/list` was empty, and the consequence past the -cosmetics is the one worth stating: the polluted org context suppressed -`RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so -a brand-new user on that browser silently never got the new-user flow at all. - -Nothing about row visibility rode on this. With the stale id the server answers -`403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero -environments; the damage was entirely in what the client believed about itself. - -Three changes, and the third is the one that closes the class rather than the instance: - -- The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention - its siblings already use. -- It can no longer be written un-namespaced at all. Where no session user is known yet the - value lives in memory for that page-load only — a namespacing that kept a bare-key - fallback would re-open the defect the first time a write happened before the user id - resolved. -- **A change of session user drops the previous user's client state wholesale.** This is an - allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so - the NEXT storage key someone adds without a `:u:` scope is covered before it is written. - Only device-scoped entries survive: the arriving session's own bearer token, the pointer - recording whose state the browser holds, and the UI theme. - -Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a -non-null `localStorage` read and falls back to the in-memory value, and the memory value is -nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge -too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- -sign-in path that never reloads the page. - -Existing browsers are not migrated. A value sitting under the retired bare key is -unattributable — nothing recorded whose org id it is — so migrating it is precisely the -defect it would be migrating away from, and it is deleted instead. A signed-in user loses -nothing durable: the active organization is a server-owned fact that -`AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no -active org is held, including the ADR-0081 single-membership repair. One boot re-supplies -it; users with no organization land on the guided first-run flow, which is the outcome this -card is about. - -`apps/console`'s pre-render auth preflight purges every spelling of the active-org key — -the retired bare one and each `:u:` scope — when it finds a dead bearer token, and -deliberately leaves the session-user pointer in place so the next sign-in can still tell -that the browser changed hands. diff --git a/.changeset/per-view-config-alias-fold-2890.md b/.changeset/per-view-config-alias-fold-2890.md deleted file mode 100644 index 1636fc20ce..0000000000 --- a/.changeset/per-view-config-alias-fold-2890.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`normalizeListViewSchema` now folds the four per-view-type config aliases phase 3 -carried over — `kanban.groupField` → `groupByField`, `kanban.cardFields` → -`columns`, `gallery.imageField` → `coverField`, `timeline.dateField` → -`startDateField` (objectui#2890). - -These are the pre-#2231 objectui spellings, kept declared alongside the spec keys -so stored view metadata would keep validating. They now fold at the same -component boundary as the A1–A5 vocabulary folds, in the same one-directional -shape: the canonical key wins when a config carries both, and the legacy key is -removed from the result so a missed read-site fails loudly instead of quietly -taking the legacy path. - -One rendering behaviour changes, and it is a correction of the same inverted -precedence the `densityMode` fold fixed: `ListView`'s kanban adapter resolves the -card field list as `cardFields || columns` — legacy over canonical — so a kanban -config carrying **both** rendered the legacy `cardFields` value and silently -ignored the spec-canonical `columns`. After the fold the authored `columns` is -what reaches it. Configs carrying only one of the two are unaffected, and every -other reader of these four keys was already canonical-first. - -`calendar.defaultView` is deliberately **not** folded: it aliases nothing and has -no spec counterpart, so it wants promotion upstream rather than a rename. diff --git a/.changeset/percent-figure-grid-citation-5608.md b/.changeset/percent-figure-grid-citation-5608.md deleted file mode 100644 index 7c6075e49b..0000000000 --- a/.changeset/percent-figure-grid-citation-5608.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Comments only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared (objectui#5608). - -Two live sites still carried the bare `27,581 of 1,200,013` figure with neither the grid -nor the measured surface named: the `DisplayNumberFormatOptions.style` doc comment and a -comment in the percentage-points test. Both were accurate. Neither was *reconcilable* — a -reader who had also seen `packages/fields`'s `27,577 of 1,200,003` met two counts that -look like a contradiction, and a figure a reader cannot reconcile is what makes a correct -comment get distrusted wholesale. That is the same defect class objectui#4596 was queued -for, and the wording here is lifted from its PR rather than phrased a third way — a third -phrasing for one fact is the defect one level up. - -Both counts stand unchanged. The disambiguator is the surface measured, so each site now -names it: objectui#4576's tie-dense grid (0.005 steps to 2,000, precisions 0/1/2) on -`formatDisplayNumber` / `formatMeasure`'s call shape, against objectui#4590's re-measure -of the same route through `formatPercent`. No value, assertion or behaviour moves. - -Why the empty form is the right declaration, stated from what was measured rather than -carried over from objectui#4596's PR — the two cases are mirror images. There, a function -BODY comment was changed, so the emitted `.js` moved and the `.d.ts` did not. Here the -changed source comment is the JSDoc on an exported interface member: the interface erases -at runtime, so the new prose reaches `dist/utils/number-display.d.ts` and is absent from -`dist/utils/number-display.js` (checked on a real build of the package). The second site -is a test file, excluded from the build program by `src/**/__tests__/**` and never in -`dist` at all. - -So a consumer-visible byte does move — the text shown on hover. That is documentation of -an existing measurement, not a change to the measurement, the type, or any behaviour: -nothing a consumer types against or calls is different, which is why this releases -nothing. Bytes moving is not the criterion; released behaviour and consumer-visible -surface are, and neither moves. diff --git a/.changeset/pin-advisory-sink-seam-6969.md b/.changeset/pin-advisory-sink-seam-6969.md deleted file mode 100644 index be477420c6..0000000000 --- a/.changeset/pin-advisory-sink-seam-6969.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: pins the metadata advisory sink seam (`useMetadataClient` -> -`createConsoleMetadataClient` -> `MetadataClient`), so an advisory the server -returns on a save or a publish is asserted to actually reach the toast. No -published behaviour changes. diff --git a/.changeset/pin-ledger-docstring-key-totals.md b/.changeset/pin-ledger-docstring-key-totals.md deleted file mode 100644 index f38646b69f..0000000000 --- a/.changeset/pin-ledger-docstring-key-totals.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Test-only change to `@object-ui/types`' zod-mirror-parity pin file. Both LEDGER -DOCSTRINGS — above `interface UnmirroredDeclared` and above `interface -RuntimeOnlyDeclared` — stated that ledger records **94 keys**; two independent -instruments measure **87**, wrong since objectui#7779 and fully green the whole time -(objectui#8243). Those two live figures are corrected and are now derived from the -ledger through the docstrings' own spelling. The stale decomposition beside them -(`94 − 1 seeded + 3 mirrored + 2 retired + 23 reclassified`) needs key provenance no -ledger here records, so instead of being refreshed or deleted it is rewritten as a -reading at NAMED REVISIONS — 98 keys at `beccf1c6b`, of which 85 survive and 13 have -left at `ed7178bf3` — which cannot rot and needs no hand re-derivation at the next -repair. The measured 13 and the prose-derived 10 / 3 split of it are kept visibly -apart. No published behaviour changes. diff --git a/.changeset/pin-zod-mirror-parity-header-key-totals.md b/.changeset/pin-zod-mirror-parity-header-key-totals.md deleted file mode 100644 index 22f7df993b..0000000000 --- a/.changeset/pin-zod-mirror-parity-header-key-totals.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Test-only change to `@object-ui/types`' zod-mirror-parity pin file. Its header stated -`KnownDrift` as **62 keys**; two independent instruments measure **63**, wrong since -objectui#7664 and green the whole time (objectui#8222). The digit is corrected and the -remaining LIVE figures in that header are closed out: the two unpinned key totals and -the three cross-ledger restatements are now derived from the ledgers through the -header's own spelling, and the one figure that cannot be derived is excluded in -writing with its reason. No published behaviour changes. diff --git a/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md b/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md deleted file mode 100644 index 0409f99ecd..0000000000 --- a/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change to `@object-ui/types`' zod-mirror-parity pin file: the three LEDGER -entry counts its header states, and the ratchet section's restatement of the first of -them, are now compared to the ledgers they describe (objectui#7733). One of those -figures was already wrong on `main`. No published behaviour changes. diff --git a/.changeset/pivot-table-stable-empty-rows-5562.md b/.changeset/pivot-table-stable-empty-rows-5562.md deleted file mode 100644 index d7423a5b53..0000000000 --- a/.changeset/pivot-table-stable-empty-rows-5562.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`PivotTable` no longer re-runs its cross-tabulation memo on every render when it -has no rows (objectui#5562). - -The component spelled the empty array twice — as the destructuring default for -`schema.data` and as the `Array.isArray` fallback that keeps a provider-config -object out of iteration — so a schema declaring no `data` key, or one whose -`data` is a provider config rather than rows, produced a fresh array identity on -every render. That value is the first entry of the memo's dependency list, so -the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the -aggregated matrix and the row/column/grand totals on every render, over nothing. -Both spellings now resolve to one module-scope frozen empty, so "no rows" is a -stable value and the memo holds. - -Wasted work only: the churn feeds a memo rather than a `setState`, and -`PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no -render loop was possible. The identical fix landed for `data-table` in -objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the -direct-use path those two did not cover, where `DashboardRenderer` and -`DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the -chain. diff --git a/.changeset/placeholder-spelling-one-rule.md b/.changeset/placeholder-spelling-one-rule.md deleted file mode 100644 index 1a727b92fa..0000000000 --- a/.changeset/placeholder-spelling-one-rule.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Tooling and test-only (objectui#7310): the placeholder-spelling rule that -objectui#3512's gate and `check-i18n-call-site-keys.mjs` class 7 each carried a -copy of now lives once, in `scripts/placeholder-spelling.mjs`, which both -import. No published behaviour changes — `scripts/` ships in no package and -`packages/i18n`'s `__tests__/` is excluded from its build. The merged -implementation was proven byte-equivalent to both copies over a 23,420-input -corpus before either was deleted, and the gate's full output is unchanged. diff --git a/.changeset/plain-taxis-attend.md b/.changeset/plain-taxis-attend.md deleted file mode 100644 index 4b7cd354ce..0000000000 --- a/.changeset/plain-taxis-attend.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Approvals drawer: the business summary card no longer renders a field the object -declares `hidden: true`. - -`payloadSummary` built the card from the request's `payload_json` snapshot behind -five filters (system keys, the lead amount key, null/object/empty values, -unresolved opaque ids, a six-field cut) and no field-visibility filter, so a -hidden field that survived to the first six survivors rendered in the card, -labelled. The drawer now reads the open request's object metadata and drops the -declared-hidden keys before the six-field cut, so the next business field is -promoted into the freed slot rather than the card silently shrinking. The lead -amount figure at the top of the same card takes the same trim. - -Per the platform ruling, `hidden: true` is a UI-only contract and `internal: true` -is the serialization primitive, so this is the UI enforcing the only contract -`hidden` has — not a client-side compensation. Field-level security is unchanged -and remains the server's answer. The metadata read is the same cached -`GET /meta/object/:name` the record form already performs, once per object per -page visit, and an unanswered read leaves the card exactly as it renders today. diff --git a/.changeset/plugin-component-input-deprecate-5674.md b/.changeset/plugin-component-input-deprecate-5674.md deleted file mode 100644 index 7f89a7f1de..0000000000 --- a/.changeset/plugin-component-input-deprecate-5674.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`PluginComponentInput` is deprecated in favour of `ComponentInput` (objectui#5674). - -This is stage 1 of a two-stage retirement the maintainer ruled on 2026-08-22: deprecate -for a release, then remove. Nothing is removed here — the export still exists and still -names the same type. - -`@object-ui/types`' entry point publishes `ComponentInput as PluginComponentInput`. Until -objectui#4972 that alias pointed at a genuinely different declaration: `plugin-scope.ts` -restated its own nine-key `ComponentInput`. objectui#5671 converged that declaration onto -`base.ts`, so the alias became a second published name for the *same* type, carrying no -information the first does not. Two published names for one type is a shape that costs -readers a step and gives AI-authored code a coin-flip between spellings. - -**Why a deprecation window rather than a deletion.** The measurement that licenses -deleting an export from a published package is *"no importer"*, and what can be measured -from inside this repository is only *"no importer here"*. In-repo the name has zero -importers — searched across every root (`packages/`, `apps/`, `content/`, `docs/`, -`skills/`, `examples/`, `e2e/`, `scripts/`, and the root docs), plus the sibling framework -repository, with a control name searched identically so a broken search could not read as -a clean one; the only occurrences are the alias itself and prose about it. What no search -here can see is a consumer on npm. The deprecation window is the answer to that -unmeasurable half: it converts a silent break into a warned one before the removal lands. - -**Reversibility, deliberately.** Deprecating is undoable; deleting a published export is -not. Where the evidence is one-sided, the retirement takes the reversible step first. - -The tag reaches consumers rather than only the source: a JSDoc block on an export -specifier survives declaration emit and lands attached to that specifier in the emitted -`index.d.ts`, which was measured for this change rather than assumed. With comments -stripped, the emitted entry point is byte-identical before and after — the published -*type* surface does not move, it only gains the notice. - -Stage 2 (removing the alias, and the now-dead re-export in `plugin-scope.ts` that exists -only to feed it) is filed as a follow-up and ships as a `minor`, per this repo's policy -that its own breaking changes never declare `major`. diff --git a/.changeset/plugin-form-readme-classname-quantifier-5131.md b/.changeset/plugin-form-readme-classname-quantifier-5131.md deleted file mode 100644 index 112411d63c..0000000000 --- a/.changeset/plugin-form-readme-classname-quantifier-5131.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`README.md`'s "Not a `FormField` key" table said a field-level `className` is -"read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier -holds only for the renderer's *explicit* read — `className={fp.className}` on -the `section-divider` branch of -`packages/components/src/renderers/form/form.tsx`. The same renderer forwards -every key it did not destructure, and `className` is not among the names taken -off the field config, not among the ones `stripRendererOnlyProps` removes, and -so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input` -branch spreads it onto `<Input>`. A field-level `className` therefore lands -visibly on ordinary built-in controls, and a reader taking "exactly one" -literally concludes the opposite of what the code does (objectui#5131). - -The cell now describes the contract rather than the reader count: an undeclared -key still rides the props spread down to whichever component the field resolves -to, nothing in the contract promises that, and a registered widget honours it -only if it happens to spread its leftover props — the wording the docs site -already ships, so the two sources agree again. The advice in the row is -unchanged and was never wrong (`span` / `colSpan` for width, -`FormSchema.fieldContainerClass` for the grid), and the explicit -`section-divider` read is kept, now named as explicit. - -This is a documentation fix to a file `plugin-form` publishes to npm, which is -why it carries a version: the npm landing page only picks up the correction on a -release. No behaviour, export, type, or `dist` byte changes. diff --git a/.changeset/plugin-map-reach-test-narration.md b/.changeset/plugin-map-reach-test-narration.md deleted file mode 100644 index 7fe78586d7..0000000000 --- a/.changeset/plugin-map-reach-test-narration.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Comment-only correction in `packages/plugin-map/src/ObjectMap.listViewMapConfigReach.test.tsx`: the narration above the `titleField` arm attributed the `'Marker'` placeholder to `getMapConfig`, which never held it. No assertion, fixture or published behaviour changes. diff --git a/.changeset/preview-sample-lookup-reference-6647.md b/.changeset/preview-sample-lookup-reference-6647.md deleted file mode 100644 index a6f2cbbcc2..0000000000 --- a/.changeset/preview-sample-lookup-reference-6647.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Dev-only + test-only change; no published behaviour changes. - -The `object` preview sample (`apps/console/src/preview-samples.ts`) now spells its -lookup target `reference` instead of the by-name-refused `reference_to`, and the -`KNOWN_STALE` ledger entry in `preview-samples-spec-valid.test.ts` records both -defects that actually remain. Neither file reaches a published entry point: -`preview-samples.ts` is imported only by `preview-gallery.tsx`, which is served -from the standalone `preview-gallery.html` Vite entry — not a `rollupOptions.input` -of the production build, so it is absent from the `dist` that `@object-ui/console` -publishes. diff --git a/.changeset/properties-template-visibility-diagnostic-5756.md b/.changeset/properties-template-visibility-diagnostic-5756.md deleted file mode 100644 index 5d050acf44..0000000000 --- a/.changeset/properties-template-visibility-diagnostic-5756.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated -spelling of the objectui#5454/#5687 diagnostics when it is written inside -`properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }` -(objectui#5756). - -The `properties.*` evaluation loop runs, and interpolates every `${…}` template it -finds, **before** the visibility gate ever sees the value — so by the time the -existing diagnostics ran, a template-spelled predicate had already collapsed into a -plain boolean and there was no predicate text left to inspect. The bare-string -spelling of the exact same gate (`properties: { visible: "data.status == 'draft'" -}`) was unaffected — nothing interpolates a string with no `${` in it — and was -already reported; only the template spelling was structurally invisible. - -Reached by moving the diagnostic check to inside the `properties` evaluation loop, -on the predicate's raw (pre-interpolation) text, gated on the key being one of the -six visibility keys the render chain consults (`visibleWhen` / `visible` / -`visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content` -interpolation, or any other non-visibility key, is untouched and stays silent. - -Reports only the key that actually **decides** the node's visibility, mirroring -objectui#5454's own leg semantics (its reporter is likewise only ever invoked on -the leg the chain's early-return sequence actually reaches): a `properties.visible` -template that a co-declared `visibleWhen` outranks is not reported for deciding -nothing. - -**No verdict changes and no interpolation changes.** The diagnostic call's return -value is discarded; the real verdict is still computed afterward, off the -post-evaluation, post-hoist schema, by the same code path as before this change. -Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data -predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate — -only the silence moved, one render-step earlier. diff --git a/.changeset/proto-instrument-fixture.md b/.changeset/proto-instrument-fixture.md deleted file mode 100644 index a3e358f90f..0000000000 --- a/.changeset/proto-instrument-fixture.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only (objectui#6524): the `__proto__` instrument fixture in -`MetadataService.objectPayloadFieldsMap.test.ts` was spelled as a plain object -literal, which per Annex B.3.1 sets the prototype instead of adding a key — so -the assertion parsed an empty `fields` map and would have stayed green if the -spec began refusing the name. Respelled as a computed key and pinned with the -literal-versus-computed control. No published behaviour changes. diff --git a/.changeset/queryparams-closed-keys-7497.md b/.changeset/queryparams-closed-keys-7497.md deleted file mode 100644 index 0e341c38ba..0000000000 --- a/.changeset/queryparams-closed-keys-7497.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`QueryParams` (`@object-ui/types`) no longer carries the `[key: string]: any` -index signature; its key set is now exactly the nine declared `$`-prefixed -members — `$select`, `$filter`, `$orderby`, `$skip`, `$top`, `$expand`, -`$search`, `$searchFields`, `$count` (objectui#7497). - -**What stops type-checking that type-checked before.** Any object literal -assigned or passed as a `QueryParams` that carries a key outside those nine — -the unprefixed spellings the readers silently dropped (`{ filter }` for -`$filter`, `{ limit }` for `$top`, `{ options: { $top } }`), and any -`$`-prefixed name that is not declared (`{ $limit }`). Reading an undeclared -key off a `QueryParams` value (`params.filter`) is refused too. A published -guide had taught `adapter.find('contacts', { filter: { active: true } })` and -asserted the client saw that `filter`: it compiled, `convertQueryParams` -dropped the key, and the assertion could never pass. That literal is now a -compile error at the call site. - -**What does not change.** Every reader in this repository — -`convertQueryParams` and `rawFindWithPopulate` in `@object-ui/data-objectstack`, -`queryParamsToRecord` in `@object-ui/core`, `ValueDataSource.find` — reads only -declared members, so nothing the runtime honoured is refused. A -`Record<string, unknown>` or `Record<string, any>` VALUE (the shape -`@objectstack/spec`'s `ViewData.params` parses to) still passes, as does a -spread of one beside `$top` / `$skip`; a type assertion (`{ limit: 1 } as -QueryParams`) still compiles because assertions skip excess-property checks, -which is why `object-ui/no-unprefixed-query-params` keeps its typed cases. - -**Why `minor` and not `patch` or `major`.** Narrowing the accepted set of a -published contract is a breaking change for any downstream caller that relied -on the extra keys. The census behind the grade: across this repository's -packages, apps, examples, e2e and scripts trees (4,362 files), zero `find` / -`findOne` call sites or `QueryParams` literals carry a non-`$` key, and no -adapter reads one — the signature carried nothing but typos. This repository -records its own breaking changes as `minor` with the break spelled out; `major` -is reserved for following `@objectstack` across a major (see AGENTS.md, version -alignment). A `patch` would be wrong: this is a deliberate narrowing, not a -fix inside the accepted set. - -Migration for a downstream caller that did pass an extra key: if an adapter of -yours reads it, declare it on your own params type and widen at your adapter's -boundary; if nothing reads it, it was already being dropped — delete it. diff --git a/.changeset/quick-reference-release-sync-5394.md b/.changeset/quick-reference-release-sync-5394.md deleted file mode 100644 index d88e63d9ed..0000000000 --- a/.changeset/quick-reference-release-sync-5394.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Release tooling and repo docs only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. - -`changeset:version` now runs `scripts/sync-quick-reference-release.mjs` after bumping the -manifests, so `QUICK_REFERENCE.md`'s "Current Release" block moves in the same commit as -the versions it quotes. Before this, the release path had no human in it and nothing -updated the doc, so the block fossilised once per release and the anti-fossil gate -`scripts/__tests__/quick-reference-current-release-4143.test.ts` reddened `main` on the -push build every time (objectui#4642, objectui#4977, objectui#5394). The gate is -unchanged — it is the judge; this is the thing that keeps the doc true. - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/radio-multiple-docblock-correction.md b/.changeset/radio-multiple-docblock-correction.md deleted file mode 100644 index 2d7522ebdd..0000000000 --- a/.changeset/radio-multiple-docblock-correction.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Comment-only correction in `@object-ui/fields`. The `MULTI_VALUE_FORM_TYPES` docblock in -`field-type-alias.ts` asserted that every `MULTI_CAPABLE_TYPES` member other than `select` -renders both arities inside one widget, naming `LookupField`, `FileField` and `ImageField` -while silently omitting `radio` — which reads `multiple` nowhere, and so is the one member -with no multi-arity renderer. The docblock now states that exception, pins it to this repo's -own single-value contract for `radio`, and points at the producer-side work that refuses the -authored combination. No published behaviour changes. diff --git a/.changeset/react-form-view-bridge-drifted-arms-5652.md b/.changeset/react-form-view-bridge-drifted-arms-5652.md deleted file mode 100644 index 3f0c9b7965..0000000000 --- a/.changeset/react-form-view-bridge-drifted-arms-5652.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/react': minor ---- - -The spec-bridge's form-view input now accepts everything `@objectstack/spec`'s -FormViewSchema accepts — three key types had drifted narrower, and one of them -had inverted (objectui#5652). - -`bridges/form-view.ts` held a third hand-written description of the FormViewSchema -contract, after the leaf (objectui#5542) and the two containers (objectui#5596) -were converged elsewhere. A description nothing compares is one spec release from -being a fork, and this one had already drifted on the keys that decide whether a -legal document renders: - -- `FormSection.columns` refused the string spelling of a column count, which the - contract admits and folds to a number in its own pipe. It is now the contract's - type, and the bridge performs the fold — the `object-form` node's section - declares `columns` as a number and its container indexes a grid-class map by - it, so forwarding `'2'` handed every downstream renderer a value outside the - type it declares. -- `FormField.dependsOn` was declared `string[]`, which is the exact inverse of the - contract: it admitted only the array arm the contract rejects, and refused the - bare parent-field name that is the one configuration making `field-selector` - and `dynamic-config` work (objectui#5040). -- `visibleWhen`, on both fields and sections, was declared `string`, so the - ADR-0089 expression object — the arm `evalFieldPredicate` reads — could not be - described at all. Both arms now travel whole onto the node. - -A section's `fields` may also be a bare object-field name, the spec shorthand the -list bridge already honours. The form bridge ran it through the object mapper -instead, producing a field with no identity (`{ name: undefined }`) for the most -ordinary section a form can declare; it is now forwarded verbatim, which is what -the node's own `fields` slot admits. - -Each drift-prone key's type is now bound to the `@objectstack/spec` symbol that -owns it rather than restated, so it cannot drift again, and the compile-time pins -in `FormViewWidenedArms.test.ts` fail if a future edit restates any of them by -hand. The documented subset itself is unchanged: the keys this bridge does not -declare, including the retirement ledger it keeps, stay exactly as they were. diff --git a/.changeset/react-pages-guide-source-contradictions-5413.md b/.changeset/react-pages-guide-source-contradictions-5413.md deleted file mode 100644 index dbc67fae9f..0000000000 --- a/.changeset/react-pages-guide-source-contradictions-5413.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Docs only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -`content/docs/guide/react-pages.md` contradicted the framework's sources on four points. -The guide is live material (`@object-ui/react-runtime`'s README links it twice), so each -one was reachable teaching. All four were re-measured against `objectstack@f094214b3` and -resolved toward the source: - -- **Tailwind styling.** The guide taught Tailwind `className` as the react tier's styling - primitive, which ADR-0080's 2026-06-30 amendment retracted under ADR-0065 (Accepted): a - page's `source` is runtime metadata the console's build-time Tailwind never scans, so an - authored utility class silently produces no CSS. Replaced with the per-tier primitive the - shipped `page-source-className-tailwind` rule names — inline `style={{ … }}` with - `hsl(var(--token))` on the react tier, structured props plus a JSON `style` object on the - html tier — and added a Styling section giving the mechanism. -- **`record:*` scope.** The family was offered as the illustrative in-scope example while - `os validate` rejects it (`react-block-needs-record-context`, severity error, matched by - type). The tag-derivation rule is kept — it is accurate — and the exclusion, the real - error text, and the per-block alternatives are now stated beside it. -- **`adapter.find` options.** The `Live data` sample passed `filters:`, which is not a - `QueryParams` key; `convertQueryParams` reads only `$`-prefixed keys, so the sample - returned the object's records unfiltered with no error. Corrected to `$filter`. The same - sample also treated the result as an array — `find` resolves to a `QueryResult`, so - `.map` on it throws; corrected to `res.data` alongside it. -- **Block inventory.** The guide conflated the runtime scope (every public non-container - block) with the authored contract (`REACT_BLOCKS`: `ObjectForm`, `ListView`, - `ObjectChart`, `Block`). Both are now stated as two tiers, with the generated - `react-blocks.md` named as the prop authority, and the flat-props example moved off two - deprecated `ObjectGrid` spellings (`pageSize`, `fields`) onto the canonical - `pagination` / `fields` on `ListView`. diff --git a/.changeset/readonly-richtext-raw-markup-5498.md b/.changeset/readonly-richtext-raw-markup-5498.md deleted file mode 100644 index c14d4ec2e2..0000000000 --- a/.changeset/readonly-richtext-raw-markup-5498.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -A readonly `markdown` / `html` / `richtext` form field now renders its content -FORMATTED instead of showing the user its markup source (objectui#5498). - -`RichTextField`'s readonly early return rendered `{value}` as a React text child, -so a readonly field of any of those three types displayed the stored markup as -literal characters — a markdown field's asterisks and hashes, a richtext field's -tags. The `prose` classes on that wrapper were the tell: they style rendered rich -content, and there was none to style. Every other read surface — grid, kanban -card, gallery, related list, dashboard record panel and the record detail page's -read mode — dispatches through `getCellRenderer` and rendered the same stored -bytes formatted, so one field disagreed with itself depending on which surface it -was read on. - -The readonly branch now renders through the same components `getCellRenderer` -resolves: `markdown` through the GFM renderer, `html` and `richtext` through the -sanitizing HTML renderer. The two renderers moved out of the package barrel into -`widgets/richTextDisplay.tsx` so the widget can reach them without importing the -barrel back, and both sides now read one shared type-to-renderer table rather -than two that can drift apart. - -The editor header's format label is fixed with it: it was computed as -`field.format || 'markdown'`, and `format` is declared on `date` / `datetime` / -`time` / `phone` / `auto_number` and on no rich-content type — so it read -`undefined` for every real field and labelled an `html` field "Format: markdown". -The label is now derived from the field type's display pipeline, so it names the -syntax the value is actually stored in. diff --git a/.changeset/reanchor-adr-0124-d1-citations-5701.md b/.changeset/reanchor-adr-0124-d1-citations-5701.md deleted file mode 100644 index e176b10272..0000000000 --- a/.changeset/reanchor-adr-0124-d1-citations-5701.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Traceability only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The change is comment text at eight live source sites; no -executable line moves, and `git diff -U0` carries zero non-comment added or removed lines. - -`server enforces, client is courtesy` was cited at those eight sites as the framework's -**`ADR-0057 D10`**. That decision reads *"Setup-nav surfacing follows the capability -(ADR-0029 K2); the object stays open"* — nav-entry tiering, not enforcement location. The -rule these sites actually invoke is decided by the framework's **`ADR-0124 D1`**, *"The -server is the enforcement point; client-side gating is a usability courtesy"* (Accepted -2026-08-18). #5699 fixed the repo-ambiguity half of this defect — whose ADR numbering the -citation meant — and deliberately left the anchor half; this is that half. - -The new anchor is derived from an authority, not chosen: the framework's own ADR-0057 -carries a note aimed at precisely this citation — *"If a citation of `ADR-0057 D10` -brought you here looking for that rule, ADR-0124 is where it is decided."* The substantive -claim at every site is unchanged; only the anchor moves. - -The disambiguating parenthetical #5699 shipped — *"framework numbering; this repo's own -ADR-0057 is an unrelated document"* — is retired along with the number it disambiguated. -It warned about a collision specific to `0057`; this repository has no ADR-0124 at all -(its own series stops at `0059`), and ADR-0124 records that a fresh, unambiguous number -was chosen so that citations of it would not need such a warning. The `the framework's …` -possessive stays at every site, so each one still says whose numbering it means. - -Left byte-untouched, deliberately: `packages/data-objectstack/src/appAccessProbe.test.ts` -cites the same decision for *"an app gated by an absent optional service"* — the -capability/service-gating family that decision genuinely does decide, so it is correct as -it stands. `docs/adr/0036-field-conditional-rules.md`, the wording all eight derive from, -carries the same misattribution and moves in its own PR: `docs/adr/**` is a governed -surface that stops at draft for human merge. diff --git a/.changeset/record-activity-open-vocabulary-fallback.md b/.changeset/record-activity-open-vocabulary-fallback.md deleted file mode 100644 index f1e300a948..0000000000 --- a/.changeset/record-activity-open-vocabulary-fallback.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:activity` renders an author-extended `sys_activity.type` instead of dropping it - -**Behaviour change.** `activityRowToFeedItem` used to return `null` for any -`sys_activity.type` outside `ACTIVITY_TYPE_TO_FEED_TYPE`: the row was stored, -queryable and invisible, with only a console warning to say so. It now renders -through a defined fallback presentation (`UNMAPPED_ACTIVITY_FEED_TYPE`, the -generic `system` feed type), still announced once per distinct type. - -This follows the maintainer ruling of 2026-08-24 on objectstack#11507, -direction 4: `sys_activity.type` is **author-extensible**. Every field on -`sys_activity` is `readonly: true` and objectql's `validateRecord` skips -readonly fields on both write branches, and ADR-0052 §5b.2 forwards an author's -`activityMilestones[].type` into the column verbatim — so a value the platform -never declared is legitimate stored data, not a mistake, and dropping it -reproduces the objectui#5840 failure for every author who writes one. - -The types this map deliberately excludes (`commented` / `mentioned` / `login` / -`logout` → `undefined`) are unchanged: those are decisions, not gaps, and they -still return `null` silently. `ACTIVITY_TYPE_TO_FEED_TYPE` itself is unchanged, -so the copy `RecordDetailView` reads is unaffected. - -The pin that goes with it replaces the set-equality check objectui#5840 removed, -and is deliberately two-directional: the map must cover every **built-in** type -(superset — a new built-in turns it red), and an unknown type must reach the -feed through the fallback (never dropped, never crashing). Set equality is not -asserted in either spelling, and the docblock says so, because pinning to the -closed declaration is what made #5840 drop stored rows. diff --git a/.changeset/record-activity-scheduled-event-5840.md b/.changeset/record-activity-scheduled-event-5840.md deleted file mode 100644 index 011e1447b6..0000000000 --- a/.changeset/record-activity-scheduled-event-5840.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/plugin-detail": patch ---- - -`record:activity` no longer discards scheduled activities. `sys_activity` rows with -`type: "scheduled"` now map to the `event` feed kind instead of being dropped before any -filter runs, so a not-yet-held meeting reaches the timeline and `types: ['event']` stops -being a permanently empty tab. Held meetings are unchanged: `completed` still maps to -`task` and still hides unless `showCompleted` is set, which is what keeps an upcoming -meeting visible by default while a finished one is not. - -The unknown-type default is unchanged and deliberately so — a row whose type nothing maps -is still dropped rather than bucketed into `system`, because rendering an unmeasured kind -as something it is not would be new wrong data rather than recovered data. What changes is -that the drop is no longer silent: an unmapped type now logs one `console.warn` naming it, -once per type. Types the map knows and deliberately excludes (`commented`, `mentioned`, -`login`, `logout`) stay silent, since those are decisions rather than gaps. diff --git a/.changeset/record-activity-types-filter-never-widens-5841.md b/.changeset/record-activity-types-filter-never-widens-5841.md deleted file mode 100644 index 94250820b8..0000000000 --- a/.changeset/record-activity-types-filter-never-widens-5841.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@object-ui/plugin-detail": patch ---- - -`record:activity` no longer widens when its `types` filter cannot be honoured. An -unrecognised or empty kind list used to sanitise to `undefined`, which the filter -pipeline reads as "no filter authored" — so three distinct authored intents collapsed -into one rendering, and the widest one: a page that named the wrong kind was served -every activity on the record, with no diagnostic anywhere at runtime. The principle now -held is that a sanitiser may narrow an author's request or refuse it, but must never -silently widen it; widening turns a typo into "show the user everything", which is the -one outcome no author asked for, and it hides behind a plausible result — a populated -timeline reads as working, an empty one gets investigated. - -**Behaviour change.** A page authoring `types: []`, or a `types` list whose every entry -is unrecognised, now renders an EMPTY timeline where it previously rendered every kind. -That is the fix rather than a regression, but a page relying on the old fallback will -visibly change: `types: []` is honoured as "no kinds", and a list that keeps nothing -filters to nothing. A mixed list keeps its recognised entries and drops the rest. Omitting -`types` is unchanged and remains the only way to say "no filter". A `types` that is not a -list at all (`types: 'comment'` — brackets dropped) is likewise refused rather than -ignored, since a filter that cannot be read is not a request to remove the filter. - -Unrecognised entries are now named out loud: one `console.warn` listing them and the -declared feed item types, deduped so one bad kind warns once however many times the feed -re-renders, matching the unknown-activity-type diagnostic already in this file. A -well-formed filter stays silent, `types: []` included — there is nothing to report about -a request that was carried out exactly. diff --git a/.changeset/record-activity-unproduced-feed-kinds-5877.md b/.changeset/record-activity-unproduced-feed-kinds-5877.md deleted file mode 100644 index d99137a020..0000000000 --- a/.changeset/record-activity-unproduced-feed-kinds-5877.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:activity` now says out loud when a `types` entry names a feed kind nothing produces. - -`FeedItemType` publishes thirteen kinds and ObjectUI produces five of them -(`comment`, `field_change`, `task`, `event`, `system`). Authoring -`types: ['approval']` parsed, typechecked, built, and rendered a permanently -empty timeline with no diagnostic anywhere — a declared surface enforced by -nothing, which reads to an author, or to an AI writing the metadata, as a -working feature that simply has no data yet. The entry is still honoured exactly -as authored and nothing that renders changes; what changes is that the emptiness -is now diagnosable, on its own deduped channel beside the unrecognised-entry and -unrecognised-`filterMode` warnings already in this block. - -The message keeps two populations apart, because reporting a decision as a -defect is how a warning channel gets trained out of an author's attention: -`record_create` / `record_delete` / `sharing` are named as **deliberately not -adopted** — those rows map to `field_change` on purpose, since the record page -and this block must agree what a `created` row is before either can move to the -richer kind — while the remaining kinds are reported as having no producer, and -the message stops there rather than calling that a gap. Whether somebody ruled -against a kind and left no note is not recorded anywhere the renderer can read, -and the wording says so instead of guessing. - -The producer census the diagnostic reads is derived from the producers -themselves — the `sys_activity` map's range plus its unmapped fallback — so -giving a kind a producer retires its warning in the same edit. The one producer -that cannot be derived (`comment`, built from `sys_comment` rows by the console's -record page, a package this one cannot import) is declared, and a new test -re-runs the census over the whole repository so the declaration cannot go stale -silently. The warning also names what it cannot see: a host that supplies the -feed itself can produce kinds no census taken here bounds. diff --git a/.changeset/record-alert-cta-label-i18n-4998.md b/.changeset/record-alert-cta-label-i18n-4998.md deleted file mode 100644 index c42fa2e98f..0000000000 --- a/.changeset/record-alert-cta-label-i18n-4998.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is -widened to `string | I18nLabel` in both copies (`properties.*` and the flat -compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. - -The renderer already resolves `action.label` through the same inline-locale-map -`pickLocalized` call as `title` / `body` (`const ctaLabel = -pickLocalized(props.action?.label, language)`), so a bare `string` declaration -was narrower than the renderer's own runtime behavior — the same -declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for -`title` / `body` one level up in the same interface (objectui#4998). - -Type-only: the block's published authoring surface still declares `action` as -a bare `object` with the member shape in prose only -(`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — -that half stays parked on the `ComponentInput` member-shape question (PR -#3795) and is out of scope here. diff --git a/.changeset/record-alert-loading-frame-diagnostic-5776.md b/.changeset/record-alert-loading-frame-diagnostic-5776.md deleted file mode 100644 index e6b72b9772..0000000000 --- a/.changeset/record-alert-loading-frame-diagnostic-5776.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:alert`'s `visible` predicate no longer logs a spurious `record is not -defined` evaluation-failure warning during the record-detail loading frame. - -The predicate is evaluated on every render (Rules of Hooks), including the -frame before `useRecordContext().data` has loaded. In that frame `record` is -unset, `usePredicateRecordContext` binds an empty context bag by design (no -`record` key at all), and a bare/`${…}` predicate referencing `record.*` -faulted with `record is not defined` — logged via `console.warn` on every -page load, even the correct, working ones, because the SAME predicate -resolved fine one frame later once `record` populated. The banner's own -visibility was never wrong (it already hides unconditionally while unloaded); -only the diagnostic was permanently misleading on the happy path. - -The predicate is now skipped while `record` hasn't loaded — the same -condition the banner's own "hide while unloaded" early return already used — -since its verdict in that frame was never consulted anyway. A predicate that -is genuinely broken (bad field, bad syntax) still faults, and still logs, on -every frame once `record` is populated (objectui#5776). diff --git a/.changeset/record-alert-row-binding-4807.md b/.changeset/record-alert-row-binding-4807.md deleted file mode 100644 index ecca389daf..0000000000 --- a/.changeset/record-alert-row-binding-4807.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`record:alert` binds the row through `usePredicateRecordContext`, so an -author-declared `properties.visible` is actually consulted. - -`renderers/record-alert.tsx` was the last predicate face in the repo still -handing `useCondition` a root-only `{ record }` bag. Every other row-scoped -predicate — the four generic action renderers (objectui#4075) and app-shell's -`DeclaredActionsBar` (objectui#4077) — binds the row through the shared -`usePredicateRecordContext(record)` helper, which resolves the three spellings -objectui#5330 ruled on: canonical `record.status`, the deprecated row-action -shorthand `status`, and deprecated legacy `data.status`. - -Under the root-only bag only the canonical spelling worked, and the two others -failed in **opposite** directions — both of them silently, because this call -site is fail-soft: - -- **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator - threw. The legacy `${…}` path answers a throw with its own source text, a - non-empty and therefore truthy string, so the verdict was **SHOWN on every - row**. A banner the author had gated was permanently on screen. -- **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's - ambient predicate scope (`providers/ExpressionProvider.tsx`) carries - `data: {}`, so the predicate read that object instead of the row, compared - `undefined`, and the verdict was a constant false — **never shown**. - -**Behaviour change, stated plainly:** a shipped `record:alert` whose `visible` -was written in either deprecated spelling was inert and is now live. A banner -that was permanently visible may begin to hide, and one that never appeared may -begin to show — that is the point of the fix, but it is a verdict change rather -than a no-op. Canonical `record.*` predicates are unaffected in verdict: they -resolved before and resolve now, pinned on both polarities. An in-tree census -found no `record:alert` `visible` predicate outside this package's own tests. - -A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`, -with its own deliberate bindings (`data` is the data-source adapter there, not -the row). This change does not touch it; the two still compose as AND. - -The renderer's header comment described the shared-scope behaviour it did not -have. It now describes what the file does, including the fail-soft policy and -the two-gate composition. diff --git a/.changeset/record-detail-param-dialog-title-5610.md b/.changeset/record-detail-param-dialog-title-5610.md deleted file mode 100644 index f8c16e205f..0000000000 --- a/.changeset/record-detail-param-dialog-title-5610.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`RecordDetailView`'s param-collection dialog now titles itself from `action.label` -alone — the unreachable `|| action?.title` fallback beside it is removed -(objectui#5610). - -This was the second copy of the limb objectui#4282 removed from -`useConsoleActionRuntime`. `RecordDetailView` builds its own action runtime rather -than routing through that hook, so the two near-identical `paramCollectionHandler`s -have drifted as a pair and the first fix could not reach this one. - -`title` is declared on no action surface in the ecosystem: it is absent from -`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from -`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` -inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` -`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, -`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand -side of that `||` could not be reached by authored metadata: a fallback that cannot -fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. -Nothing a user hits changes; the line now reads exactly one key, matching the -`description` line directly below it. - -`RecordDetailView.paramDialogTitle.test.tsx` pins the reader so the alias cannot be -reinstated silently: an action carrying `title` and no `label` must open an untitled -dialog rather than a dialog named by a key no producer sets. A pin per reader is the -only shape that covers both handlers, since the hook's own pin cannot see this site. diff --git a/.changeset/record-handoff-card.md b/.changeset/record-handoff-card.md deleted file mode 100644 index ca1c638ff0..0000000000 --- a/.changeset/record-handoff-card.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -「打开这条记录 →」卡片——ask 的记录交接终于有了客户端的另一半 - -服务端半边(cloud#1659 的 `open_record`)先落了地,实测发现它是**半活的**:agent 发出 -`status:'record_handoff'`、回答说「点击上方链接打开」,而上方根本没有链接——控制台 -有 `build_handoff` 的探测器,这个状态一处都不认识,信号被原样丢弃。 - -按五步补齐:`detectRecordHandoff`(含持久化 `{type:'text',value}` 包裹形状——replay -信封那课的规矩)→ live 映射提升 → 水合提升 → 卡片渲染 → 宿主回调。 - -两个设计点: - -- **app 段点击时现场解析**。记录路由要 `/apps/:app/:object/record/:id`,交接载荷只有 - 对象和记录 id;宿主回调用一次同源元数据读取 `_packageId` 再导航,不给 agent 增加 - 它未必知道的参数。 -- **刻意不做「被取代」置灰**。builder 卡的旧 prompt 会过时,旧的记录链接不会——记录 - 不因为有新交接而失效。 - -真机闭环验证:问「把《沉默的大多数》标记成已读」→ 卡片渲染 -(`沉默的大多数 — 把阅读状态改为已读`)→ 点击 → 落在 -`/apps/app.hdke/hdke_book/record/<id>` 详情页,「编辑」在手边。 - -缺任一 id 的交接在探测器就被丢弃,与服务端的拒绝对称——指向空处的卡片比散文更糟。 diff --git a/.changeset/record-picker-empty-text-i18n-5590.md b/.changeset/record-picker-empty-text-i18n-5590.md deleted file mode 100644 index 8c53ac4a90..0000000000 --- a/.changeset/record-picker-empty-text-i18n-5590.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `emptyText` now resolves the inline per-locale map its -contract has admitted since rc.6, and its published declaration says so -(objectui#5590). - -`@objectstack/spec` widened this key to the `I18nLabel` union -(`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA -still carries it — measured, not assumed: -`ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })` -succeeds. The renderer honoured only the string arm, handing the map straight to a -text node. React refuses a plain object in a child position rather than stringifying -it, so an author writing the map form the contract accepts did not get a mis-rendered -empty state — the whole picker subtree threw -`Objects are not valid as a React child (found: object with keys {en, zh-CN})`. - -The read site now resolves through `pickLocalized`, the objectui-side helper the -sibling text-node sites already read through (`element:text.content`, -`element:button.label`, `page:card.title`), which spells a miss as `''` rather than -the spec resolver's `undefined`. The default is applied before resolution, so -`emptyText` absent still means "No records" and an authored empty string still -renders empty. - -The `ComponentMeta` entry, which held a single `'string'` arm precisely because the -renderer dropped the other one, now declares `['string', 'object']`. That narrowing -was correct for exactly as long as it was true: with the map arm reaching the screen -resolved, withholding it would be the false declaration in the other direction — the -manifest gate reporting `type-mismatch` on a legal write the same input's own -`description` teaches the author to make. The `apps/console` specimen that pinned the -narrow arm named this release condition in its own words ("keeps its single `'string'` -arm until the render site catches up") and is flipped here, keeping its controls. - -Three comments in the renderer deferred this gap to objectui#4163, which closed as -completed on 2026-08-15 while the gap was still open; the file now carries no -reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very -key as its worked example of an arm deliberately withheld, and is corrected in the -same change so the example stays true. diff --git a/.changeset/record-picker-label-association-5771.md b/.changeset/record-picker-label-association-5771.md deleted file mode 100644 index df693b93c5..0000000000 --- a/.changeset/record-picker-label-association-5771.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `label` is now programmatically associated with its -`SelectTrigger` combobox — fixing a case where the caption the block's own -registration prose advertised (`'Caption rendered above the picker, in a -<label> element'`) named nothing at all (objectui#5771). - -Pre-fix, the render body was `{label && <label className="…">{label}</label>}` -against a `SelectTrigger` with no `id`: neither end of the association carried -any wiring. That is a step worse than the sibling gap objectui#5735 closed on -`element:text_input` — there the `label` half was already correctly wired and -only `description` was adrift; here the label element had no `htmlFor` and the -trigger had no `id`, so the picker had no accessible name unless a -`placeholder` happened to render text into the trigger's content. - -The fix follows the shape objectui#3341 already ruled on for the same defect -class (`InlineCreateRelated`'s `<label>`/`<Input>` pair) and objectui#5735 -just landed on the complement block: `htmlFor`/`id` against the -`SelectTrigger` — a `button` with `role="combobox"`, and therefore labelable, -so no `aria-labelledby` is needed (Radix sets none on the trigger) — wired -**when `schema.id` exists**, matching `element:text_input`'s precedent rather -than an always-on `useId()` fallback. Only the author can supply `schema.id`, -so a picker authored without one keeps its pre-fix, unassociated caption text -exactly as before; nothing about this change mints an id the author did not -provide. The `<label>` element itself is also swapped for the shared `Label` -(`@radix-ui/react-label`) primitive `element:text_input` already uses for the -same wiring, rather than a bare `<label>` with a `htmlFor` bolted on. - -The registration prose for `label` is corrected to describe what the code now -does (tied to the control by `htmlFor` when the node carries an `id`) instead -of a caption association the renderer never performed. diff --git a/.changeset/record-picker-label-placeholder-i18n-5637.md b/.changeset/record-picker-label-placeholder-i18n-5637.md deleted file mode 100644 index 58a25eaf35..0000000000 --- a/.changeset/record-picker-label-placeholder-i18n-5637.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `label` and `placeholder` now resolve the inline -per-locale map their contract admits, and their published declarations say so -(objectui#5637). - -These are the two remaining keys of the trio objectui#5590 fixed one of. All -three members of `ElementRecordPickerPropsSchema` are the `I18nLabel` union -(`string | Record< string, string >`) — measured on the installed -`@objectstack/spec` 17.1.0 pin, where each resolves to -`optional -> union -> string | record` and -`safeParse({ object: 'account', <key>: { en, 'zh-CN' } })` succeeds. The -renderer honoured only the string arm on both, and the two keys failed in two -different ways: - -- `placeholder` was read raw and handed to `SelectValue`. React refuses a plain - object in that position rather than stringifying it, so the whole picker - subtree threw - `Objects are not valid as a React child (found: object with keys {en, zh-CN})` - — the same harm objectui#5590 removed from `emptyText`. -- `label` went through the file's local `toText`, whose object branch ends - `String(o.label ?? o.name ?? o.title ?? o.en ?? '')`. Reaching `o.en` - unconditionally is an English pick wearing locale resolution's clothes, so a - `zh-CN` viewer was shown the English entry — and a map that simply omits `en` - resolved to `''`, which the `{label && …}` render site drops, making the - picker's label element DISAPPEAR with nothing thrown and nothing logged. - -Both keys now resolve at their own read site through `pickLocalized`, the same -helper the settled `emptyText` shape uses. `toText` is deliberately unchanged: -it is shared with the row values (`toText(row?.[labelField])`), which are record -field values rather than `I18nLabel`, so teaching it locale resolution would -have changed a second, unrelated call site. The `placeholder` default is applied -before resolution, so an absent key still means "Select a record…" and an -authored empty string still renders empty. - -The two `ComponentMeta` entries, which held a single `'string'` arm precisely -because the renderer dropped the other one, now declare `['string', 'object']` -— declared in the change that makes the arm render, never before, which is the -order `ComponentInput.type` prescribes and the order `emptyText` set. - -KNOWN GAP, unchanged by this release: the sibling `label` read sites in -`renderers/layout/containers.tsx` compose -`translateLabel(pickLocalized(…), language)`, and that second helper is not -applied here — `translateLabel` and its `KNOWN_LABEL_DICT` are module-private to -that file. Only the locale-map resolution lands in this change; a plain-English -string `label` is still rendered verbatim in every language, exactly as before. diff --git a/.changeset/record-quick-actions-resultdialog-pin-5711.md b/.changeset/record-quick-actions-resultdialog-pin-5711.md deleted file mode 100644 index 351cbd3b8a..0000000000 --- a/.changeset/record-quick-actions-resultdialog-pin-5711.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-only change — no published behaviour changes. - -Adds a component-level test pinning that a `record:quick_actions` action's -declared `resultDialog` reaches the ambient `<ActionProvider>`'s shared -`ActionRunner` and opens the reveal dialog with the response value (success -toast suppressed), plus the negative leg — with no provider, the response is -silently discarded and the documented `console.warn` fires. Closes the gap -found while implementing objectstack#10681 (objectui#5711). diff --git a/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md b/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md deleted file mode 100644 index c9b1b8e39c..0000000000 --- a/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`getRecordDisplayName`: stop consulting the undeclared object-level -`titleField`, restoring `nameField` as the top of the object ladder - -Step 0 of the unified record-title resolver read -`options?.titleField ?? objectDef?.titleField`. The second leg ranked an -object-level `titleField` above `nameField` — the pointer ADR-0079 Phase 2 made -canonical — and above the deprecated `displayNameField` alias and the legacy -`titleFormat` template. - -`@objectstack/spec`'s object schema does not declare that key, and it is not -merely undeclared: the schema is a `strictObject`, so -`ObjectSchema.safeParse({ …, titleField: 'x' })` fails with `unrecognized_keys` -— the same code a nonsense key gets — while `nameField`, `displayNameField` and -`titleFormat` all parse and survive. A producer census across both repos found -nothing that puts the key on an object-shaped payload: not the metadata, not any -`getObjectSchema` implementation (the ObjectStack adapter stamps only reference -keys and field-widget hints), not the lookup-chip path, not the -search-candidate path, and not the platform's own server-side resolver -(`@objectstack/objectql#titleFieldOf` reads `nameField` → `displayNameField`). -Reading a key no producer can ship is a consumer-side alias — the shape -Commandment #0.1 bans — and it inverted the governed-authority default on top of -that. - -No authoring surface changes and no view loses its author-chosen title field: -`titleField` remains a real, declared VIEW key (`ui/CalendarConfig`, -`ui/GalleryConfig`, `ui/GanttConfig`, `ui/ListMapConfig`, -`ui/ObjectKanbanProps`, `ui/TimelineConfig`), views hand it in as -`options.titleField`, and that half of step 0 still wins over everything. -The behaviour change is confined to an object payload that carried a key the -contract rejects: it now resolves through the declared ladder instead. diff --git a/.changeset/reference-rail-entries-stale-prose.md b/.changeset/reference-rail-entries-stale-prose.md deleted file mode 100644 index 560c79cef5..0000000000 --- a/.changeset/reference-rail-entries-stale-prose.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-only comment refresh in `registry-inputs-spec-parity.test.ts`: the -`record:reference_rail.entries` exemption note no longer narrates the -`ReferenceRailEntry` `icon` divergence as an open contract question (ruled -Option B 2026-08-22, objectui#5494 landed the derivation from -`@objectstack/spec/ui`), and drops the "the renderer reads it" premise that was -false when written. The exemption itself is unchanged — `inputs` is a flat -scalar carrier and still cannot express an array of objects. No published -behaviour changes. diff --git a/.changeset/reference-rail-entry-spec-derived.md b/.changeset/reference-rail-entry-spec-derived.md deleted file mode 100644 index fd9b93bab6..0000000000 --- a/.changeset/reference-rail-entry-spec-derived.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/plugin-detail": minor ---- - -**`ReferenceRailEntry` is now the spec's type, and the reference-rail `icon` key retires** (objectui#5494, maintainer ruling 2026-08-22). - -`ReferenceRailEntry` is owned by `@objectstack/spec` as of 17.1.0. The hand-written interface in `record-reference-rail.tsx` is replaced by a re-export of the spec's `ReferenceRailEntry` (derived from `ReferenceRailEntrySchema`, `$strict` over `{ objectName, relationshipField, title?, limit?, displayField? }`), and `buildDefaultPageSchema` no longer emits `icon` on the reference-rail entries it synthesizes. - -**Migration — if you write `icon` on `record:reference_rail` entries, remove it.** Be aware of what this does and does not change: - -- **Runtime validation does not move.** The spec's strict schema already refused `icon` at save — that refusal is unchanged. What was broken was objectui's declaration: the TS type advertised a key that could never be saved. -- **Nothing ever rendered `icon`.** No render path in `RecordReferenceRailRenderer` has ever read the key (measured back to the file's first commit, and independently by the spec's `ui-reference-rail-unknown-keys-refused` migration entry). An authored `icon` that survived in unsaved/preview metadata was already a silent no-op — after this change the TS type says so instead of suggesting otherwise. -- **The published TS surface narrows.** Code that imported `ReferenceRailEntry` from `@object-ui/plugin-detail` internals and set `icon` will now get a type error. That error is the contract speaking: remove the key. - -The reference-rail icon affordance is retired rather than proposed upstream: a stock scan of this repo, its examples and schema-catalog corpus, and the objectstack tree (examples, templates, docs) found no reachable authored usage of `icon` on reference-rail entries. Stored customer metadata is not reachable from this seat and was not scanned. diff --git a/.changeset/registerfunction-casefold-doc-5363.md b/.changeset/registerfunction-casefold-doc-5363.md deleted file mode 100644 index def190682d..0000000000 --- a/.changeset/registerfunction-casefold-doc-5363.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`ExpressionEvaluator.registerFunction` now documents the case-fold it has always -performed: the name is stored — and must be called — in UPPER CASE -(objectui#5363). - -`registerFunction('formatCurrency', fn)` registers `FORMATCURRENCY`, because the -method delegates to `FormulaFunctions.register`, which stores under -`name.toUpperCase()`. That fold is correct for the spreadsheet-style built-in -vocabulary (`SUM`, `IF`, `UPPER`) and is unchanged here — but nothing declared -it on the public method, and two things keep it from being self-evident at the -call site. The registry API stays case-insensitive, so `getFormulas().has()` and -`.get()` both answer to the original spelling and never reveal the fold; only -expressions see the stored key, because the evaluation scope is built from -`FormulaFunctions.toObject()`, a plain object whose identifiers are matched -case-sensitively. And a wrong-case call site does not raise: `evaluate()` -catches, warns, and returns `defaultValue ?? expression`, so the template -renders its own `${...}` source as literal text on screen rather than erroring. - -Behavior is untouched — this is the declaration catching up with what the code -enforces. It ships as a patch rather than as an empty changeset because the -JSDoc is emitted into the published `dist/evaluator/ExpressionEvaluator.d.ts`, -so it is what consumers see on hover. - -`ExpressionEvaluator.test.ts` gains three cases pinning the half that was -uncovered — that the given spelling does *not* resolve in an expression, that -the failure renders the raw template source instead of throwing, and that the -registry API stays case-insensitive underneath — so making registration -case-preserving fails a test instead of silently invalidating the new JSDoc. diff --git a/.changeset/release-lane-tests-and-concurrency-5404.md b/.changeset/release-lane-tests-and-concurrency-5404.md deleted file mode 100644 index 31f50fe34e..0000000000 --- a/.changeset/release-lane-tests-and-concurrency-5404.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -Two changes to `.github/workflows/changeset-release.yml`, the workflow whose last step -publishes to npm. - -**The duplicate `pnpm test` is gone.** It re-tested a commit already on `main`, so it -could never keep anything out — it could only stop the release afterwards, and it did: -runs #3606 and #3901, 2 of the last 27 release-PR merges, failed there and skipped the -changesets step entirely. It cost 31m19s of a 33m22s job (medians over the 106 runs of -#3712-#3911 that executed; install 6s, build 35s, the changesets action 24s). The -backstop for `main` is `ci.yml`'s push lane, which runs the whole suite under coverage -across four shards and enforces the thresholds on the merged report. - -**The concurrency group is now keyed by commit rather than by branch.** A group shared by -every push to `main` does not queue: GitHub holds one pending run per group and cancels -the rest, so 93 of those same 200 runs were `cancelled` with an empty jobs array — not -one step ever ran. On a workflow that publishes, a discarded run is a discarded publish. -Ordering moves into a fail-open wait step that holds a run until every older release run -has finished, so runs are still serialised, but a contended lane now delays a release -instead of dropping it. diff --git a/.changeset/remove-inert-showrefresh-5567.md b/.changeset/remove-inert-showrefresh-5567.md deleted file mode 100644 index 017478cc2f..0000000000 --- a/.changeset/remove-inert-showrefresh-5567.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/plugin-view': minor -'@object-ui/app-shell': patch ---- - -Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). - -The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. - -Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. - -`@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. diff --git a/.changeset/rename-command-palette-shortcuts-demo.md b/.changeset/rename-command-palette-shortcuts-demo.md deleted file mode 100644 index 4effb7cfe8..0000000000 --- a/.changeset/rename-command-palette-shortcuts-demo.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Rename the `components-form-command/command-palette-with-shortcuts` catalog demo to -`components-form-command/file-command-palette`, so its id, title and docs heading name what -the fixture actually renders after objectui#6157 removed the invented `shortcut` key. The -docs heading, the generated catalog index and the fixture test follow the rename. - -No published behaviour changes: the two `@object-ui/components` files touched are a code -comment and a test name that spelled the old filename. diff --git a/.changeset/render-publish-advisory-findings-5026.md b/.changeset/render-publish-advisory-findings-5026.md deleted file mode 100644 index 659dd337e1..0000000000 --- a/.changeset/render-publish-advisory-findings-5026.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save - -objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. - -The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door. - -`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones. - -One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb. - -**BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group. - -Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) still discards per-draft advisories server-side — objectstack#9343, open and unruled — and nothing here compensates for that from the client side. A test pins the absence, so a later traversal of a batch-shaped `published[]` cannot be added without turning it red. diff --git a/.changeset/replay-dispatch-error-5695.md b/.changeset/replay-dispatch-error-5695.md deleted file mode 100644 index 8c014116b1..0000000000 --- a/.changeset/replay-dispatch-error-5695.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. diff --git a/.changeset/report-view-datasource-object-key-5116.md b/.changeset/report-view-datasource-object-key-5116.md deleted file mode 100644 index 2d3435113c..0000000000 --- a/.changeset/report-view-datasource-object-key-5116.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`ReportView` reads a report's data binding through the one key the contract declares — `dataSource.object`. - -The view accepted `resource` as a second spelling of `object`, in two places, and -named that spelling in a warning the user could read: - -``` -:171 liveReport?.objectName || liveReport?.dataSource?.object - || liveReport?.dataSource?.resource -:273 dataFetchSource.dataSource.object || dataFetchSource.dataSource.resource -:275 console.warn('ReportView: dataSource missing object/resource property') -``` - -`resource` is not on this binding. `ElementDataSourceConfig` declares `object`, -`view?`, `filter?`, `sort?` and `limit?`; its `@objectstack/spec` twin -`ElementDataSourceSchema` is a strict object, so an extra `resource` key is -*rejected* there rather than ignored; and the binding's own predicate -`isElementDataSourceConfig` decides on `object`. A `resource`-only binding -therefore was never a binding on any other renderer in the system — it rendered -here and silently produced nothing anywhere else, with neither end reporting a -problem. That divergence is what a consumer-side alias buys: one renderer -answering a question the contract says has no answer. - -`resource` is a real key on other surfaces — `CRUDSchema.resource`, the -`DataSource` adapter's first parameter, `LiveExportOptions.resource` — and all -three are untouched. None of them is this one. - -Behaviour, measured by rendering each input shape before and after. Only the -`resource`-only shape moves: - -| binding | before | after | -| --- | --- | --- | -| `object` only | queries that object | unchanged | -| `resource` only | queries it as if declared | not queried; named warning, no rows, fallback field list | -| both | queries `object` | unchanged | -| neither | not queried; warning | unchanged | - -So off-spec report metadata that used to render now fails loudly instead of -appearing to work. A producer census found nothing that would notice: no site in -this repository, and none in the `objectstack` framework repository, writes -`resource` onto a report `dataSource`. The limb was speculative in the commit -that introduced it, and per AGENTS.md #0.1 an off-spec spelling is corrected at -the producer, never taught a second dialect by the renderer. - -The `:275` wording now names only `object`. A diagnostic that lists a key the -contract does not declare is not a small thing: it is the system telling an -author — increasingly, an author's code generator — that the wrong spelling is -supported. diff --git a/.changeset/retire-codecov-upload-5436.md b/.changeset/retire-codecov-upload-5436.md deleted file mode 100644 index 66b7492a39..0000000000 --- a/.changeset/retire-codecov-upload-5436.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -`ci.yml`'s `Test (coverage)` job no longer uploads to Codecov. `CODECOV_TOKEN` was never -set on this repository and Codecov no longer accepts tokenless uploads, so the step failed -server-side (`Token length: 0`) on every push after objectui#5403 landed — a permanently -red lane that trains everyone to ignore red. Per the maintainer ruling of 2026-08-22 -(Option B), the upload is retired rather than the secret set: the merged report is now -published as the `coverage-report` build artifact (7 days, matching the shard blobs it is -derived from) alongside the existing step summary. - -The coverage GATE is untouched and was never Codecov's: the configured -`coverage.thresholds` are enforced by the merge step over the whole merged map, and the -shard legs still override them to zero so that merge step remains the only place the gate -runs. objectui#5403's loud verdict step is carried forward rather than deleted — it now -reports `shards=… blobs=… merge=… report=…` and is green only when all four shards passed, -all four blobs arrived, they merged, the thresholds were enforced over the result, and the -report was published. A threshold breach is annotated separately from a lane that never -delivered, because the two call for opposite actions. - -What is given up is the Codecov trend dashboard and its PR coverage comments. diff --git a/.changeset/retire-components-action-param-dialog-5685.md b/.changeset/retire-components-action-param-dialog-5685.md deleted file mode 100644 index 803fed5351..0000000000 --- a/.changeset/retire-components-action-param-dialog-5685.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/components': minor ---- - -Retire `ActionParamDialog`: the `custom` barrel's second action-param dialog is -removed, and the app-shell dialog is recorded as the surviving implementation -(objectui#5685, maintainer ruling of 2026-08-22). - -**Breaking for any out-of-repo host that imported it** (declared `minor` per the -repo's version-alignment rule — the major tracks `@objectstack`, never an -API-break count): `@object-ui/components` no longer exports `ActionParamDialog` -or `ActionParamDialogProps`. Measured at the branch point, the export had zero -production consumers — its only in-repo importers were its own five test files, -which retire with it, and no other repository in the organization imports the -symbol from this package. - -This file was the repo's SECOND implementation of the action-param surface, and -its audit trail is the reason it retires instead of being maintained: the last -close look (objectui#4758) found per-option `visibleWhen` not evaluated at all, -and the five hardcoded English strings this card originally recorded were the -next drift installment. A dormant second dialect of a governed surface is one -production import away from being live; removing it removes the whole drift -class. - -FROM → TO for an out-of-repo host: - -- `import { ActionParamDialog } from '@object-ui/components'` — no drop-in - replacement is published. The surviving implementation is - `@object-ui/app-shell`'s `ActionParamDialog` (`src/views/ActionParamDialog.tsx`), - rendered by app-shell's action runtime (`useConsoleActionRuntime`, - `RecordDetailView`) rather than exported standalone. A host that needs its own - param form builds on `@object-ui/fields`' shared field widgets - (`resolveFormWidgetType` / `getLazyFieldWidget`, ADR-0059) — the same seam the - surviving dialog renders through. - -The unreleased objectui#4758 changeset for this component (`select` options -through the shared option evaluator) is withdrawn in the same change: the -component retires before that fix ever ships, so the release notes carry the -removal rather than new behaviour of a surface this release does not contain. diff --git a/.changeset/retire-spec-bridge-6366.md b/.changeset/retire-spec-bridge-6366.md deleted file mode 100644 index 86bdf64d64..0000000000 --- a/.changeset/retire-spec-bridge-6366.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/react': minor ---- - -refactor(spec-bridge): retire the spec-bridge — `SpecBridge`, `bridgeListView`, `bridgeFormView` (#6366) - -Removed under the 2026-08-27 maintainer ruling on #6366 (Option A — remove, and -for `SpecBridge` as a WHOLE, not just the form half), on the same criteria the -17.0.0 entry used to remove `bridgePage` / `bridgeDashboard`: no runtime -consumer, and a route that could not deliver a working result even in -principle. The stage-1 measurement (2026-08-26, PM-verified) the ruling rests -on: - -- **Zero consumers at every reachable endpoint** — no non-test caller in this - repo, none in the sibling `objectstack` repo (which does not depend on - `@object-ui/react` at all), and a GitHub-wide public code search returning - hits only inside this repository. External npm-private hosts are - unmeasurable from here; the ruling records fallback C (keep + document the - boundary) should evidence of one ever surface. -- **The bridged form route was structurally unable to work**: a bridged node - carries neither `objectName` nor `customFields`, so `ObjectForm` takes the - branch its own code labels "cannot proceed" and renders, in the registry - wrapper's words, "a field-less card in silence". The only working host - recipe (`{...node, objectName: '...'} as any`) existed solely inside the - list bridge's integration tests and was documented nowhere. - -This knowingly reverses the 17.0.0 changelog line "The `list` and `form` -bridges are unaffected and remain the live authoring path" — by maintainer -ruling, on the same measurement criteria that entry itself applied. - -#5898's restored-key work on the form-view bridge (the -`FormViewSpecConformance` suite and the spec keys it carried onto the bridged -node) is **superseded by this removal, not fixed** — the route it repaired is -gone. #6366's measured type-vocabulary asymmetry (a bridged field carrying -`text` where the normalizer produces `field:text`) is likewise mooted rather -than repaired. - -The suites pinning the removed route go with it — retirement of a route -nothing travels, not quarantine: the seven suites under -`react/src/spec-bridge/__tests__/`, and plugin-grid's -`specBridgeColumnSpelling` / `specBridgeExportFormats` render-integration -suites. `ObjectGrid`'s own column-spelling, export-gate and density behavior -keep their non-bridge pins (`columnDeclaredSpellingOnly`, `exportGate`, -`exportServer`, `rowHeightOffSpecBoundary`, and core's -`normalize-list-view` suite). - -BREAKING CHANGE: the public exports `SpecBridge`, `bridgeListView`, -`bridgeFormView` and the types `BridgeContext` / `BridgeFn` / `ObjectDefLite` -are removed from `@object-ui/react`. There is no replacement translation -layer — author `object-grid` / `object-form` nodes directly (the live path is -`app-shell`'s `ObjectView`, which builds them from the object's own metadata), -exactly as the 17.0.0 entry already directed for pages and dashboards. diff --git a/.changeset/retire-theme-component-schema-5489.md b/.changeset/retire-theme-component-schema-5489.md deleted file mode 100644 index 121ce3a106..0000000000 --- a/.changeset/retire-theme-component-schema-5489.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer -implemented (objectui#5489). - -`packages/types/src/theme.ts` declared a theme-manager **component** carrying -`themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and -`storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching -Zod object as a member of `ThemeUnionSchema` and therefore of -`AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no -`ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`, -and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS` -(`packages/components/src/renderers/placeholders.tsx`), so it did not even -resolve to a placeholder — a page declaring one got the registry's "Unknown -component type" panel (OBJUI-001) instead of a theme manager. Declared-but- -unenforced, removed under the maintainer ruling of 2026-08-21 on -objectstack#10485 (option B). - -Removed from the published surface: the `ThemeComponentSchema` type -(`@object-ui/types`), the `ThemeComponentSchema` Zod object -(`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the -`'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling -`type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than -accepted and then rendered as an error panel, which is pinned by a test. - -**The theme system is unchanged.** `Theme` (the spec's authoring theme -document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine` -(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and -untouched — the same ruling retains them explicitly. Author a theme as a -document handed to `ThemeProvider`; that path never went through the removed -component kind. diff --git a/.changeset/retire-theme-switcher-preview-5647.md b/.changeset/retire-theme-switcher-preview-5647.md deleted file mode 100644 index 29633b09ba..0000000000 --- a/.changeset/retire-theme-switcher-preview-5647.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `ThemeSwitcherSchema` (`type: 'theme-switcher'`) and -`ThemePreviewSchema` (`type: 'theme-preview'`) — the two remaining theme -component kinds, which no renderer implemented — together with -`ThemeUnionSchema`, the union that after objectui#5489 held only these two -members (objectui#5647). - -`packages/types/src/theme.ts` declared a theme-switcher control (`variant`, -`showMode`, `showThemes`, `lightIcon`, `darkIcon`) and a theme-preview panel -(`showColors`, `showTypography`, `showComponents`), and -`packages/types/src/zod/theme.zod.ts` published the matching Zod objects as -the two members of `ThemeUnionSchema` and therefore of `AnyComponentSchema`. -Nothing rendered either: neither literal appears at any -`ComponentRegistry.register(...)` / `registerLazy(...)` site in -`packages/*/src` (202 registered keys enumerated; positive control on the same -pipeline: `tooltip` → 1), nor in `PROTOCOL_COMPONENTS` / -`PALETTE_PLACEHOLDER_BLOCKS` -(`packages/components/src/renderers/placeholders.tsx`), and no fixture -declares either kind (control: `"type": "form"` → 81) — so a page declaring -one got the registry's "Unknown component type" panel (OBJUI-001), never a -switcher or a preview. Declared-but-unenforced, removed under the 2026-08-21 -maintainer ruling (option B) on objectstack#10485, extended to these siblings -by inheritance on identical evidence (objectui#5647). - -Removed from the published surface: the `ThemeSwitcherSchema` / -`ThemePreviewSchema` types (`@object-ui/types`), the matching Zod objects and -`ThemeUnionSchema` (`@object-ui/types/zod`), and the `ThemeSwitcherSchemaType` -/ `ThemePreviewSchemaType` inference aliases. `zod/theme.zod.ts` now exports -nothing and stands as the tombstone module. A schema spelling -`type: 'theme-switcher'` or `type: 'theme-preview'` is now REFUSED by -`AnyComponentSchema.safeParse` rather than accepted and then rendered as an -error panel, which is pinned by a test. - -**The theme system is unchanged.** `Theme` (the theme document vocabulary, -owned by `@object-ui/types` since objectui#5716), `ThemeEngine` -(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are retained and -untouched — the rulings retain them explicitly. Author a theme as a document -handed to `ThemeProvider`; that path never went through the removed component -kinds. diff --git a/.changeset/retire-usebranding-hook-5368.md b/.changeset/retire-usebranding-hook-5368.md deleted file mode 100644 index bf5f6e24d2..0000000000 --- a/.changeset/retire-usebranding-hook-5368.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Delete `apps/console/src/hooks/useBranding.ts`, a `@deprecated` wrapper with zero callers. - -Nothing published changes. The console's npm tarball ships only `dist`, `plugin.*` and -`README.md` (`files` in `apps/console/package.json`) — `src` is never in it — and the -package's single `exports` entry resolves to `plugin.js`, which is compiled from -`plugin.ts` alone and imports nothing from `src`. With no importer anywhere in the repo, -the hook was also absent from the built SPA bundle. Empty frontmatter is therefore the -accurate declaration: a source deletion under a released package that releases nothing. diff --git a/.changeset/retired-field-type-gate-4914.md b/.changeset/retired-field-type-gate-4914.md deleted file mode 100644 index ca91ef84fd..0000000000 --- a/.changeset/retired-field-type-gate-4914.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch -'@object-ui/components': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-view': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-list': patch ---- - -A RETIRED field-type spelling is now refused — out loud, once — by every -field-type predicate in the renderer, not just by the widget road -(objectui#4914, maintainer ruling B of 2026-08-18). - -`@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs -ahead of six predicate faces that previously granted a retired spelling -first-class treatment: the filter builder's operator buckets and its value -control (`@object-ui/components`), the detail page's highlight-strip picker -(`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), -the dashboard's `$expand` whitelist and `isLookupType` -(`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter -control (`@object-ui/plugin-list`). Each one now fires the migration -prescription on the console — once per spelling across all of them, never once -per predicate — and then answers as it would for a spelling it does not -recognise. - -This closes the whole CLASS rather than one word: the gate is quantified over -`RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the -day it lands. It is the shape objectui#4932 and objectui#4942 already -established for the form and inline-edit roads. - -Measured before the change, and the reason the fix is a gate rather than a -deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` -equalled the `user` bucket item for item, `computeLookupExpand` actively -requested `$expand` for it, `isLookupType('owner')` was `true` alongside -`reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as -`picklist` does. Deleting the members alone would have traded a visible -contradiction for a SILENT degradation — a filter picker collapsing to a bare id -box, `$expand` quietly stopping so cells show raw foreign-key ids — which is -verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. -The gate keeps that fallback and adds the half that was missing: the author is -told. - -The boundary question is answered on record: `owner` arriving through a -backend-vocabulary normalizer is an authoring error to refuse loudly, not -legitimate foreign input to tolerate. The open backend vocabulary those -normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, -`datetime_tz` and the rest are equally absent from the spec's closed `FieldType` -and are equally unretired, so they classify exactly as before. - -`RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` -move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that -package's published surface is unchanged apart from the newly ruled gate. -`@object-ui/components` is a consumer of the gate and `@object-ui/fields` -depends on it, so a single shared table could not live in `fields` — and a -second copy would have meant a second dedupe set and two console lines for one -spelling. No package gained a new dependency. - -A retired spelling never loses a stored value: `retypeFilterValue` is -deliberately not gated, and the refused filter row stays operable rather than -drawing a blank operator trigger. diff --git a/.changeset/retired-lucide-spellings-5622.md b/.changeset/retired-lucide-spellings-5622.md deleted file mode 100644 index aa246fa963..0000000000 --- a/.changeset/retired-lucide-spellings-5622.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/components': patch -'@object-ui/plugin-list': patch ---- - -Repair five retired lucide icon spellings that reach a record-reading resolver, and pin -the names against the runtime `icons` record so the next lucide bump goes red instead of -silently blanking a glyph (objectui#5622). - -lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it -as a deprecated named export. A retired name therefore still imports, still type-checks, -and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it -is used as a STRING, because every string lookup here reads that record. Nothing goes red -either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at -implementation time. - -What a user sees change: - -- `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon - again. Its items become an `action:bar` schema whose renderers resolve `icon` through - `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been - drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. -- The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in - both the registration `icon` and the `name` input's `defaultValue`) resolves again: the - designer palette entry's glyph was blank, and an `icon` dropped from that palette - rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette - looks exactly as it did. -- `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` - (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The - gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher - landed in objectui#5586, so one view type no longer draws two different icons depending - on which switcher is on screen. - -Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and -alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather -than resolvability, because every retired spelling repaired here is the SAME component -object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, -`Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, -`XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the -export, would pass on the broken name. That is the blindness that let this ship. diff --git a/.changeset/richtext-cell-renderer-5452.md b/.changeset/richtext-cell-renderer-5452.md deleted file mode 100644 index 8b1448c2dc..0000000000 --- a/.changeset/richtext-cell-renderer-5452.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/fields": patch ---- - -A populated `richtext` field no longer renders as a blank cell (objectui#5452). - -`richtext` stores HTML — the spec documents the type as "Formatted content with -HTML/WYSIWYG", the showcase seed's own specimen is `<p>Rich <strong>text</strong></p>`, -and this repo's designer bridge already maps `richtext` onto its `html` type. The -display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose -sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops -raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the -cell body came out empty — with no error, no fallback and no console warning, so a -populated field read as an empty field and anyone auditing data through a grid -concluded the records were blank. Measured on the same stored bytes, a neighbouring -`html`-typed column rendered them correctly, which is what ruled out "the value never -arrived". - -`richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` -(script/style/iframe/object/embed blocks, inline event handlers and `javascript:` -URLs removed) and keeps everything a rich-text editor legitimately emits — headings, -paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at -once: the grid, the kanban card, the gallery, the related list, the dashboard record -panel and the record detail page all resolve their read-mode cells through this same -`getCellRenderer`. - -The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" -one type by moving every `markdown` cell's trust boundary, so `markdown` still drops -raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. diff --git a/.changeset/rotten-pugs-invent.md b/.changeset/rotten-pugs-invent.md deleted file mode 100644 index 752b948778..0000000000 --- a/.changeset/rotten-pugs-invent.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Page-block canvas: draw the real icon and colour tone for block types that render fine but are not offered in the palette. - -The canvas read its per-node icon and tone from `BLOCK_TYPE_META`, which is the palette's *offer* list ("what may an author drag in") — while the canvas is answering a different question ("what may an author already have in this page"). The two diverge for an alias pair: `record:discussion` and `record:chatter` are one renderer under two names, so exactly one spelling is always unoffered, and a page carrying it showed a plain unknown-block box and neutral grey instead of the message icon and the blue record tone its twin gets. - -`block-types.ts` now declares `BLOCK_RENDERER_ALIAS_GROUPS` — spellings that resolve to the same registered renderer — and the canvas resolves display chrome through `resolveBlockDisplayMeta()`, which falls back to an alias sibling's entry. Keyed on renderer identity, not on "is excluded": palette exclusions that genuinely are not page blocks (`element:text_input`, `element:record_picker`, `element:form`, `ai:chat_window`) keep the generic box, and nothing became draggable — the palette offer list is unchanged. diff --git a/.changeset/row-predicate-phase2-record-only-5741.md b/.changeset/row-predicate-phase2-record-only-5741.md deleted file mode 100644 index 2ce7fefa07..0000000000 --- a/.changeset/row-predicate-phase2-record-only-5741.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/react': minor ---- - -row predicates on runtime record surfaces resolve `record.*` only; the bare-field and `data.*` spellings are no longer bound - -Phase 2 of the row-predicate canon (objectui#5330, ruled 2026-08-20, option B; -Phase 2 ruled 2026-09-02 and amended 2026-09-05 on objectui#5741). Until now a -row predicate — `visible` / `disabled` / `enabled` on an action renderer, a row -action, a `record:alert`, a `page:header` action, a conditional-formatting -`condition` — bound the row three ways: canonical `record.status`, bare -`status`, and `data.status`. The two non-canonical spellings are retired on -every runtime record surface, in both evaluation tiers (`evalRowPredicate` / -`partitionRowsByPredicate` in `@object-ui/core`; `usePredicateRecordContext` + -`useCondition` in `@object-ui/react`) and for both dialects: a legacy -`${data.x}` / `${x}` string on a row surface retires with the CEL spellings. - -**What a retired spelling does now: it faults, exactly as it already did on the -server** (`buildScope({ record })` mounts exactly `['record']`, so `status` and -`data` are unknown variables there), and each surface applies its EXISTING -fault policy — no runtime detector, no "treat as absent" special case, no -uniform override: - -- `evalRowPredicate` / `partitionRowsByPredicate` (row kebab, selection bar, - `page:header` actions, conditional formatting): the caller's `fallback` — - hidden / every row excluded / no style — reported once by the existing fault - warning, which names the unknown variable (`Unknown variable: status`) and, - on the fast route, carries the `record.` hint. -- `useCondition` legs that opt into `throwOnError` (`action:button` and - `action:menu` `visible`, `DeclaredActionsBar` `visible`): fail-closed — - hidden on every row, reported once as `was hidden/disabled: its predicate - threw — status is not defined`. -- the non-throwing `useCondition` legs (`action:icon` / `action:group` - `visible`, every `disabled` / `enabled`, `record:alert`): fail-soft — shown / - greyed / enabled on every row, with the evaluator's own console line. -- a host scope that carries its OWN `data` (app-shell's ambient `data: {}`) is - left standing: `data.*` on a record surface then reads the host's object — a - constant, silent `false` — which is what "no longer bound to the row" means. - -The Phase-1 deprecation warning is removed with the bindings: -`warnNonCanonicalRowSpelling` and `resetRowPredicateCanonWarnings` are no -longer exported from `@object-ui/core`. `detectNonCanonicalRowSpelling`, -`ROW_PREDICATE_CANONICAL_ROOT` and the `NonCanonicalRowSpelling` type stay -exported — the offline instrument for sweeping authored metadata. - -The layer rule is unchanged: `data` remains the canonical root on -metadata-editing surfaces (ADR-0089 D3, `CANONICAL_ROOT_BY_LAYER`), and -app-shell's metadata-admin `SchemaForm` / `predicate.ts` keep binding -`{ data: row }` through their own evaluator. - -No stored-metadata survey, export or migration rewrite was run (the maintainer -ruled the stored population out of scope, 「不考虑存量」); the Phase-1 warning -period was the notice. - -Release note: Phase 1 (PR #5737 — the canon statement plus the warning) shipped -in `@object-ui/core@17.6.0` (npm, 2026-08-24) although its changeset -`.changeset/row-predicate-record-canon-5330.md` is still pending on `main`, so -the next CHANGELOG section lists Phase 1 and this Phase 2 together: the warning -it describes was live from 17.6.0 and is gone from this release on. diff --git a/.changeset/row-predicate-record-canon-5330.md b/.changeset/row-predicate-record-canon-5330.md deleted file mode 100644 index cfd76abde8..0000000000 --- a/.changeset/row-predicate-record-canon-5330.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/react': minor ---- - -Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now -warn once, and are unchanged otherwise. - -A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row -scope, a `record:alert`) has bound the row three ways since objectui#4075 — -`record.status`, bare `status`, and `data.status` — without any of them being -declared the contract. The maintainer ruled that question on 2026-08-20 -(objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for -the identical renderer-tolerance shape: **the canon is `record.*`**, and the -other two enter a deprecation window. - -The canon states the **server's** accept set, which was this card's first -measurement and turns out to be strictly narrower than the renderer's. Measured -against `@objectstack/formula@17.1.0`, the engine the server evaluates with: - -| spelling | server runtime | server authoring oracle | -|---|---|---| -| `record.status` | `{ ok: true, value: true }` | accepted | -| bare `status` | `Unknown variable: status` | refused | -| `data.status` | `Unknown variable: data` | **silently accepted** | - -`buildScope({ record })` mounts exactly `['record']` — `data` is never bound and -the row's fields are never flattened to top level. The three-way binding is a -client tolerance with no server counterpart, which is why the warning belongs on -this side. - -`data.*` is the dangerous one, and the reason the warning exists. `data` is in -`@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle -waves it through — that list is a deliberately generous "never faults" lint -baseline, not the runtime accept set. A `data.*` row predicate therefore passes -every authoring gate the platform has and then binds nothing at runtime: not an -error, a constant `false`. A `visible` that is constantly false is a button that -silently never appears — the objectui#4075 fail-closed signature. - -What ships: - -- `@object-ui/core` exports `detectNonCanonicalRowSpelling`, - `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and - `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, - which carries the canon statement and the measurement. -- Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and - `useCondition` (react, for bags bound by `usePredicateRecordContext`). -- Detection reuses the server's own oracles (`collectCelRootIdentifiers`, - `firstUndeclaredReference`) rather than a regex, so no second dialect - judgement is invented client-side. - -**No spelling is removed and no behaviour changes.** Every predicate that -resolved before resolves now — the ruling defers removal behind a stored-metadata -survey, and the warning is what makes that survey possible (ADR-0078: a -tolerance nothing ever reports can never be retired). - -The deprecation is scoped to the **runtime record layer**. `data` remains the -canonical root one layer over, in a metadata-editing form (ADR-0089 D3 -`CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. diff --git a/.changeset/rowcolor-prototype-guard-6295.md b/.changeset/rowcolor-prototype-guard-6295.md deleted file mode 100644 index de76be8132..0000000000 --- a/.changeset/rowcolor-prototype-guard-6295.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -Guard `useRowColor`'s two object-literal lookups with `Object.prototype.hasOwnProperty.call`. - -Both `config.colors` (the authored map) and the module's `COLOR_TO_CLASS` literal inherit -`Object.prototype`, and both were reached with a bare index. A record whose colour field -held `constructor`, `toString`, `valueOf` or `hasOwnProperty` resolved to an inherited -function: the `if (!color)` guard passed it (functions are truthy) and `colorToClass` then -called `.startsWith` on it, throwing a `TypeError` inside the row-className resolver during -render — a grid crash triggered by record data rather than by metadata. The same shape one -call deeper in `colorToClass` did not throw; it handed an `Object.prototype` member back as -the row's `className`, which reached React as a class attribute. Both now resolve to -`undefined`, as an undeclared value always did. diff --git a/.changeset/runtime-config-server-side-pointer.md b/.changeset/runtime-config-server-side-pointer.md deleted file mode 100644 index 835e851994..0000000000 --- a/.changeset/runtime-config-server-side-pointer.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Comment-only change to `app-shell`'s `runtime-config.ts`: the "Server-side" pointer block -now anchors on exported symbol + package (`RuntimeConfigPlugin` from -`@objectstack/cloud-connection`, `RuntimeConfigPlugin` from `@objectstack/objectos-runtime`, -`createStudioRuntimeConfigPlugin` from `@objectstack/service-cloud`) instead of file paths, -two of which had gone stale. No published behaviour changes. diff --git a/.changeset/runtime-served-error-reporting-dsn.md b/.changeset/runtime-served-error-reporting-dsn.md deleted file mode 100644 index 8ffc86943a..0000000000 --- a/.changeset/runtime-served-error-reporting-dsn.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': minor ---- - -Console error reporting is now configured entirely by the **server**: the DSN and every -knob that travels with it arrive on `GET /api/v1/runtime/config`, and the build-time -`VITE_SENTRY_DSN` path is retired (objectui#5522, consumer half of objectstack#12681). - -ObjectStack's users consume a **prebuilt** Console. Under the previous two-key gate — -a build-time DSN **and** a runtime permission — a build-time key was unreachable for -them, so a self-hosting operator could not enable client error reporting at all: the -permission was reachable and the source was not. The maintainer named it on 2026-08-27: - -> 「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」 - -``` -send ⇔ the runtime served a DSN -``` - -**The DSN's presence IS the grant.** There is no companion permission flag, and the one -that briefly existed is removed rather than paralleled. Two knobs in two places produced -two silent dead states — "permission on, no DSN" and "DSN in, permission off" — that look -identical from the browser; one knob cannot disagree with itself. Turning reporting off is -unsetting the server DSN, and there is deliberately no build-time force-off left, because -nobody consuming a prebuilt console could reach one. - -The fail-closed posture is unchanged and structurally stronger. Absence of a *source* is -not a value that can be misread, so a runtime predating the key, a third-party host, a -404, a network failure, a malformed body and a config that has not arrived yet all read as -off — where the boolean needed a strict `=== true` plus a written argument about why a -negative `disabled` flag would have been vacuous on exactly the runtimes that were leaking. - -### What moved, and the one thing that did not - -`sendDefaultPii`, `environment`, `tracesSampleRate` and the error-session replay rate move -into the runtime payload. They were build-time variables, so a prebuilt-console consumer -could set none of them — including the one deciding whether IP and User-Agent leave their -network. This is not new surface; it is the same surface moved to the side that can -operate it. - -`VITE_SENTRY_RELEASE` **stays build-time**, and is now the only `VITE_SENTRY_*` variable -that exists. A release identifies which bundle produced a stack trace and must match the -source maps that bundle's pipeline uploaded — a property of the build, which no server can -know. `VITE_SENTRY_ENABLED`, `VITE_SENTRY_ENVIRONMENT`, `VITE_SENTRY_TRACES_SAMPLE_RATE` -and `VITE_SENTRY_REPLAY` are retired along with `VITE_SENTRY_DSN`. - -### Breaking - -| FROM | TO | -|:--|:--| -| `VITE_SENTRY_DSN=…` in the Console build environment | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=…` on the ObjectStack runtime | -| `VITE_SENTRY_SEND_DEFAULT_PII=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true` | -| `VITE_SENTRY_ENVIRONMENT=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT=…` | -| `VITE_SENTRY_TRACES_SAMPLE_RATE=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE=…` | -| `VITE_SENTRY_REPLAY=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1` | -| `VITE_SENTRY_ENABLED=false` | unset the runtime DSN | -| `isClientErrorReportingAllowed(): boolean` | `getClientErrorReporting(): RuntimeClientErrorReporting \| null` | -| `resolveSentryGate(env, runtimeAllows)` | `resolveSentryGate(runtimeErrorReporting)` | -| `RuntimeTelemetry.allowClientErrorReporting: boolean` | `RuntimeTelemetry.errorReporting?: RuntimeClientErrorReporting` | - -One-line fix for a deployment: move your `VITE_SENTRY_*` values onto the ObjectStack -server as the `OS_TELEMETRY_CLIENT_ERROR_REPORTING_*` variables above, and drop them from -the Console build environment. One-line fix for a consumer of `@object-ui/app-shell`: -`const sink = getClientErrorReporting(); if (sink) …` in place of -`if (buildTimeDsn && isClientErrorReportingAllowed())` — the build-time conjunct is gone, -because the server now supplies the source. - -**Landing order is safe in both directions.** A Console built before this change meets a -new server, reads an absent `allowClientErrorReporting` and stays off; a Console built -after it meets an old server, reads an absent DSN and stays off. Neither half can turn -reporting on by itself, so the two repos' PRs can land in any order. - -The `committed-telemetry-endpoint.test.ts` ratchet is unchanged in rules and unchanged in -job: nothing endpoint-shaped may be committed to this repo. Its rules key on the -variable's suffix and on the value rather than on the `VITE_` prefix, so they already -cover the runtime-side spelling — now pinned, so a later tidy-up cannot narrow them to the -retired names and reopen the hole under a new one. diff --git a/.changeset/safe-field-label-identity-5564.md b/.changeset/safe-field-label-identity-5564.md deleted file mode 100644 index b7ae0e40f1..0000000000 --- a/.changeset/safe-field-label-identity-5564.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`useObjectLabel` now keeps a stable identity when no i18next instance is bound, -so the memoization it advertises holds on the no-provider path too -(objectui#5564). - -react-i18next's `useTranslation` builds its return value out of a fresh `{}` on -every render when it has nothing to bind to (`const finalI18n = i18n || {}`, -which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived -with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`, -so the memo never held: measured 4 distinct returned objects across 4 renders -with no instance, against 1 with one. That is the wrong way round — the memo -exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in -heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case -as the one it exists to serve. - -Both memo dependencies are now pinned to module-level constants while no -instance is bound. The substitution is unobservable rather than merely -convenient: every `t()` call in the module sits inside a -`for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]` -under exactly the same "is there a usable instance" predicate — so while the -substitution is in effect, the closures cannot read either value. When an -instance appears the dependencies become the live values again, so a provider -mounting after first render recomputes the object exactly once and resolves -real translations from then on. - -No API change: no new exports, no signature changes, and the returned surface is -identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside -`useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the -memo's own docstring names. diff --git a/.changeset/scatter-unplaceable-points.md b/.changeset/scatter-unplaceable-points.md deleted file mode 100644 index aa619cb442..0000000000 --- a/.changeset/scatter-unplaceable-points.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -Scatter now says when it cannot place a row, instead of drawing an empty axis. - -Scatter is the only two-measure positional chart in the renderer: `xAxisKey` feeds -a numeric X axis and `series[0]` a numeric Y axis, so a point exists only when -both are numbers. Measured in real Chromium, rows it could not place produced a -tile byte-identical to a scatter handed no rows at all, and six different -authoring failures shared one image. A chart with one placeable row among three -was 99.75% pixel-identical to a genuinely one-row scatter. - -Handed rows it cannot place any of, a scatter now renders the file's refusal -shell under `data-chart-error="no-plottable-points"`, naming both keys. When some -rows place and some do not it draws as before with a `data-chart-note="unplotted-points"` -footnote carrying the count. Charts whose rows all place are byte-identical to -before, and no wrapper element is added to them. - -The predicate is positional, not magnitude-based: zero and negative coordinates -are ordinary scatter data and keep drawing. diff --git a/.changeset/schema-input-bridge-permanent-4622.md b/.changeset/schema-input-bridge-permanent-4622.md deleted file mode 100644 index 4b78051281..0000000000 --- a/.changeset/schema-input-bridge-permanent-4622.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`toRenderableSchema`'s header now says the bridge is permanent, instead of instructing -callers to remove it (objectui#4622). - -No executable line changes — but the artifact is **not** unchanged, and that is worth -stating plainly rather than rounding to "comment-only". This package builds with plain -`tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc -is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both -what an editor shows on hover at every call site and bytes that ship. - -Measured by building the package the way the repo builds it, at both revisions: -`dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850 -to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19 -differing lines in the emitted file are JSDoc continuations and the three executable lines -are byte-identical, so the growth is the paragraph and nothing else. The trade is -deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old -paragraph's instruction produced once already. - -The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have -not been reconciled" and that "when it lands, the call sites using this can go back to -forwarding directly". Both halves went false when PR #4608 merged, and the second half is -the harmful one: it is an instruction whose trigger condition has now fired, sitting -directly above the function a future author is about to call. - -The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of -`@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring -an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548 -ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So -a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a -permanent crossing between two intentionally different types rather than scaffolding -awaiting a merge. - -Following the old instruction has a measured cost: five `apps/site` call sites were -forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly -five hours until PR #4621 routed all five through this function (objectui#4617). diff --git a/.changeset/sdui-parser-inline-whitespace-5661.md b/.changeset/sdui-parser-inline-whitespace-5661.md deleted file mode 100644 index 7affbea3dc..0000000000 --- a/.changeset/sdui-parser-inline-whitespace-5661.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/sdui-parser': patch ---- - -`kind:'html'` page sources keep the space between a text run and an adjacent -inline element: `A <strong>x</strong> page` now compiles to `A `/`<strong>`/` -page` and renders as `A x page` rather than `Axpage` (objectui#5661). - -The parser collapsed each text run's whitespace to a single space — correct, and -what HTML itself does — and then `.trim()`ed it, which is not: HTML collapses a -whitespace run to one space, it does not delete it. The deleted space was -precisely the separator between a run and its inline sibling, so every authored -sentence carrying emphasis or a link in the tier the guide recommends by default -rendered with its words run together. It was silent: the page rendered, the -structure was right, no diagnostic fired. - -The rule is deliberately mechanical rather than a block/inline taxonomy invented -for a schema tree that has none: keep one leading space when a sibling precedes -the run, and one trailing space when a sibling element follows it. The parent's -own start and end still drop their edge space, so `<p> hi </p>` is unchanged. - -Its one bounded cost: a whitespace-only run BETWEEN two siblings survives as a -single space, so a pretty-printed `<ul>` gains one `' '` string child per gap -between its `<li>`s — one space per gap, never the source's newline and -indentation, never at the container's own edges, and never inside an item's own -text. That bound is pinned by a test rather than left as a claim. diff --git a/.changeset/sdui-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md deleted file mode 100644 index f14b09da71..0000000000 --- a/.changeset/sdui-preview-page-source-tailwind-5470.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Publishes nothing — declared with an empty frontmatter rather than left undeclared. - -The changed files are `apps/console`'s three ADR-0080 browser preview harnesses -(`src/sdui-*-preview.tsx`) plus a test. They are dev-server-only: `apps/console`'s -vite config declares no `build.rollupOptions.input`, so the build's only entry is -`index.html` (resolved config, measured: `input` unset) — the three -`sdui-*-preview.html` entries and the modules behind them never enter `dist/`. -`@object-ui/console`'s `files` omits `src`, and its `exports` map has exactly one -entry (`.` → `./plugin.js`, built from `tsconfig.plugin.json` including -`plugin.ts` alone, which imports nothing from `src/`). Nothing here reaches a -consumer. - -Behind the change: page `source` is runtime metadata, and the console's Tailwind -is compiled at build time by scanning the console's own `src` with no safelist, so -a utility class authored in real page metadata produces no CSS and no error -(ADR-0065; ADR-0080's 2026-06-30 amendment). `sdui-tiers-preview.tsx` — the -harness making an explicit authoring claim — now styles with each tier's real -primitive (the html tier with `<flex>`'s structured props plus JSON `style` -objects, the react tier with inline `style` objects, colours as -`hsl(var(--token))`), so it demonstrates what authors are told to write and now -follows the theme in light and dark. The two renderer-plumbing harnesses keep -their Tailwind and declare the exception in their headers; a test pins both -halves against the shipped `page-source-className-tailwind` rule. diff --git a/.changeset/secret-widget-spellings-5375.md b/.changeset/secret-widget-spellings-5375.md deleted file mode 100644 index 2f405f2118..0000000000 --- a/.changeset/secret-widget-spellings-5375.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/core': minor ---- - -Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. - -Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path -(no `registerAllFields()`), before and after objectui#5322's fix: - -``` -type registry hit rendered type -ui:password true text -secret false text -field:secret false text -``` - -Two halves, per the maintainer ruling of 2026-08-20: - -- **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring - ERROR.** A form field's widget id (`widget`, else `type`) may name the - `field:` namespace or a bare name; any other namespace resolves no field - widget (objectui#5254) and used to degrade silently to a plain text box. - `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and - `assertValidSchema` throws. Behaviour change: a schema that previously - validated with e.g. `type: 'ui:password'` is now invalid — inventing a - plausible-looking widget id fails loudly instead of rendering clear text. - `field:` ids stay valid whether or not the widget is registered, since - registration is a runtime fact an authoring-time validator cannot see. -- **`@object-ui/components` — the known secret types cover the remaining - spellings.** Bare `secret` and `ui:password` render the native masked input, - and `field:secret` is refused outright like `field:password`. Existing authors - need no migration. - -`ui:password` **is** registered — as an SDUI node renderer for a top-level -`{ type: 'email' }`-style node — so an author who checked whether it resolved -got a yes and still got a clear-text box on the field path. No producer emits -any of the three; all are reachable only through a hand-authored standalone -form schema, which is exactly the surface where the author is the producer and -no normalizer sits in between. diff --git a/.changeset/self-import-gate-scan-once-5402.md b/.changeset/self-import-gate-scan-once-5402.md deleted file mode 100644 index 32ece0aee5..0000000000 --- a/.changeset/self-import-gate-scan-once-5402.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`scripts/check-package-self-import.mjs` now separates the SCAN from the JUDGEMENT: -`scanRepository(root)` performs the expensive TypeScript parse once, and `judgeScan(scan, -exemptions)` is a pure filter over its result. `analyze()` keeps its signature and its -behaviour exactly (differentially verified against the previous implementation over both a -fixture tree and this repository, across all six exemption-table shapes), and the CLI's -output is byte-identical. - -The reason is `scripts/__tests__/check-package-self-import.test.ts`, which asserts this -repository is green under the repository's own exemption table AND under no exemptions at -all. Those were two calls to `analyze(repoRoot)` — the same full parse of ~3,100 files and -~30 MB of source performed twice — and one of them sat inside a 15-second `it()`. That -fits uninstrumented (measured 8.8 s) and does not fit under v8 coverage (measured 41.3 s), -so `ci.yml`'s `Test (coverage)` job failed 100% of the time from 2026-08-16 — 51 completed -jobs, 0 successes, 50 of them this one file, every one `Test timed out in 15000ms` — and -Codecov received nothing for four days (objectui#5402). - -It is a constant factor, not a race. `@vitest/coverage-v8` arms -`Profiler.startPreciseCoverage({ callCount, detailed })` in the worker BEFORE test modules -and their dependencies compile; V8 emits those block counters at compile time and does so -isolate-wide, so `node_modules/typescript` is instrumented too — `coverage.exclude` filters -the report, never the instrumentation. Measured here, the identical parse costs 4.1-4.9 s -uninstrumented and 32-35 s when coverage was armed first, and starting coverage AFTER the -same code is compiled costs nothing at all. - -The test file now scans once at module scope and judges it per assertion: the timeout-prone -test drops from 41,268 ms to 1 ms under coverage, and the file's total work halves. No -timeout was raised, nothing is skipped, and coverage is not disabled for anything. - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/sentry-undisableable-telemetry-5522.md b/.changeset/sentry-undisableable-telemetry-5522.md deleted file mode 100644 index 11941d4db1..0000000000 --- a/.changeset/sentry-undisableable-telemetry-5522.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in -(objectui#5522). - -`@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from -`apps/console/.env.production` — is what the hosted SaaS console and the on-premises / -air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the -bundle as a frozen object literal, so the DSN committed there was a live third-party -telemetry endpoint compiled into artifacts that land inside customer networks. It could -not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off -that same frozen literal, so on a shipped bundle it is `undefined` forever and editing -env vars on the deployed host does nothing. An air-gapped deployment was measured -sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by -the customer. - -- `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, - `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never - imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. -- `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so - IP address and User-Agent are never the inherited default of a build that did not ask - for them. -- The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not - send. The direction is deliberately inverted from the usual — an unreported error is - recoverable, PII leaving an air-gapped deployment is not. - -**Action required for deployments that want error reporting** (the hosted SaaS/demo -console): inject `VITE_SENTRY_DSN` from your build environment, the same way -`VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you -still want IP/User-Agent on events. Nothing else changes for builds that opt in. diff --git a/.changeset/settled-schema-resolution-hook-6482.md b/.changeset/settled-schema-resolution-hook-6482.md deleted file mode 100644 index 7f3f4345fe..0000000000 --- a/.changeset/settled-schema-resolution-hook-6482.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/react': minor ---- - -New export: `useSettledSchema` — the settled-schema RESOLUTION half shared by -`ObjectKanban` / `ObjectView` / `ObjectCalendar`'s fetch-gate hand copies -(objectui#6482, maintainer ruling Option A). It tracks whether an object's -definition has finished resolving FOR THE KEY THE CURRENT RENDER IS ASKING -ABOUT, returning `{ ready, def }` from one piece of internal state so `ready` -and `def` can never be observed inconsistently and a stale key can never read -as ready — the structural fix for the `ObjectTree` defect (objectui#6481) -where a definition and a separate, one-way-latched "settled" boolean could -disagree for a render after the object changed. - -Gate PLACEMENT — which effect branch actually waits on `ready` — stays a -per-component decision and is not part of this hook; see the hook's own doc -comment. Existing hand copies are migrated on their own subsequent cards, not -by this change. diff --git a/.changeset/shard-coverage-job-5403.md b/.changeset/shard-coverage-job-5403.md deleted file mode 100644 index 8e30f8848f..0000000000 --- a/.changeset/shard-coverage-job-5403.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. `ci.yml`'s coverage lane is sharded 4 ways with a blob-report -merge, and the Codecov upload can no longer go missing in silence: the merge job states -on every path whether Codecov received a report for the commit, and is red when it did -not. diff --git a/.changeset/shared-zod-wrapper-keys-6923.md b/.changeset/shared-zod-wrapper-keys-6923.md deleted file mode 100644 index 317c2a9eff..0000000000 --- a/.changeset/shared-zod-wrapper-keys-6923.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Internal only — no user-visible change, nothing to release. - -objectui#6923: the Zod wrapper-key list that five test/gate sites each spelled -out by hand now lives once, in `packages/test-support` (a `private: true`, -never-published package), and is read by both the TypeScript suites and the -`.mjs` CI gates. Only test files, CI gate scripts and the private -`test-support` package change; no released package's runtime code is touched. diff --git a/.changeset/signout-purge-per-key-try-5777.md b/.changeset/signout-purge-per-key-try-5777.md deleted file mode 100644 index 8b5252c493..0000000000 --- a/.changeset/signout-purge-per-key-try-5777.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's -`objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a -single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777). - -The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n` -aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed — -`AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are -the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198 -classifies a surviving entry as a cross-principal disclosure on a shared browser, not -mere staleness — so a partial sweep here is the sharper half of the same defect class -objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`). - -`Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on -its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is -not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()` -(objectui#5731) quarantines a key: this function does not own reads for the metadata -seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to -guard and nothing to quarantine — adding read-back verification would be a general -storage-error-handling refactor of the module, out of this card's scope. What is -mirrored is the reporting channel: a key whose `removeItem` throws is named in a -`console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is -discoverable instead of silent. - -Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata -key, asserting every other metadata key on both sides of it is still swept and unrelated -non-matching keys are untouched, plus a control that the warning fires only on an actual -failure. diff --git a/.changeset/simple-form-consults-declared-submit-handler.md b/.changeset/simple-form-consults-declared-submit-handler.md deleted file mode 100644 index 74dfa9e5ef..0000000000 --- a/.changeset/simple-form-consults-declared-submit-handler.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`SimpleObjectForm`: consult a declared `submitHandler` before the inline-fields carve-out - -`ObjectFormSchema.submitHandler` is documented as handing the collected values to the host INSTEAD of calling `dataSource.create` / `dataSource.update`, so a form that declares it has a submit target with or without an adapter. `SimpleObjectForm.handleSubmit` nevertheless opened with the inline-fields carve-out (`hasInlineFields && !dataSource`), which returned before the persistence chain: a host that had declared it owns the write was never asked, and `onSuccess` confirmed a write that never happened (measured `onSuccess 1 / submitHandler 0`). - -The carve-out now fires only when no `submitHandler` is declared, and the "no submit target" refusal moved into the persistence chain after the seam — the shape the five variant renderers already use, reusing their shared refusal from `submitTarget.ts` rather than a private copy. A form with inline fields and no seam is unchanged: its `onSuccess` is still the write. diff --git a/.changeset/six-donkeys-shake.md b/.changeset/six-donkeys-shake.md deleted file mode 100644 index 3a674dcb50..0000000000 --- a/.changeset/six-donkeys-shake.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change to three `apps/console` test files: the `vi.mock('@object-ui/app-shell', …)` -factories no longer call `importOriginal()`, which was transforming the whole source-aliased -barrel graph (measured 10019 ms per file) to reach a handful of real exports. They now spread -only the app-shell submodules those exports live in. No published behaviour changes. diff --git a/.changeset/skill-provider-envelope-teaching-5372.md b/.changeset/skill-provider-envelope-teaching-5372.md deleted file mode 100644 index 4e6dd9e2a7..0000000000 --- a/.changeset/skill-provider-envelope-teaching-5372.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Published-skill teaching only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. No package `src/` is touched: the -change is confined to `skills/objectui/**` (the published skill package) plus one new -test under `packages/components/src/__tests__/`, which pins the corrected teaching to -the real renderer. - -The rules told authors that the `properties` / `props` envelope belonged to the -`element:*` namespace and that every other key "lives on the node". Measured on a real -`SchemaRenderer` inside a `SchemaRendererProvider`, `properties` is evaluated and then -hoisted onto the node in *every* namespace — so it was the only spelling that reached a -`data-table`'s rows from a provider `dataSource`, while the node-level and `props` -spellings rendered a header over the empty state with nothing thrown and nothing logged. -The guides now record that measurement instead of contradicting it. diff --git a/.changeset/sparse-predicate-warning-cause-5399.md b/.changeset/sparse-predicate-warning-cause-5399.md deleted file mode 100644 index adebbd2ba1..0000000000 --- a/.changeset/sparse-predicate-warning-cause-5399.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it -states what it actually measured (objectui#5399). - -When an action's `visible` predicate references a `record.<key>` the bound payload -does not carry, the warner names the missing key and then explained the cause: - -> Hidden (hidden: true) fields are stripped from detail payloads server-side, so a -> predicate gating on one may evaluate to a hide-by-default verdict. - -That cause is false, and it names a mechanism this repo does not own. `hidden` is a -UI concern — the framework spec describes it as "Hidden from default UI" -(`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against -the framework checkout rather than taken on trust: ObjectQL's own strip for the -`__search` companion documents that the `hidden` / `readonly` / `system` markers are -"None of them is a PROJECTION rule", which is precisely why a dedicated strip rule -had to be written for that one column; drivers answer a query with no `fields` using -`SELECT *`; and `metadata-protocol` enumerates what the read path does drop — -`internal: true` columns and the `__search` companion, and nothing else. The only two -read-side uses of `field.hidden` in the framework are auto-view/auto-form column -generation and companion-source eligibility, neither of which removes a key from a -record body. - -So an author who read this diagnostic went hunting for a `hidden` flag they would -either not find, or find on a field the payload demonstrably still returns — while -the real source of the sparseness (a projected or partial read) went unexamined. A -confidently wrong cause in a diagnostic is worse than no cause, because it is -actionable in the wrong direction. - -The replacement states the fact this surface can actually see and the consequence it -does own: the page bound a payload without those keys, a projected or partial read -will not carry them, and the predicate therefore fails closed and hides the action. -The measured half of the message — action name, missing fields, predicate source — -is unchanged, and nothing about what triggers the warning changed. - -Message text only. The same false claim also sat in this warner's own doc comments -and in the comments of the test that pins the message; both are corrected here, and -the docstring now carries an explicit note against re-attributing the cause to -`hidden: true`. No other call site was swept. diff --git a/.changeset/spec-17-3-0-lockfile-bump.md b/.changeset/spec-17-3-0-lockfile-bump.md deleted file mode 100644 index 167ba120b8..0000000000 --- a/.changeset/spec-17-3-0-lockfile-bump.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Dev-time only: move the `@objectstack/spec` resolution in `pnpm-lock.yaml` from -17.2.0 to 17.3.0. No published package changes — every manifest's declared range -(`^17.0.0` / `^17.1.0` / `^17.2.0`) already admitted 17.3.0, so no floor moved and -no package's dependency declaration differs by a byte. Empty frontmatter is the -deliberate "no release" declaration for a change that publishes nothing. diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md deleted file mode 100644 index 261be17ee2..0000000000 --- a/.changeset/spec-pin-17-1-0-5328.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor -'@object-ui/core': minor -'@object-ui/app-shell': minor ---- - -Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. - -The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. - -**A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. - -The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. - -`@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. - -**A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. - -**The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. - -**The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. diff --git a/.changeset/spec-refresh-17-2-0-5668.md b/.changeset/spec-refresh-17-2-0-5668.md deleted file mode 100644 index 541883958e..0000000000 --- a/.changeset/spec-refresh-17-2-0-5668.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains. - -**The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`. - -The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists. diff --git a/.changeset/spellgantt-revoked-proxy-exclusion.md b/.changeset/spellgantt-revoked-proxy-exclusion.md deleted file mode 100644 index 164a41f979..0000000000 --- a/.changeset/spellgantt-revoked-proxy-exclusion.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Docs and tests only for `@object-ui/plugin-timeline`. `spellGanttDateValue`'s -docblock claimed every branch was total; measured in-render, `Array.isArray` -throws on a revoked `Proxy`, so the claim is now scoped to the input set that -is actually exercised and the exclusion is pinned as rows in the existing -adversarial set. The same measurement falsified the wider claim that -`Array.isArray` is the last non-total operation on the gantt date path — five -reads upstream of the helper throw first, recorded in objectui#7153. No -published behaviour changes. diff --git a/.changeset/spotty-pages-drop-leaks.md b/.changeset/spotty-pages-drop-leaks.md deleted file mode 100644 index 4bd3fc7907..0000000000 --- a/.changeset/spotty-pages-drop-leaks.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/components': patch ---- - -fix(components): the `page` wrapper now filters its DOM attributes through the shared whitelist instead of a hand-maintained list - -`PageRenderer` stripped PageSchema's descriptor keys with a hand-maintained -destructure list and spread the remainder onto its wrapper `<div>`. Whenever -that list fell behind the schema, an authored key was not dropped — it was -forwarded, and React stringifies unknown attributes in silence, so an authored -`actions: [{…}, {…}]` reached the DOM as `actions="[object Object],[object -Object]"`. - -The renderer now calls `toDomProps` from `@object-ui/core` — the same -whitelist every converged SDUI widget already uses (objectui#4425). Twenty-five -attributes measured leaking off the wrapper stop being emitted, including the -`context` bag the console injects into every page it renders. Everything the -wrapper legitimately carries is unchanged: `class`, `style`, `id`, `role`, -`tabindex`, `data-page-type`, `data-obj-id`, `data-obj-type`, and the open -`data-*` / `aria-*` families. - -This changes no schema's accept/reject behaviour — `BaseSchema.passthrough()` -is untouched — and adds no read point for any previously leaking key. diff --git a/.changeset/spotty-pianos-shave.md b/.changeset/spotty-pianos-shave.md deleted file mode 100644 index a32380b233..0000000000 --- a/.changeset/spotty-pianos-shave.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: removed two `eslint-disable-next-line react-hooks/static-components` directives that the rule no longer needs in the record-picker i18n test files. No published behaviour changes. diff --git a/.changeset/spotty-pins-cover-fields.md b/.changeset/spotty-pins-cover-fields.md deleted file mode 100644 index f05f863e4b..0000000000 --- a/.changeset/spotty-pins-cover-fields.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -`scripts/__tests__/plugin-published-stylesheet.test.ts` now runs its per-package -assertions over `@object-ui/fields` as well as the two plugin sheets. Test-only: -no published package's source changes, so this declares "no release" explicitly -rather than bumping anything. - -Fields is the package whose stylesheet has actually been shipping to consumers -since objectui#4059 — the defect the test's own header cites as the reason the -shape exists — and it was the one supplement sheet the suite did not cover. Its -`dist/index.css` was guarded only by the four write-time assertions inside the -shared builder, which run during a build; CI runs this suite on an unbuilt -worktree, so nothing in the test run inspected it. It could not be a subject -before objectui#6405 re-pointed it at the shared -`createPluginStylesheetBuilder`, because it ran its own copy and exported no -module surface. - -The `CARD_THEMED` entry for fields is derived from the sheet the build actually -emits — the surviving classes whose declarations resolve a `--color-*` token -from components' unpublished `@theme` block — rather than read back off fields' -own `MUST_SURVIVE`, which would have pinned nothing. diff --git a/.changeset/stale-4163-pointers-5591.md b/.changeset/stale-4163-pointers-5591.md deleted file mode 100644 index 7b77930b57..0000000000 --- a/.changeset/stale-4163-pointers-5591.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`setLocalized`'s published docblock states the single-locale write rule that is -actually in force, instead of deferring the multi-locale-authoring question to a -closed card (objectui#5591). - -The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163 -closed as completed on 2026-08-15 with that product question still unanswered, so the -parenthetical pointed at nothing — and it read as though the question had been settled -somewhere a reader could go and check. This is the failure mode objectui#5428 -demonstrated is not harmless: there, a dangling deferral of exactly this shape let an -expired justification sit unread for a release cycle at two surfaces. - -The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in -force (`setLocalized` reaches only the entry for the locale the author is in), keep the -open product question open **in place**, and record why there is deliberately no tracker -reference — so the next reader cannot restore one. Re-pointing is how the class -regenerates, because the next card closes too. The same wording form already landed in -`plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`. - -Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with -`pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`. - -Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per -package rather than assumed, and the two packages this change touches differ: - -- `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches - the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is - `composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count: - `dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js` - `06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so - it publishes something. -- `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between - declarations and a test docblock, neither attached to an exported declaration. - `dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild - (`93252e8cdf5a6faa…` both sides). The only artifact that moved is - `dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above - the declarations; no declaration text changed. Nothing user-visible publishes from that - package, so it is not named here. diff --git a/.changeset/stale-disable-directives-4853.md b/.changeset/stale-disable-directives-4853.md deleted file mode 100644 index 5cb6141c27..0000000000 --- a/.changeset/stale-disable-directives-4853.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Deleted two dead `eslint-disable` directives, each naming a rule that is not -configured for the file it sits in. Comment-only: no runtime source, no test -assertion and no published contract changes, so this releases nothing. diff --git a/.changeset/standard-widgets-phone-coverage.md b/.changeset/standard-widgets-phone-coverage.md deleted file mode 100644 index b6f027dcab..0000000000 --- a/.changeset/standard-widgets-phone-coverage.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-only change to `@object-ui/fields`: adds the missing `PhoneField` cases to -`standard-widgets.test.tsx` and documents where `DateField` and `TextAreaField` -coverage already lives. No published behaviour changes. diff --git a/.changeset/standing-pending-drafts-bar-5694.md b/.changeset/standing-pending-drafts-bar-5694.md deleted file mode 100644 index f1d669f659..0000000000 --- a/.changeset/standing-pending-drafts-bar-5694.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -AI build surface gains a standing 「未发布改动」 bar (#5694): while the conversation's bound package has pending drafts, a bar floats above the composer — surviving scrolling — counting the unpublished changes and publishing them through the same governed `publish-drafts` route as the inline card button, with probe findings surfaced instead of a blind success toast. Renders nothing when the count is zero or the conversation is unbound. diff --git a/.changeset/studio-jargon-and-tool-card-i18n.md b/.changeset/studio-jargon-and-tool-card-i18n.md deleted file mode 100644 index c1fddafc88..0000000000 --- a/.changeset/studio-jargon-and-tool-card-i18n.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-chatbot': minor -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Studio workbench and AI tool cards speak the author's language (objectui#7254) - -- The Interfaces breadcrumb, canvas caption and navigation rail show the - metadata label plus a translated kind; the internal `type · name` pair moves - to the tooltip. An unlabelled nav leaf now falls back to its object name - instead of rendering an empty row. -- The Studio top-bar package switcher reads the package's human name from - either position the packages endpoint serves it in, instead of degrading a - registry-shaped entry to its reverse-domain id. -- The dashboard property panel is localized: the spec's authoring form is - overlaid through the platform's own `metadataForms.<type>` convention, so - section headings, field labels, hints and the `header` composite's sub-fields - render in Chinese (developer vocabulary such as "Tailwind units" is replaced - with something an author can act on, not transliterated). -- AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty - platform-provided tools, ten locale packs), the header status badge is - localized, and the plan count strip is a real plural family instead of an - English `+ "s"` concatenation. -- The tool card's header badge and its body badge now come from one producer: - a proposal that has been confirmed, built or published no longer keeps a - header reading "Awaiting Approval". diff --git a/.changeset/studio-metadata-form-ux-5416.md b/.changeset/studio-metadata-form-ux-5416.md deleted file mode 100644 index 551450b9e7..0000000000 --- a/.changeset/studio-metadata-form-ux-5416.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button. - -Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on -the first surfaces a new author sees. - -**Validation no longer runs on mount.** `新建软件包` opened with both required -fields already red: the create draft is `{ version, type }`, so -`ManifestSchema.safeParse` reported `name` and `id` missing on the very first -render, and the dialog then jumped a line height per field as each error -cleared. `SchemaForm` now defers the error *line* until the row has been -touched — first focusout anywhere in it, or the field's own first edit — for -create forms only. The rule itself did not move: `issues` still reaches the host -unchanged, so the submit button is gated on exactly the same validation, and -edit/view forms still report from mount, where the issues describe stored values -rather than something half-typed. - -**The package form's help text is translated.** The labels came from the -metadata-admin i18n bundle and the help line under each one came straight from -`ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over -"Human-readable package name". `getPackageForm` now reads each field's help -through the same bundle as its label, via a new `tOptional` that returns -`undefined` rather than echoing the key back. Only zh entries exist: an en-US -console finds nothing and keeps falling through to the spec's own sentence, so -the English keeps exactly one producer — `@objectstack/spec` in the framework -repo — and this repo never holds a copy of it to drift. - -**The publish panel's primary button opens clear of the toast stack.** The -console mounts its toaster bottom-right and `DraftChangesPanel` is a -`side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way -there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed -out. Opening the panel now clears the stack the surface the author just left had -raised. Repositioning the toaster was measured and rejected: the draft preview -bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar` -anchors bottom-centre and the nav rail owns the left edge, so a move only -relocates the same collision onto a different primary control. - -Not fixed here, and not fixable here: the other strings the card names are -produced outside this repo. `Owning Business Unit` (`packages/spec`), -`Search Index` (`packages/objectql`) and the `Revise Window` flow node's name -and description (`packages/plugins/plugin-approvals`) all come from the -framework, which owns their translation catalogue; patching them in the console -would create a second source of truth that diverges at the next framework -release. diff --git a/.changeset/studio-nav-canonical-keys.md b/.changeset/studio-nav-canonical-keys.md deleted file mode 100644 index 73ce02c260..0000000000 --- a/.changeset/studio-nav-canonical-keys.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio Interfaces: the nav-leaf binding reads the canonical target key only - -`resolveSurface` fell back to the bare spellings `page` / `object` / -`dashboard` / `report`, and carried a `case 'view'`. Every -`NavigationItemSchema` member is a `strictObject`, none of those bare -spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is -not one of the union's nine members — so all of them are keys `AppSchema` -answers with `unrecognized_keys`, and every one of those branches could only -fire on an app that cannot be saved. The Studio nav item inspector's object -picker likewise read `node.object ?? node.objectName`, preferring the rejected -spelling over the canonical one; it now reads the canonical key first. - -No shape that parses today stops parsing: this narrows the designer back to -what the contract already declares. diff --git a/.changeset/studio-new-object-asks-for-owd-5418.md b/.changeset/studio-new-object-asks-for-owd-5418.md deleted file mode 100644 index 05f2f1e0cc..0000000000 --- a/.changeset/studio-new-object-asks-for-owd-5418.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. - -Creating an object through Studio collected exactly two things — display name and -identifier — and saved a draft that declared no `sharingModel`. The draft saved -happily, the form designer worked, and the object was then refused at 发布 → -全部发布 by `security-owd-unset`: a required decision the surface never asked -for, delivered by failing, as English ADR prose in a toast that then vanished on -a timer. The one actionable word in it named a control three clicks away that -nothing routed to. - -The publish gate is correct and is unchanged — an org-wide default has to be an -authored decision, not an accident. What changes is when the console asks and -when it answers: - -- **The create dialog asks.** A third field collects the baseline, pre-selected - to `private` and glossed with the Settings tab's own strings, so a new object - is publishable by construction. `buildObjectSkeleton` now takes the value as a - required parameter — a future create path cannot omit the baseline without - failing to type-check. `controlled_by_parent` is deliberately not offered at - creation: it derives access from a master relation a brand-new object does not - have yet, so offering it would trade one publish refusal for another. -- **The review sheet reports it.** The pending-changes panel now runs the - framework's own `validateSecurityPosture` over the pending object drafts and - names any blocking finding, with its fix-it hint, next to the Publish button. - It mirrors the producer's rule rather than re-deriving it, and it reports - without blocking — the server door stays the authority. -- **The Settings tab stops calling an unset baseline safe.** It described unset - as "defaults to Private", which answers what the runtime does and not whether - the object can ship. It now reads as the publish-blocking problem it is, - styled like the external-wider warning beside it. diff --git a/.changeset/studio-route-entry-capability-5519.md b/.changeset/studio-route-entry-capability-5519.md deleted file mode 100644 index bed8579047..0000000000 --- a/.changeset/studio-route-entry-capability-5519.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Console now gates the `/studio/*` routes on the `studio.access` ENTRY -capability, not just on the backend's refusal of the writes behind them -(objectui#5519). - -`/_console/studio/` rendered the full Studio pillar builder — Data / -Automations / Interfaces / Access, with Publish and Save draft — to any -authenticated principal who typed the URL, on deployments where the Studio nav -tile is deliberately absent and every metadata write is refused. A plain tenant -user was walked through the entire "new package" form and only refused at -submit (403). The lockdown criterion for that deployment shape is two-part — UI -entry hidden AND API refused — and only the API half was met; what stood on -this side was a write-level gate where an entry-level one belongs. - -The whole `/studio` subtree now hangs off one route element that reads -`systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this -app already consumes) and admits only a principal whose LOADED set carries -`studio.access` — the capability declared as "Enter the Studio metadata-design -surfaces", which a tenant org owner does not hold by design. Everyone else is -sent to `/home` without the builder ever mounting. - -The fail direction is deliberately inverted from this app's other capability -gates: those fail OPEN on an unknown answer because their bad outcome is a -holder losing a button, whereas a route gate's bad outcome is a non-holder -seeing the builder. So the loading window renders the console splash (never the -builder), an outright fetch failure renders the retryable error splash, and a -`200` that carries no `systemPermissions` at all is refused rather than waved -through. The server-side refusals are untouched. diff --git a/.changeset/studio-simplify-5813.md b/.changeset/studio-simplify-5813.md deleted file mode 100644 index c5860b40c1..0000000000 --- a/.changeset/studio-simplify-5813.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Studio simplification, first cut (#5813): the Data pillar's object sub-tabs collapse to 记录/表单 with the five power panels (验证/钩子/操作/API/设置) behind one 「高级」 menu (the open panel's name wears the active pill, so the collapsed default never hides where you are); drafts now AUTO-save (debounced 1.5s, CEL-blocking gates the timer per the objectui#4306 rule, a failed save waits for the next edit) and the four 保存草稿 buttons are retired in favor of a quiet saving/last-saved hint; the 权限 pillar moves from the top-level row into a 「更多」 overflow whose items carry the same dirty-guard as the primary pillars — the /studio/:pkg/access route and every page behind it are untouched. diff --git a/.changeset/submithandler-variant-forms.md b/.changeset/submithandler-variant-forms.md deleted file mode 100644 index 84007e9a29..0000000000 --- a/.changeset/submithandler-variant-forms.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -Honour the declared `submitHandler` seam in every form variant, not just the simple one. - -`ObjectFormSchema.submitHandler` is documented as the seam a host uses to own persistence: the form validates and hands the collected values over instead of calling `dataSource.create` / `dataSource.update`. `ObjectForm` forwarded the key into every variant it routes to, but only `SimpleObjectForm` read it — `TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` persisted directly. - -**Behaviour change on a persistence path.** A master-detail parent half rendered `tabbed` (or `split`) now commits through the atomic `batchTransaction` together with its child collections, instead of writing the parent independently through `dataSource.create`. Previously the child leg was never attempted on those layouts: the parent was committed alone, the entered line items were silently discarded, no compensation ran, and a success toast confirmed the save. A failing child leg now leaves no committed parent, on every layout that renders the parent half inline. - -`WizardForm` additionally skips its own default success toast / redirect arms when a `submitHandler` is present, matching `ObjectForm`, so a host that owns the write also owns the outcome. - -The `object-master-detail-form.formType` vocabulary is unchanged and stays `simple | tabbed`. diff --git a/.changeset/surface-context-send-1610.md b/.changeset/surface-context-send-1610.md deleted file mode 100644 index 0b92a22514..0000000000 --- a/.changeset/surface-context-send-1610.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-chatbot': patch -'@object-ui/i18n': patch ---- - -The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. diff --git a/.changeset/surface-deeplink-live-channel-5476.md b/.changeset/surface-deeplink-live-channel-5476.md deleted file mode 100644 index 9515eb71b3..0000000000 --- a/.changeset/surface-deeplink-live-channel-5476.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's pre-publish security block can now take you to the object it names. - -The pending-changes sheet reports what the publish door would refuse — as -`object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under -Settings → Record sharing". Naming it was the half that shipped; reaching it was not. -The `?surface=<type>:<name>` deep-link that would have carried the author there -captures the URL exactly ONCE, at mount, and the sheet is opened over an -already-mounted pillar — so writing the param changed the URL and moved nothing. - -That mount-time capture is deliberate and stays exactly as it was: the mirror half -rewrites the param on every in-pillar selection, so a capture that followed the URL -would re-trigger its restore on each one. What was missing is a third half — a live -target delivered BESIDE the URL, which is what a producer already inside the pillar -needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity -(`{type, name}`), the host routes cross-pillar requests back through the URL (that -pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the -author declines over unsaved edits, and the mounted pillar applies the rest. - -Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next -rail reload would drag the author back off whatever they had since selected — the -regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` -pins a hand-picked object surviving a package switch, and -`surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it -still ignores every URL change after mount, and a live request never moves it. - -The sheet's other home is the Home / draft-preview bar, where the Studio object editor -is not a reachable destination at all. Reachability is answered structurally — the -producer hook returns `null` when no host published the channel — so off-Studio the -item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both -directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. - -Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its -signature, its return and its behaviour, and only the Data pillar subscribes to the -new channel. The channel is its own React-only module on purpose — importing the hook -into the sheet would have pulled `nav-selection` and the App-nav inspector into the -console's eager graph. diff --git a/.changeset/sweep-store-per-key-try-5763.md b/.changeset/sweep-store-per-key-try-5763.md deleted file mode 100644 index 615b71b68f..0000000000 --- a/.changeset/sweep-store-per-key-try-5763.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage` -state on a change of session user (objectui#5664 part 3) — now costs one key when a -single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763). - -The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key -`n` aborted the walk, so keys `n+1..end` were never swept, and the failure was -swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt` -believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the -next un-namespaced key — one nobody has written yet — cannot re-open the cross-user -pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys -survived depended on `Object.keys` iteration order rather than on anything bounded. The -previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their -permission-filtered app list, a cross-principal disclosure per objectui#5198) could all -land on the wrong side of the abort. - -`Object.keys(store)` — the reason a guard exists here at all — stays guarded on its -own; only the per-key guard is new, so one uncooperative key now costs exactly that key. - -Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is -NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its -one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a -quarantine are what keep a failed `clear()` from handing the value straight back. -`sweepStore` walks keys it does not own reads for — another package's recents cache, a -metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding -read-back verification for keys this function does not otherwise touch would be a -general storage-error-handling refactor of the module, which this card is scoped away -from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is -named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is -discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in -path, inside an `AuthProvider` effect) still cannot act on the failure and must not -throw either. - -A working `localStorage`/`sessionStorage` behaves exactly as before: every -non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist -(`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected. diff --git a/.changeset/switch-org-declared-reresolve-5750.md b/.changeset/switch-org-declared-reresolve-5750.md deleted file mode 100644 index 0a76461413..0000000000 --- a/.changeset/switch-org-declared-reresolve-5750.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`switchOrganization` now re-resolves identity for the organization it just switched -to by its own explicit decision, instead of depending on an accidental -`TokenStorage` side effect to notice the switch (objectui#5750). - -`AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity -re-resolved across a switch only because `POST /organization/set-active` happens to -return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from -the UNSIGNED `session.token` spelling `getSession()` normally stores — so -`TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription -calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That -signed spelling is deterministic on the raw session token, not on the organization: -two switches with no `get-session` landing in between produce the identical signed -value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity -is left answering for whichever organization the FIRST switch targeted even though -`activeOrganization` already reads as the new one. - -Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage" -link on an org card, plus its own "Back to organizations" button) — ordinary -client-side navigation with no full-page reload and nothing debouncing repeat -switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not -reachable paths for this: both force `window.location.href` immediately after a -successful switch, and the resulting fresh `AuthProvider` mount always performs an -authoritative `loadSession()` regardless of how the race above resolved. - -`switchOrganization` now tracks the organization it last resolved to itself and -re-resolves explicitly whenever a switch's target differs from that, while -suppressing the (now redundant) rotation notification for its own `set-active` -call — so the common single-switch path still spends exactly one `get-session`, not -two. A generation guard discards a still-in-flight, now-superseded switch's answer -rather than let it clobber a later switch's fresher one. diff --git a/.changeset/switch-org-identity-reresolve-5719.md b/.changeset/switch-org-identity-reresolve-5719.md deleted file mode 100644 index 48c7be0598..0000000000 --- a/.changeset/switch-org-identity-reresolve-5719.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` behaviour moves; the only files added are -`packages/auth/src/__tests__/switchOrgIdentityReresolve-5719.test.tsx` and this changeset. - -Pins that console identity **re-resolves across an organization switch**, and pins the -mechanism that actually provides it. - -objectui#5719 observed that `AuthProvider.switchOrganization` never calls `loadSession()`, -while the framework derives `user.positions[]` inside `customSession` from -`session.activeOrganizationId` — so leg 3 of `useWorkspaceAdminStatus` would keep answering -for the PREVIOUS organization, and because that hook is `leg1 || leg2 || leg3` with -`isResolved` short-circuited to true on any positive leg, a stale `org_owner` reads as a -CONFIRMED admin in the new organization. The card was filed as an observation because its -last step could not be settled from this repo. - -Measured against better-auth 1.6.28 driven standalone with the organization and bearer -plugins the framework always enables: `POST /organization/set-active` **does** hand back a -`set-auth-token`, so the objectui#4467 rotation subscription already re-resolves identity -and there is no defect. The mechanism, however, is an accident worth naming: the switch does -not mint a new session — `updateSession` writes only `activeOrganizationId` — it hands back -the **signed** `token.signature` spelling, while `createAuthClient.getSession()` stores the -**unsigned** `session.token`. Two spellings of one session, and `TokenStorage.set` notifies -on a value change. - -Nothing declared that. Normalising either lane onto a single spelling — a reasonable-looking -tidy-up — would silently stop identity re-resolving across an org switch, with the -over-permissive consequence #5719 described and no test anywhere to catch it. These cases -are that test, including the owner-of-both control (so the pin cannot pass on code that -merely stopped reporting admin) and a counterfactual that fixes what the guarantee rests on. - -No explicit `refreshSession()` was added: the rotation path already spends exactly one -`get-session` per switch, so an explicit call would make every org switch cost two. diff --git a/.changeset/table-renderer-declared-column-contract-5350.md b/.changeset/table-renderer-declared-column-contract-5350.md deleted file mode 100644 index a405857ba3..0000000000 --- a/.changeset/table-renderer-declared-column-contract-5350.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': minor ---- - -The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling. - -`renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a -cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared -on `TableColumn`, which declares `header` and `accessorKey` — both required -(`packages/types/src/data-display.ts`). This was the fourth site of the -column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid` -(objectui#5350). - -Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the -family direction — *retire the consumer-side alias; unify the producers* — and it -names this site: the declared `header`/`accessorKey` contract wins. - -What makes this site different from its three siblings is that the alias was not -merely tolerated, it was **published**. `content/docs/api/schema-reference.md` -§TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a -property row reading *"Column definitions with `name`, `label`, …"*, while -`packages/types` declared the opposite pair. Docs and type disagreed about one -type they both call `TableColumn`, each internally consistent. Retiring the alias -without correcting the page would have turned a documented, working example into a -silently broken one, so both halves land together: the page now authors -`accessorKey`/`header`. The same row also advertised a `render` property that -`TableColumn` has never declared — the renderer's hook is `cell` — and that claim -is dropped rather than re-spelled. - -The failure mode of a now-unresolvable column is worth stating, because it is -quiet: the column keeps its slot and its neighbours are unaffected, the header or -the cells simply render empty, and nothing throws. This renderer keys its cells by -index rather than by accessor, so unlike `data-table` it does not even produce -React's generic missing-key warning — a retired-spelling column is fully silent. -Whether that silence should become an authoring diagnostic is objectui#5349's -question; no diagnostic is added here. - -The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so -re-introducing an undeclared alias on this renderer is now a type error rather -than a reviewer's catch. diff --git a/.changeset/tall-tools-shake.md b/.changeset/tall-tools-shake.md deleted file mode 100644 index 7c4cfb5558..0000000000 --- a/.changeset/tall-tools-shake.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: drop eight unused schema imports from `packages/types/src/__tests__/phase2-schemas.test.ts`, so an import-based coverage census no longer reads them as pinned by that suite. No published behaviour changes. diff --git a/.changeset/tenant-header-edge-contract-5279.md b/.changeset/tenant-header-edge-contract-5279.md deleted file mode 100644 index 3ba15b9414..0000000000 --- a/.changeset/tenant-header-edge-contract-5279.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the -unstamped-first-request window in which it is not sent (objectui#5279). Documentation -only — no behaviour changes. - -The header had no written contract anywhere, and the shape of the missing information was -actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a -search confined to this repo and the framework (`objectstack`) returns zero readers and -reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was -held until a cloud-side reading came back non-empty. Without the contract written down, -the next person to grep reaches the same false conclusion and deletes a live routing -input. - -`packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means -(a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim, -not an authorization input, not what scopes rows), who stamps it and under exactly which -condition, who reads it, and what a reader may and may not assume. The framework half is -stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the -API-key principal or `session.activeOrganizationId` and from no header — alongside -`plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability. -The configuration half is quoted from the contract this package can actually resolve, -`TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the -default of a configurable `tenantHeaderName` and `header` ranks second of six -identification sources behind `subdomain`. - -The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is -filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` -> -`getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at -all. What a reader observes is documented as **absent, never present-and-empty**, with the -five situations that open the window and the instruction to fall through to the next -identification source rather than fail closed. The gap is recorded, deliberately not -closed: the cloud readers observe today's behaviour, so changing when the header first -appears is its own decision. - -Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes -about the wire — no active organization means no header at all, the stamp is not gated on -`/api/` the way `Authorization` is, and the active organization overwrites a caller-set -`X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed. diff --git a/.changeset/text-input-description-aria-describedby-5735.md b/.changeset/text-input-description-aria-describedby-5735.md deleted file mode 100644 index af1dc6abe2..0000000000 --- a/.changeset/text-input-description-aria-describedby-5735.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:text_input` now ties its authored `description` to the field with -`aria-describedby`, so assistive tech announces the helper text as the input's -accessible description instead of leaving it as an unassociated paragraph -beside the field (objectui#5735). - -Before this the paragraph and the input were siblings with no programmatic -relationship: a screen reader moving to the field announced the label and the -value and never the helper text. The `label` half of the same block was already -wired (`htmlFor` against the input's `id`), which is what made the gap specific -to `description` rather than a general absence of a11y wiring — and the -identical key authored on a field INSIDE `renderers/form/form.tsx` has been -announced all along, so one authoring key behaved two ways depending on which -container the author reached for. It no longer does. - -The paragraph's id is minted per instance with `React.useId()` — the same source -`FormItem` mints the form renderer's description id from — and deliberately not -derived from `schema.id`. The two associations in this block need ids on -opposite ends: `htmlFor` names the INPUT, whose id only the author can supply, -so that wiring still holds only when they gave the node an `id`; `aria-describedby` -names the PARAGRAPH, which the renderer owns, so the description association -holds unconditionally and cannot collide when two nodes share an authored id. -The attribute is emitted only when a paragraph is actually rendered — an absent -or empty `description` leaves the input with no `aria-describedby` rather than a -dangling reference. - -The key's published `ComponentInput` description, which documented this gap in -so many words, is rewritten in the same change. Its closing advice — prefer -`label` for an instruction a user must not miss — is kept rather than deleted, -on a new basis: a description is announced after the field's name and screen -readers gate description text behind verbosity settings a user can turn down, so -it remains the half of the announcement most likely to go unheard. diff --git a/.changeset/text-input-i18n-label-arms-5717.md b/.changeset/text-input-i18n-label-arms-5717.md deleted file mode 100644 index f8cbd403a3..0000000000 --- a/.changeset/text-input-i18n-label-arms-5717.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:text_input` now DECLARES the inline-translation arm on `label`, -`placeholder` and `description`, so the manifest gate stops reporting -`type-mismatch` on a locale map its own renderer has always resolved correctly -(objectui#5717). - -`@objectstack/spec` types all three keys as the `I18nLabel` union -(`string | Record<string, string>`) — measured on the installed 17.1.0 pin, per -key, from the schema's own verdicts — and `text-input.tsx` has resolved all -three through `pickLocalized` at their read sites since it was written. Only the -`ComponentMeta` entries stayed at a single `'string'` arm. Driven through the -same `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the -save gate use, an author writing `{ en: 'Owner', 'zh-CN': '负责人' }` got three -warnings on a write that renders correctly in the viewer's language: - -``` -<element:text_input> prop "label" expected a string -<element:text_input> prop "placeholder" expected a string -<element:text_input> prop "description" expected a string -``` - -That is the INVERSE of the direction the rest of this family moved in. -`element:record_picker.emptyText` (objectui#5590) and that block's `label` / -`placeholder` (objectui#5637) each held one arm for exactly as long as their -render site dropped the map, and gained the object arm in the change that taught -the render site to resolve it — never declare an arm the renderer drops. Here -the render site was never behind, so the same rule's other half applies: -withholding an arm the renderer resolves is the false declaration in the other -direction, and noise on legal writes trains authors (AI authors included) to -dismiss the `unknown-prop` and `type-mismatch` reports that are real. - -Per-key, not blanket. `defaultValue` on this same block keeps `['string', -'number']` — its contract has no object arm, measured the same way — and the -console specimen file keeps asserting that an `I18N_MAP` there still reports -`type-mismatch`. That control is what makes this a widening of three keys rather -than of a component. - -All three keys also gain a `description` written from what the renderer does -rather than from restating the contract: where each one lands in the rendered -output (a `<label>` above the field, the native `placeholder` attribute inside -it, a `<p>` below it), when each is omitted, and the locale fallback chain the -read site follows. No renderer behaviour changed. diff --git a/.changeset/theme-clientvalidation-dead-entry-5715.md b/.changeset/theme-clientvalidation-dead-entry-5715.md deleted file mode 100644 index 0f5648934d..0000000000 --- a/.changeset/theme-clientvalidation-dead-entry-5715.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Removed the dead `theme:` entry from `clientValidation.ts`'s `LOADERS` table, which -read `ThemeSchema` off `@objectstack/spec/ui` — a symbol the spec retired upstream -(objectstack#10485 / PR objectstack#10695, which deleted the whole `ui/theme.zod.ts` -module). `theme` was never a registered metadata type, so metadata-admin never asked -for it (objectui#5715). - -This is dead-code hygiene, not a bug fix, and it changes nothing an author can -observe. Two measurements say why, and both cut against the more dramatic reading: - -- The entry is not broken *today*. objectui's own `@objectstack/spec` pin (17.1.0) - still publishes `ThemeSchema` as a working strict Zod schema — measured by - importing the `ui` subpath and by ablation, where restoring the entry made - `validateMetadataDraft('theme', ...)` actively reject a draft. The skew is with - objectstack's `main`, not with what objectui installs, which is exactly why no - gate here ever went red. -- It could never have crashed. `getSchemaForType` duck-checks the loader's result - (`typeof schema.safeParse === 'function'`) and wraps the call in `try/catch`, so an - `undefined` export degrades to a silent no-op validator, never a throw. After a - future spec bump past the retirement that is the branch it would have taken. - -The one thing that was genuinely off: `hasClientValidator` answers from key presence -alone, so it returned `true` for `theme` while the validator behind it would have -judged nothing. Per its own docblock that combination makes `ResourceEditPage` treat -live client issues as the error source and suppress the server's `_diagnostics` — a -stored item rendering as clean. Unreachable in practice, but it is the reason the -entry is worth removing rather than leaving inert. - -The line is replaced by an absence note in the shape the file already uses for -`workflow` and `approval`, because re-adding this entry from the spec would -type-check and go green against the current pin. A metadata-admin theme editing -surface remains a separate capability decision. diff --git a/.changeset/theme-schema-doc-rewrite-5648.md b/.changeset/theme-schema-doc-rewrite-5648.md deleted file mode 100644 index 52c05730ce..0000000000 --- a/.changeset/theme-schema-doc-rewrite-5648.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Docs-only: rewrite `content/docs/core/theme-schema.mdx` around the retained theme -system (`Theme` from `@object-ui/types`, `ThemeEngine`, `ThemeProvider`) and delete -the page's three `check-doc-component-types.mjs` exemption entries (objectui#5648). -No published package changes. diff --git a/.changeset/theme-types-localized-5716.md b/.changeset/theme-types-localized-5716.md deleted file mode 100644 index 85b3fede0e..0000000000 --- a/.changeset/theme-types-localized-5716.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/providers': minor ---- - -Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. - -Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): - -- `Typography` — the shape lives on as the inline `Theme['typography']` member. -- `BorderRadius` — lives on as inline `Theme['borderRadius']`. -- `Shadow` — lives on as inline `Theme['shadows']`. -- `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. - -Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). - -The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. - -`@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). diff --git a/.changeset/theme-zod-retired-spec-reexports.md b/.changeset/theme-zod-retired-spec-reexports.md deleted file mode 100644 index 68023be60c..0000000000 --- a/.changeset/theme-zod-retired-spec-reexports.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Remove the six retired `@objectstack/spec/ui` theme-schema re-exports from `@object-ui/types/zod` — `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`, `ThemeDefinitionSchema` (the spec's `ThemeSchema`) — plus their `…SchemaType` inference helpers, and the `theme` / `mode` props of `ThemePreviewSchema` (zod and interface) that consumed them. - -objectstack#10485 (ADR-0049 enforce-or-remove, PR objectstack#10695) retired the spec's whole `ui/theme.zod.ts` module, and the maintainer's ruling on objectstack#10856 (Options A + C) has objectui remove the dangling imports first so the Console Pin Gate can build objectui against the framework tree; restoring the spec exports (Option B) was explicitly not taken. Breaking in effect for anyone importing those six names from `@object-ui/types/zod`: there is no replacement — the validators retired upstream with no successor. The theme TYPE surface (`Theme`, `ThemeMode`, `ColorPalette`, … re-exported from `@object-ui/types`) and the ThemeEngine/ThemeProvider runtime are unchanged. diff --git a/.changeset/tidy-buttons-repeat.md b/.changeset/tidy-buttons-repeat.md deleted file mode 100644 index 30b4700642..0000000000 --- a/.changeset/tidy-buttons-repeat.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio package lookup now renders a producer-marked `error.userMessage` from the failure -envelope, preferring it over the diagnostic `error.message`. - -`fetchFullPackage` — the `/api/v1/packages` read behind "Package info & settings" and the -managed-snapshot refresh — read only `error.message` and `error.code`. In the 5xx band the -platform withholds the producer's prose and substitutes the generic `Internal server error` -into `message`, while the marked `userMessage` rides through untouched, so an author met a -500/503 that carried a sentence written for them and was shown the generic one instead. -Per the envelope contract, presence of `userMessage` IS the producer's marking and a -consumer that sees it renders it verbatim; `error.code` still travels alongside, and an -unmarked refusal is unchanged. diff --git a/.changeset/tidy-donkeys-attack.md b/.changeset/tidy-donkeys-attack.md deleted file mode 100644 index a7314fd9e4..0000000000 --- a/.changeset/tidy-donkeys-attack.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`navigateOnSuccess` now honours a mounted host, and says so when its destination is refused - -`ObjectForm` and `WizardForm` consume `navigateOnSuccess` through -`resolveSuccessNavigate`, and both arms travelled to an accepted destination with a bare -`window.location.assign`. A rooted path such as `/apps/x/o/record/{id}` assigned that way -resolves against the ORIGIN root, so under a host mounted at a sub-path (the framework CLI -configures one for every embedded deployment) an authored in-app destination left the -application. Both arms now route an app-relative destination through the injected -navigation seam both components already held for `submitBehavior.url`, so a mounted host's -basename is applied. With no host seam the behaviour is byte-for-byte what it was — a host -with no router has no basename, so origin-rooted resolution is already correct there. A -same-origin ABSOLUTE destination also keeps browser-level navigation: the seam's declared -input is an application-relative path, and an author who spelled out a whole address asked -for that address. - -A declared `navigateOnSuccess` whose destination is refused — a mistyped value, or a written -record carrying no usable id — used to produce a success toast identical to the one a form -with no `navigateOnSuccess` produces, so the navigation failed with nobody told. That toast -now carries a note that the declared navigation did not happen, and the template the author -wrote is logged for them. The write genuinely succeeded, so this stays a success rather than -becoming an error state. - -Which destinations are ACCEPTED is unchanged: the same-origin guard, the `{id}` / -`{recordId}` dialect and the unescaped interpolation are the subject of an open contract -question and are deliberately untouched here. diff --git a/.changeset/tidy-donkeys-shave.md b/.changeset/tidy-donkeys-shave.md deleted file mode 100644 index 28ff741df2..0000000000 --- a/.changeset/tidy-donkeys-shave.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`ui:breadcrumb` and `ui:command` now resolve a child item's authored `icon` to a glyph instead of drawing nothing. - -`BreadcrumbItem.icon` and `CommandItem.icon` are declared keys of the protocol — present in `@object-ui/types`, mirrored in the Zod schemas, and documented on both components' pages — and neither renderer referenced `icon` at all. The catalog fixture literally named `components-data-display-breadcrumb/with-icons` rendered no icons, and the nine names the two `command` fixtures declare all drew nothing. Both names are now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `action:*`, `ui:dropdown-menu` and `ui:context-menu` resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. - -`breadcrumb` resolves once per item and draws the glyph above the page/link split, so the last crumb (rendered as the current page) carries it as well as the linked crumbs before it. - -Two fixtures declared retired lucide spellings that are absent from the runtime `icons` record and would therefore have drawn nothing: `components-data-display-breadcrumb/with-icons` declared `layout`, now `panels-top-left`, and `components-form-command/command-menu` declared `smile`, now `face-slightly-smiling` — in both cases the live key the retired export resolves to by identity. - -`ui:button-group` is NOT included: its fixtures author `icon` on `buttons[]`, but `ButtonGroupButton` declares no such key in `@object-ui/types` or its Zod mirror, so wiring it would first mean widening the published item contract. Routed for a decision rather than chosen (objectui#5931). diff --git a/.changeset/tidy-forms-carry-spec-keys.md b/.changeset/tidy-forms-carry-spec-keys.md deleted file mode 100644 index 5cb3c5052c..0000000000 --- a/.changeset/tidy-forms-carry-spec-keys.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/react': patch ---- - -fix(react): stop the form-view bridge silently dropping 18 spec keys - -`spec-bridge/bridges/form-view.ts` promised (#2545) that "every serializable -spec key is either mapped onto the `object-form` node or listed here with an -explicit reason for being ignored". Measured against `@objectstack/spec` 17.2.0 -the promise was false for 18 keys, because the conformance test enforcing it ran -its completeness loop over its own hand-written fixture rather than the -contract's key set. - -Seventeen of them now reach the node, at the destinations the receiving layer -already reads: - -- `FormViewSchema.buttons` / `.defaults` — `ObjectFormSchema` declares both and - `ObjectForm` folds them at render (action-button visibility/labels, and - create-mode initial values). -- `FormSection.pane` — explicit split-pane placement; without it a spec-authored - split form fell back to the positional rule, so reordering sections moved them - across the divider. -- `FormSection.visibleOn` — the deprecated spelling now folds onto `visibleWhen`, - matching the contract's own parse-time normalisation and the field path. -- Thirteen `FormFieldSchema` keys — `maxLength`, `minLength`, `min`, `max`, - `precision`, `scale`, `multiple`, `immutable`, `span`, `language`, `keyField`, - `disclosure`, `fields` — so authored constraints, composite config and field - width survive the bridge instead of ending there. - -`publicPicker` is deliberately not carried and now says so: it is a server-side -public-lookup authorization opt-in with no client destination. - -The conformance test's key set is now derived from the contract's own shape at -all three levels, so a spec key that is neither mapped nor explained fails the -suite by construction. diff --git a/.changeset/tidy-forms-refuse-target-less-submit.md b/.changeset/tidy-forms-refuse-target-less-submit.md deleted file mode 100644 index 2a4f20ce58..0000000000 --- a/.changeset/tidy-forms-refuse-target-less-submit.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -Variant forms refuse a submit that has nowhere to go, instead of reporting success - -`TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` each opened -`handleSubmit` with `if (!dataSource) { await schema.onSuccess?.(data); return data; }` -— a success signal emitted without consulting a declared `submitHandler` and without -persisting anything. Through `MasterDetailForm`, whose parent schema declares both -`submitHandler: submitViaBatch` and `onSuccess: handleSaved`, that produced a success -toast and, in create mode, a form reset clearing values nobody wrote. - -All five now answer the question the same way `SimpleObjectForm` and the `object-form` -element gate already do. A form has a submit target when it has a `dataSource` or a -declared `submitHandler`; with neither, the one legitimate shape is inline fields — -a non-empty `customFields`, or `sections` whose fields are all inline runtime -`FormField` objects — whose `onSuccess` is the write. Anything else throws -`DataSource is required for form submission (inline mode not configured)`, which -reaches `schema.onError` and is rethrown. A declared `submitHandler` is consulted -first, so a host that owns the write is never bypassed for want of an adapter it -never needed. diff --git a/.changeset/tidy-jars-confirm.md b/.changeset/tidy-jars-confirm.md deleted file mode 100644 index d0e53b6461..0000000000 --- a/.changeset/tidy-jars-confirm.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Comment-only change in `@object-ui/core`: `ConfirmationHandler`'s declaration now -names the out-of-runner producer of its `options` bag, so a runner-scoped liveness -sweep no longer reads the parameter as declared-but-never-produced. No behaviour, -no type surface and no published output changes. diff --git a/.changeset/tidy-pans-repeat.md b/.changeset/tidy-pans-repeat.md deleted file mode 100644 index 9ccb0c823b..0000000000 --- a/.changeset/tidy-pans-repeat.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Tests only (objectui#7234): pin that an object-declared `list_toolbar` action -reaches the DOM from the served `/meta/object` payload, and that ADR-0066 D4's -`requiredPermissions` capability gate — not the relay — is what hides a -declared-but-ungranted action at every location. No runtime behaviour changes, -so nothing is released. diff --git a/.changeset/tidy-permissions-provider-ctx-identity.md b/.changeset/tidy-permissions-provider-ctx-identity.md deleted file mode 100644 index 217495d986..0000000000 --- a/.changeset/tidy-permissions-provider-ctx-identity.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/permissions': patch ---- - -Both permission providers now build their context value where React cannot discard it - -`PermissionProvider` built its context value in a `useMemo` over four -`useCallback`s, and `MePermissionsProvider` in a `useMemo` over six. Neither -carries a semantic guarantee: React is permitted to discard the cache and -recompute even when the dependency list compares equal, and every one of those -factories builds a fresh object. A discard would therefore hand -`PermCtx.Provider` a NEW context value with every permission it carries -unchanged — which moves the key `usePermissions()` caches on, and re-runs the -consumer chain that names it: `ListView`'s data-fetch effect (an extra -`dataSource.find`), `DetailView`'s gatedSchema, `ObjectForm`, `ModalForm`, -`ObjectGrid` and `RelatedList`. - -⚠️ This is **hardening, not a repair**. Nothing misbehaves today: on this -repo's pinned React 19.2.8 the cache is not discarded spontaneously — 51 -re-renders with no provider, 51 with one and 42 under `StrictMode` each -returned one identity — and there is no `Activity`/Offscreen subtree here, -which is the documented case where React does throw memo caches away. What is -removed is the dependency on React continuing not to exercise a licence it -holds. - -Each cached member and each context value is now keyed on the identities of the -inputs it is derived from, in a module-level `WeakMap` React has no say over — -the same technique that made `usePermissions()`'s own return discard-proof one -link down the chain. The dependency sets are unchanged, so nothing churns more -often than it did, and a genuine permission change still publishes a new -context value to every consumer. Two providers given the same inputs now share -one context value, which is stricter than the per-instance memo it replaces. - -No published export changes, and the context carries exactly what it carried -before. diff --git a/.changeset/tidy-pugs-invent.md b/.changeset/tidy-pugs-invent.md deleted file mode 100644 index c8c1fab44b..0000000000 --- a/.changeset/tidy-pugs-invent.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Pin the shared inbox feed's cadence (objectui#7249): one declared poll interval owned by the feed, however many consumers mount, plus the hidden-tab throttle and the return-to-tab refresh. Tests only — no runtime change, nothing to release. diff --git a/.changeset/tidy-pumas-repeat.md b/.changeset/tidy-pumas-repeat.md deleted file mode 100644 index 77aec4c8ba..0000000000 --- a/.changeset/tidy-pumas-repeat.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Catalog-scoped ratchet on deprecated component types (`div` / `span`) in -`examples/schema-catalog`. Test-only: no published package's source changes, so -this declares "no release" explicitly rather than bumping anything. -`@object-ui/example-*` is in the changeset `ignore` list, and -`scripts/check-changeset-presence.mjs` reports "No source of a released package -changed in this range, so no changeset is owed" — this file is the declaration, -not a version bump. diff --git a/.changeset/timeline-shared-safe-field-label-5623.md b/.changeset/timeline-shared-safe-field-label-5623.md deleted file mode 100644 index 58af0449fb..0000000000 --- a/.changeset/timeline-shared-safe-field-label-5623.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Internal dedup: `ObjectTimeline` now uses the shared `useSafeFieldLabel` from -`@object-ui/react` instead of its own local `useSafeObjectLabel` wrapper / -`OBJECT_LABEL_FALLBACK` (objectui#5623). No exported symbol changed and the -timeline's `fieldOptionLabel` call site behaves identically on both the -bound and unbound i18n paths — no published behaviour changes. diff --git a/.changeset/toc-anchor-parity-cite-by-content.md b/.changeset/toc-anchor-parity-cite-by-content.md deleted file mode 100644 index d275831347..0000000000 --- a/.changeset/toc-anchor-parity-cite-by-content.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only: `toc-anchor-parity` now cites its live instances by heading content instead of by line address (objectui#7913). No published behaviour changes. diff --git a/.changeset/tool-title-i18n-channel.md b/.changeset/tool-title-i18n-channel.md deleted file mode 100644 index 1b2b1d13af..0000000000 --- a/.changeset/tool-title-i18n-channel.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -工具卡片的名字终于有了 i18n 通道(此前中文界面里必然是英文) - -实测(cloud#1658,全中文环境): - -``` -统计一下每个阅读状态各有多少本书 - Describe object 已完成 执行过程 ← 工具名英文 - Visualize data 已完成 执行过程 ← 工具名英文 - 已统计完成,各阅读状态的书本数量如下:… ← 其余全中文 -``` - -卡片上每一处都本地化了——状态、动作、回答——**唯独工具名不能**,因为 -`humanizeToolName` 是个纯英文构词器(`describe_object` → `Describe object`), -名字从未经过翻译,任何语言包都够不着它。而"它现在在做什么"恰恰是用户最需要读懂的一步。 - -现在它接受一个可选的 `translate`(形状即 `useSafeTranslate()`),按 -`chatbot.tool.<tool_name>` 查;查不到就回落到与今天完全一致的英文标题。 - -**这一步只打通通道,不改变任何现有显示**:不传 translate 时行为逐字不变(测试的第一组 -就在钉这一点),语言包也还没有条目。后续两件事各自独立、可分别推进: -把两个调用点接上 `useSafeTranslate()`;以及按需往语言包里补 `chatbot.tool.*`。 -先落通道是因为——在通道存在之前,翻译工作根本无处可放。 - -回落刻意交给英文标题而非原始名:语言包缺条目时显示 `Describe object`(与今天相同), -而不是 `describe_object`(比今天更差)。 diff --git a/.changeset/truthfulness-guard-import-set-7117.md b/.changeset/truthfulness-guard-import-set-7117.md deleted file mode 100644 index 1b55eb01a8..0000000000 --- a/.changeset/truthfulness-guard-import-set-7117.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change (objectui#7117): `exclusion-reason-truthfulness.test.ts` now imports the six `views/*-renderer.tsx` leaves and `@object-ui/plugin-detail`, so the three `PALETTE_EXCLUSIONS` keys that have a renderer outside the old import set (`app:launcher`, `global:notifications`, `record:chatter`) are actually visible to the guard, and a new renderer leaf that is not imported reds the suite instead of shrinking its coverage silently. No published behaviour changes. diff --git a/.changeset/ui-text-variant-align-enforce.md b/.changeset/ui-text-variant-align-enforce.md deleted file mode 100644 index f736debef5..0000000000 --- a/.changeset/ui-text-variant-align-enforce.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`ui:text` now honours `TextSchema.variant` and `TextSchema.align` - -The nine published variants (`h1`-`h6`, `body`, `caption`, `overline`) each map to -a distinct typographic class, and `h1`-`h6` render the heading element they name; -the four published alignments (`left`, `center`, `right`, `justify`) reach the DOM -as their Tailwind class. The registration's `inputs` list declares both keys, so -the JSX-page prop whitelist stops reporting `unknown-prop` for them. - -The published enum is unchanged — the renderer catches up to it, and a spelling -outside the enum is still refused by `safeValidateSchema` rather than quietly -ignored. Behaviour note: a document that already authored `variant: 'h1'` and saw -no heading will now see one. That is the declared behaviour arriving. A node that -never authored `variant` renders exactly as before. diff --git a/.changeset/unbundled-node-boundary-5384.md b/.changeset/unbundled-node-boundary-5384.md deleted file mode 100644 index cefdbb0617..0000000000 --- a/.changeset/unbundled-node-boundary-5384.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-map': patch -'@object-ui/app-shell': patch ---- - -Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. - -`@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module -scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; -`@object-ui/app-shell` reaches the first of those through the static -`@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has -no loader for `.css` at all, so all three resolve and then die during evaluation: - -``` -TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" - for .../react-grid-layout/css/styles.css -``` - -Nothing about how these packages load has changed — every supported host bundles -them (Vite, webpack, or Next with the package in `transpilePackages`), and that is -still the only supported way to consume them. What changed is that the boundary is -now written where a consumer meets it, instead of being learned from a red import. - -objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying -plugin packages — permanently, over the three packages as a group — rather than -moving the stylesheet imports out of module scope. No unbundled-Node consumer -exists, and buying permanent machinery to close a capability gap nobody is pulling -on was the trade the ruling declined. A real consumer request reopens it as a -design question, not as a defect: the READMEs say so and name the issue. diff --git a/.changeset/unified-pending-drafts-5801.md b/.changeset/unified-pending-drafts-5801.md deleted file mode 100644 index 126a5442a5..0000000000 --- a/.changeset/unified-pending-drafts-5801.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -One pending-drafts source of truth (#5801): `usePendingDrafts` / `fetchPendingDrafts` replace the five hand-rolled `GET /api/v1/meta/_drafts` copies behind the home banner, Studio topbar 变更/待发布 buttons, chat pending-drafts bar, chat draft-card resolver, and the preview watermark bar. Every successful publish path (`usePublishAllDrafts`, Studio `doPublish`, the chat bar) now emits the assistant bus's metadata-refresh pulse, and a chat turn that staged or replayed drafts emits it on completion — so a publish or an agent-staged draft anywhere converges every surface at once, and the home banner no longer needs its post-publish `window.location.reload()`. An errored drafts read now uniformly reports UNKNOWN (null), never a fake zero. diff --git a/.changeset/uniqueness-deprecation-jsdoc-4765.md b/.changeset/uniqueness-deprecation-jsdoc-4765.md deleted file mode 100644 index 64fc315d67..0000000000 --- a/.changeset/uniqueness-deprecation-jsdoc-4765.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Corrects the `@deprecated` prescription on `UniquenessValidation` in -`packages/types/src/data-protocol.ts`, which pointed authors at spellings the platform -no longer accepts (objectui#4765). - -Comment-only — no runtime behaviour changes. `patch` rather than an empty frontmatter -because the JSDoc sits on an **exported** declaration and therefore ships to consumers: -measured with the package's own build (`tsc`, and `tsconfig.base.json` deliberately sets -`removeComments: false`), `dist/data-protocol.d.ts` goes 40218 → 41781 bytes and the new -prose is present in the emitted `.d.ts`. What a consumer reads on hover changes, so it -is declared. The emitted `dist/data-protocol.js` is byte-identical (sha256 -`a3de34c5…`, 207 bytes both ways) — that file is a types-only module whose entire JS -output is the license banner plus `export {}`, so a comment on an erased `interface` -reaches the declaration file and nothing else. - -Two of the three spellings it prescribed were wrong, measured against the installed -`@objectstack/spec@17.0.0` (the report was written against `17.0.0-rc.6`): - -- **`indexes[].partial`** was retired in spec 17.0.0 under ADR-0049. It is a tombstone - (`z.never()`) that the parse rejects at any value, so "`partial` for a scoped - constraint" named a key that cannot be declared. A predicated unique constraint is - built at the database layer by a runtime migration issuing - `CREATE UNIQUE INDEX … WHERE`; the prescription now says so. -- **`{ fields, unique: true }`** on `ObjectSchema.indexes` is the deprecated positional - spelling of `unique: 'global'` under ADR-0120 — lint `unique/unscoped-declared-index` - warns in 17.x and protocol 18 rejects it. The prescription now states the scope: - `unique: 'global' | 'organization'`. - -The measurement also refined the report, and the refinement is the reason the rewrite is -not a uniform find-and-replace. The third spelling — **field-level** `unique: true` — is -NOT deprecated. `unique` is scope vocabulary shared by two surfaces on which the same -bare `true` means different things: at index level it stays verbatim (`isGlobalUnique` -and `isOrganizationUnique` both return `false`), which is why it is the positional -spelling of `'global'` and is being retired; at field level it is the positional spelling -of `'organization'` and, in the spec's own words, "stays valid indefinitely … no trap". -Rewriting both occurrences the same way would have replaced one piece of false guidance -with another, so the comment now names the per-surface difference explicitly. - -The interface's own deprecation is untouched and remains correct: `ValidationRuleSchema` -rejects `type: 'unique'` at the discriminator (accepted discriminants are `script`, -`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`), so a rule in -this shape cannot reach the server. - -The replacement closes with what would falsify it — `UniqueScopeSchema` and -`IndexSchema` in `@objectstack/spec` — so the next reader checks the schemas rather than -trusting the paragraph. This is the fourth piece of false guidance found in this -campaign (strictness ledger finding 18), and prose that cannot be checked is how the -first three survived. diff --git a/.changeset/unknown-component-type-case-suggestion-5247.md b/.changeset/unknown-component-type-case-suggestion-5247.md deleted file mode 100644 index 0bfd69d29f..0000000000 --- a/.changeset/unknown-component-type-case-suggestion-5247.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@object-ui/react": patch -"@object-ui/cli": patch ---- - -Name the case-only spelling when a component type misses the registry. - -Registry lookup is exactly case-sensitive, so a node typed `Page` misses a registered `page` and falls through to the OBJUI-001 "Unknown component type" panel. Because the mistake is usually uniform across a document, the symptom is not one broken widget — it is the whole page rendering as error panels, with nothing in the message pointing at the cause. - -Both surfaces that report the miss now name the spelling that would have resolved. `SchemaRenderer`'s panel reads `Unknown component type: Page — did you mean 'page'?`, and `objectui check` reports `Unknown schema type "Page" in <file> — did you mean "page"?`. When no known type differs by case alone, neither says anything extra — `zzz` gains no bogus suggestion, and this is case matching, not an edit distance, so `pge` suggests nothing either. - -**Lookup itself does not change.** `Page` still misses, still fails, and still renders the panel; only the message teaches. Normalising the lookup was considered and rejected (objectui#5247, maintainer ruling 2026-08-19): it would make two spellings valid everywhere, permanently, and legalise the typo class (`PAGE`, `pAge`) along with the PascalCase convention. - -Each surface reads its candidates from the set it can actually trust — the renderer from the live `ComponentRegistry` (including pending lazy stubs), the CLI from the registration-derived `KNOWN_SCHEMA_TYPES` snapshot — so neither can suggest a type nothing registers. diff --git a/.changeset/utilities-index-phantom-provider-5360.md b/.changeset/utilities-index-phantom-provider-5360.md deleted file mode 100644 index 4c7a34666a..0000000000 --- a/.changeset/utilities-index-phantom-provider-5360.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Docs and gate ledger only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. - -`content/docs/utilities/index.md`'s "Data Integration" section taught -`import { ObjectStackProvider } from '@object-ui/data-objectstack'`, a React context -provider on a package that is headless and exports no such thing. Compiled against the -built `dist/index.d.ts` by the same harness `scripts/check-doc-snippet-types.mjs` uses, -the block read -`TS2724: '"@object-ui/data-objectstack"' has no exported member named 'ObjectStackProvider'`, -so a reader who copied it did not get a runtime bug — they got a compile error. #4124 had -already established the finding and PR #4129 fixed it on the sibling page -`content/docs/utilities/data-objectstack.mdx`; the identical phantom survived one file -over, in the utilities index, outside that card's file surface. - -The section now teaches the same shape PR #4129 established: `createObjectStackAdapter` -returning a plain `DataSource`, injected at the renderer boundary through -`@object-ui/react`'s `SchemaRendererProvider`. The block is also self-contained — it -imports every name it uses and types its schema literal as the real `ObjectGridSchema` — -so it compiles exactly as a reader who copies that one block experiences it. - -Because that was the page's only `ts`/`tsx` block and it now produces zero diagnostics, -`content/docs/utilities/index.md` LEAVES `check-doc-snippet-types.mjs`'s `UNGATED_DOCS` -ledger instead of getting a re-measured reason: the page is compiled by the gate from -here on, and no entry on that ledger names a missing export any more. diff --git a/.changeset/vacuous-dts-justification-5673.md b/.changeset/vacuous-dts-justification-5673.md deleted file mode 100644 index 37fe29375b..0000000000 --- a/.changeset/vacuous-dts-justification-5673.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Withdraw the entry-surface justification on core's `SchemaNode` re-export, and put -the gauge that can actually fail in its place (objectui#5673). - -`packages/core/src/types/index.ts` carried, as the justification for the #4580 -re-export convergence, *core's own entry surface is unchanged (`dist/index.d.ts` is -byte-identical across the change — measured, both rounds)*. The reading was real and -it certified nothing. `core/dist/index.d.ts` is emitted from a barrel that only -FORWARDS the symbol, and forwarding never restates a shape — not `export *`, and not -the `export type { … }` line that names this one. Only the module that DECLARES the -symbol can move, so that file is byte-identical under any change to a re-exported -declaration's shape, and it could not have failed for the change class it was quoted -against. - -Measured for this change rather than argued. One optional key was injected into -`BaseSchema` — the shape `SchemaNode` publishes — both packages were rebuilt from a -cleared `dist/` and a cleared `tsconfig.tsbuildinfo`, then the probe was dropped and -both rebuilt again: - -| emitted file | base | with probe | probe dropped | -|---|---|---|---| -| `@object-ui/types` `dist/base.d.ts` — declares the shape | `31b5a01d…` | `8487500e…` **moved** | `31b5a01d…` | -| `@object-ui/core` `dist/types/index.d.ts` — forwards it | `0e64c8c6…` | `0e64c8c6…` | `0e64c8c6…` | -| `@object-ui/core` `dist/index.d.ts` — entry barrel, names it | `5cca207a…` | `5cca207a…` | `5cca207a…` | - -The corrected block states that calibration as a recipe with its failure mode, so the -next reader inherits a gauge that can be checked instead of a sentence that cannot. -The `ComponentRendererProps` block below it already reached the right verdict, but -gave a narrower reason for it — that core's entry is an `export *` barrel — which is -not the mechanism, and is wrong for a symbol the barrel names on its -`export type { … }` line; it now states the forwarding rule. - -Documentation only, in a published declaration file: these docblocks sit on export -specifiers, so `core/dist/types/index.d.ts` carries them into the tarball, while -`core/dist/index.d.ts` does not move for them either — the same insensitivity, -demonstrated once more on this very change. No type moves and no runtime behaviour -changes. diff --git a/.changeset/value-datasource-ast-filter-vocabulary.md b/.changeset/value-datasource-ast-filter-vocabulary.md deleted file mode 100644 index 42954f8092..0000000000 --- a/.changeset/value-datasource-ast-filter-vocabulary.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): `ValueDataSource` applies the filters it is given instead of returning every row - -`matchesASTFilter` recognised only two node shapes — a logical `and` / `or` head -and a three-element comparison — and answered `true` for everything else. Three -consequences, all silent: a legacy flat implicit-AND array (`[[…], […]]`) applied -no filter at all, at top level and as a nested child of `and` / `or` alike; the -null-ness operators had no arm, so `is_null` / `is_not_null` selected every row; -and 16 of the spec's 20 canonical view operators — `equals`, `greater_than`, -`starts_with` among them, the spellings `toFilterNode` lowers a stored view's -rules into — fell through the same way. - -The matcher now canonicalises operators through the spec's own -`canonicalAstOperator` and reads all four shapes `FilterArraySchema` declares, so -an in-memory `provider: 'value'` list applies the same filter the wire would. An -operator or shape it cannot execute now excludes the row and logs once per -`find()`, rather than passing every row with no signal anywhere. diff --git a/.changeset/vi-mock-inherit-guard-6849.md b/.changeset/vi-mock-inherit-guard-6849.md deleted file mode 100644 index 417385b551..0000000000 --- a/.changeset/vi-mock-inherit-guard-6849.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Test/tooling only; nothing published changes. - -Adds `scripts/check-vi-mock-inherit.mjs`, a ratchet that rejects a `vi.mock` factory -which hand-lists the exports of a covered workspace specifier instead of inheriting the -real module's export surface. The only `src/` file it touches is a plugin-view test -file, converted to the inheriting form — no runtime behaviour, no public API, no -published output changes. diff --git a/.changeset/vi-mock-inherit-slice1.md b/.changeset/vi-mock-inherit-slice1.md deleted file mode 100644 index a0667e06ba..0000000000 --- a/.changeset/vi-mock-inherit-slice1.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only: three `vi.mock` factories now inherit the real export surface of `@object-ui/plugin-markdown`, `@object-ui/data-objectstack` and `@object-ui/plugin-report` instead of hand-listing it, and those three specifiers join `check-vi-mock-inherit`'s covered set. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md b/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md deleted file mode 100644 index 6296f60200..0000000000 --- a/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: 11 `vi.mock` factories under `packages/app-shell` now inherit the real -`@object-ui/plugin-chatbot` export surface instead of hand-listing it, and that specifier -joins `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs`. No published behaviour -changes — no product source and no `package.json` was touched. diff --git a/.changeset/vi-mock-inherit-slice11-plugin-designer.md b/.changeset/vi-mock-inherit-slice11-plugin-designer.md deleted file mode 100644 index cce2fcea0d..0000000000 --- a/.changeset/vi-mock-inherit-slice11-plugin-designer.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: the ten `vi.mock('@object-ui/plugin-designer', ...)` factories in the -`AppContent.*` sibling family now inherit the real module's export surface instead of -freezing three of its 29 exports, and the specifier joins the covered set of -`scripts/check-vi-mock-inherit.mjs`. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice12-plugin-list.md b/.changeset/vi-mock-inherit-slice12-plugin-list.md deleted file mode 100644 index d58c3eb263..0000000000 --- a/.changeset/vi-mock-inherit-slice12-plugin-list.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change: the nine `vi.mock('@object-ui/plugin-list', ...)` factories in the -`ObjectView` / `InterfaceListPage` / `ObjectDataPage` family under -`packages/app-shell/src/views` now inherit the real module's export surface instead of -freezing one of its twelve exports, and the specifier joins `COVERED_SPECIFIERS` in -`scripts/check-vi-mock-inherit.mjs`. No published behaviour changes — no product source -and no `package.json` was touched. diff --git a/.changeset/vi-mock-inherit-slice13-fields.md b/.changeset/vi-mock-inherit-slice13-fields.md deleted file mode 100644 index 590c936ec6..0000000000 --- a/.changeset/vi-mock-inherit-slice13-fields.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-only change: eight `vi.mock` factories in `@object-ui/app-shell` now inherit -the real `@object-ui/fields` export surface instead of freezing it, and the -`check-vi-mock-inherit` gate covers that specifier. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice2.md b/.changeset/vi-mock-inherit-slice2.md deleted file mode 100644 index fbe8d6d86b..0000000000 --- a/.changeset/vi-mock-inherit-slice2.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: the four frozen `vi.mock` factories on `@object-ui/plugin-charts` -and `@object-ui/plugin-dashboard` now inherit the real module's export surface, and -those two specifiers join `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs` -(objectui#6892 slice 2). No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice3.md b/.changeset/vi-mock-inherit-slice3.md deleted file mode 100644 index cf7cd96961..0000000000 --- a/.changeset/vi-mock-inherit-slice3.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: the 102 frozen `vi.mock` factories on `@object-ui/auth` now inherit -the real module's export surface, and that specifier joins `COVERED_SPECIFIERS` in -`scripts/check-vi-mock-inherit.mjs` (objectui#6892 slice 3). The barrel was measured -inert at module scope before converting, so nothing new runs at import time. No -published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice4.md b/.changeset/vi-mock-inherit-slice4.md deleted file mode 100644 index ac720d0ec6..0000000000 --- a/.changeset/vi-mock-inherit-slice4.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-infrastructure only: sweep `@object-ui/collaboration`'s 33 frozen `vi.mock` -factories to the inheriting form and add the specifier to the -`check-vi-mock-inherit` gate's covered set. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice5.md b/.changeset/vi-mock-inherit-slice5.md deleted file mode 100644 index b285f11f6d..0000000000 --- a/.changeset/vi-mock-inherit-slice5.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-infrastructure only: sweep `@object-ui/plugin-form`'s frozen `vi.mock` -factories to the inheriting form across `plugin-view`, `app-shell` and -`plugin-designer` — 31 derived on the merge base plus one more that landed on -`main` mid-slice — and add the specifier to the `check-vi-mock-inherit` gate's -covered set. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice6-components.md b/.changeset/vi-mock-inherit-slice6-components.md deleted file mode 100644 index 0b05e15256..0000000000 --- a/.changeset/vi-mock-inherit-slice6-components.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: the `@object-ui/components` `vi.mock` factories in `app-shell`, -`plugin-detail` and `plugin-timeline` now inherit the real module's export -surface instead of freezing a hand-written list, and the specifier joins the -`check-vi-mock-inherit` gate's covered set. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice7-plugin-grid.md b/.changeset/vi-mock-inherit-slice7-plugin-grid.md deleted file mode 100644 index 2551942772..0000000000 --- a/.changeset/vi-mock-inherit-slice7-plugin-grid.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-only change: the 25 `vi.mock` factories that hand-listed `@object-ui/plugin-grid`'s -exports now inherit the real barrel's export surface, and the specifier joins the -`check-vi-mock-inherit` guard's covered set. No published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice8-permissions.md b/.changeset/vi-mock-inherit-slice8-permissions.md deleted file mode 100644 index bccdff0367..0000000000 --- a/.changeset/vi-mock-inherit-slice8-permissions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only change: 24 `vi.mock('@object-ui/permissions', ...)` factories now inherit -the real module's export surface instead of freezing a hand-listed one, and the -specifier joins `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs`. No -published behaviour changes. diff --git a/.changeset/vi-mock-inherit-slice9-plugin-detail.md b/.changeset/vi-mock-inherit-slice9-plugin-detail.md deleted file mode 100644 index b5f8f8856d..0000000000 --- a/.changeset/vi-mock-inherit-slice9-plugin-detail.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: the 13 `vi.mock('@object-ui/plugin-detail', ...)` factories that hand-listed the barrel's exports now inherit the real module and override after it, and `@object-ui/plugin-detail` joins the `check-vi-mock-inherit` gate's covered set. No published behaviour changes. diff --git a/.changeset/view-column-io-canonical-identity-5725.md b/.changeset/view-column-io-canonical-identity-5725.md deleted file mode 100644 index 51eab73b65..0000000000 --- a/.changeset/view-column-io-canonical-identity-5725.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The metadata designer's View **column list** now reads a column's identity — -row label and bound field name — in the ObjectStack canonical spelling only: -`field` and `label`. The legacy TanStack aliases `accessorKey` / `header` are no -longer consulted (objectui#5725). This is the editor-side half of the read -objectui#5344 retired in the column inspector one file over; the two surfaces -render inside the **same panel** and until now gave the author two different -answers about what a column is called. - -Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both -legacy keys by name (`unrecognized_keys`), so a column carrying them has no -field key and no label the spec recognises. Two consequences the list surface -had that the inspector did not: - -- **The label chain was inverted, not merely tolerant.** It read - `label ?? header ?? field ?? accessorKey`, preferring the undeclared `header` - **over** the declared `field`. A perfectly canonical column that also carried - a stray `header` key displayed the alias' value *instead of* its own declared - identity. -- **The field-name read backs more than a label.** It feeds `usedFieldNames()`, - which the Add-field picker consults, so a spec-refused column reserved a field - name and the picker tagged that field "Added" — for a column no accepted - document actually binds. - -**What an author sees:** a stored column shaped `{ accessorKey, header }` is now -named positionally in the list (`col 1`) rather than by its refused spelling, -and the row stays selectable exactly as before — the positional fallback already -existed at the end of that chain. The Add-field picker no longer reports such a -column's field name as taken. Canonical `{ field, label }` columns and -bare-string columns are untouched, and a declared `label` / `field` now outranks -any stray alias beside it. - -**What is deliberately NOT changed:** the write path. These helpers still -reorder and splice the raw `columns` array without normalising it, so no stored -document is rewritten by the act of viewing or editing it — the same fence -objectui#5344 held. diff --git a/.changeset/view-overlay-write-patch-only-5233.md b/.changeset/view-overlay-write-patch-only-5233.md deleted file mode 100644 index 427d2ae8a1..0000000000 --- a/.changeset/view-overlay-write-patch-only-5233.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A view personalization overlay now stores **only the patch** — a column drag no -longer freezes the source view's filter into it (objectui#5233). - -`ObjectView`'s `persistViewPatch` wrote `{ ...baseViewDef, ...patch }`, so an -overlay written by a mere sort/density/column change copied the view's *current -effective* `filter` — along with its `columns`, `label`, `type`, `isDefault` — -into the stored row. The display merge is `{ ...source, ...override }`, so that -copy then outranked the source view forever: an admin edited the view's filter -and every user who had once resized a column silently kept the old filter, with -nothing anywhere reporting it. - -The write now goes through the exported seam `buildPersistedViewBody`. For a -**system view's overlay** the body is the patch plus `viewKind` when the active -tab carries one — identity, not content, and the same line the read side already -draws (`VIEW_OVERLAY_IDENTITY_KEYS`); `object`, `name` and the overlay marker are -stamped by the adapter. A **saved view's own row** is deliberately still written -whole: for it the body *is* the view, there is no source underneath it for a -copied key to shadow, and the write is a whole-document PUT — narrowing it would -delete the user's view rather than narrow it. - -This is the write half of the maintainer's 2026-08-12 ruling (objectstack#7494). -It was blocked until `columnState` was admitted to the view-metadata surface as -an explicitly runtime-only overlay key (objectstack#9933, released in -`@objectstack/spec` 17.1.0) — before that a `columnState`-only patch was refused -`422 INVALID_METADATA`, and the fat copy was the only thing supplying a -recognized key. The read half (`narrowPersonalizationOverlay`) shipped earlier -and stays: rows written before this land are still tolerated on read, and because -the write replaces the whole document, the next toolbar toggle also strips such a -row at rest. No migration. diff --git a/.changeset/view-switcher-chart-gantt-icons-5586.md b/.changeset/view-switcher-chart-gantt-icons-5586.md deleted file mode 100644 index 257d32d34d..0000000000 --- a/.changeset/view-switcher-chart-gantt-icons-5586.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both -icon maps in the package now name only spellings lucide still resolves -(objectui#5586). - -`ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up -in lucide's runtime `icons` record. lucide retires a spelling by dropping it -from that record while KEEPING it as a deprecated named export, so a retired -name still imports, still type-checks and still renders as a component — and -silently resolves to nothing as a string. `ObjectView` composes the switcher -from names, and two of them had been retired on lucide-react 1.31.0: -`chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a -label with no icon at all while every sibling type had one, and nothing went red -because no lucide symbol appears in that map for the compiler to check. Measured -against the installed package: `BarChart3` and `GanttChart` are absent from -`icons`, while `ChartColumn` and `ChartGantt` are present. - -- `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`, - `gantt-chart` → `chart-gantt`. -- `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named - deprecated aliases move to the names the record carries — - `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`, - `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the - aliases already pointed at, so those two glyphs are unchanged; the `gantt` - default picks up the plain gantt glyph, which is what `iconMap` now supplies - for that view type. - -The regression pin widens from `tree` alone to EVERY name both maps supply: a -pin scoped to the two names that broke would not have caught this and would not -catch the next lucide bump. diff --git a/.changeset/viewcolumn-identity-canonical-read-5344.md b/.changeset/viewcolumn-identity-canonical-read-5344.md deleted file mode 100644 index 4a7fcd06eb..0000000000 --- a/.changeset/viewcolumn-identity-canonical-read-5344.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The metadata designer's View column inspector now reads a column's identity — -field key and label — in the ObjectStack canonical spelling only: `field` and -`label`. The legacy TanStack aliases `accessorKey` / `header` are no longer -consulted (objectui#5344). - -Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both -legacy keys by name (`unrecognized_keys`), so a column carrying them has no -field key the spec recognises. The inspector nevertheless read them and -displayed `accessorKey`'s value in the field-key box, presenting a spec-refused -key as though it were a valid column identity — the same consumer-side -tolerance alias `ObjectGrid` retired in objectui#5068, surviving one layer up in -the authoring tool, which is the surface that is supposed to teach the correct -shape. - -**What an author sees:** a stored column shaped `{ accessorKey, header }` now -shows an EMPTY field key and an empty header, and is re-authored rather than -silently carried. Canonical `{ field, label }` columns and bare-string columns -are untouched. - -**What is deliberately NOT changed:** the writeback. `patchIdentity` still -re-emits whichever spelling it was handed, so no stored document is rewritten -by the act of editing it. Normalising on write was ruled out once the -maintainer confirmed there is no population of legacy stored documents to -migrate; such a column stays unsaveable against the spec both before and after -an edit, exactly as it did before this change. What this removes is the -invisibility of that state, not the state itself. diff --git a/.changeset/visiblewhen-record-binding-5454.md b/.changeset/visiblewhen-record-binding-5454.md deleted file mode 100644 index 8cf9b2e4b5..0000000000 --- a/.changeset/visiblewhen-record-binding-5454.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/react': minor ---- - -Bind `record` into the node-level visibility evaluator, and stop a hoisted -`properties.visible` swallowing a declared `visibleWhen`. - -`@objectstack/spec` has declared since ADR-0089 that a page component's -`visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`, -`current_user`, `page.<var>`"*. `SchemaRenderer` bound no `record` at all. Its -evaluator was built from the ambient predicate scope, `data: dataSource` (the -connector **adapter**, not the row) and `page: pageVariables`; the row lives in -`RecordContext`, which that evaluator never read. - -Because the surface is fail-soft, a `record.*` predicate did not misfire — it -resolved to **shown**. Both polarities of the same predicate returned the same -verdict, so a visibility gate silently did not gate, on every block on every -record page. Measured on `record:alert`, `record:path`, `page:card` and -`element:text`. - -Three changes, all in `SchemaRenderer`'s evaluation memo: - -- **`record` is bound**, as the `record` root only — the three roots the - describe promises and nothing more. Not as bare fields, and never over - `data`, which is what `${data.*}` in a props bag resolves against. Bound - conditionally, so "no row" binds nothing rather than shadowing a `record` a - host supplied through the ambient scope. -- **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` - onto the node, so a node carrying `properties.visible` short-circuited the - declared node predicate — the one key the spec tells authors to write was the - one key that could be silently ignored. The two deprecated aliases - (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into - `visibleWhen` at parse, so a spec-parsed page never reaches them. -- **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this - predicate is broken" and "this predicate said yes" with the same word. The - verdict is unchanged on every path — `evaluateCondition` already returned - `true` for every unevaluable predicate, including the non-negated `hidden` / - `hiddenOn` legs where that `true` means HIDE — so only the silence moved. - -**Behaviour change, stated plainly:** a shipped page whose node-level -`record.*` predicate was previously inert now evaluates. A block that was -permanently visible may begin to hide — which is the point, but it is a verdict -change, not a no-op. `properties.visible` is unaffected in verdict: an -in-tree census found **zero** node-level `record.*` predicates on page -components, so nothing in this repository changes verdict. diff --git a/.changeset/vitest-config-unification-3240.md b/.changeset/vitest-config-unification-3240.md deleted file mode 100644 index 6bc68a43d8..0000000000 --- a/.changeset/vitest-config-unification-3240.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Repo tooling only — no published behaviour changes. - -objectui#3240 deletes the 17 per-package `vitest.config.ts` files (plus -`examples/schema-catalog`'s) so the root `vitest.config.mts` is the repo's single -test config, and rewrites every package `test` script to reach it by path filter -(`vitest run --root ../.. <pkgdir>/`). The three files this touches under a -released package's `src/` are COMMENTS in test files, each naming a config or -setup file this change deletes; no runtime or type surface moves. The collected -test population is byte-identical before and after — 2459 (project, file) pairs, -diffed programmatically. diff --git a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md b/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md deleted file mode 100644 index 61e50a177f..0000000000 --- a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Test tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`assertCanonicalVitestInvocation` refuses a package-cwd Vitest run because such a run uses -a different config than CI does, so its green says nothing about CI. Its docstring claimed -that "every per-package `vitest.config.ts` re-exports the root config, and a package -without one resolves upward to it, so no package-level path skips this file". Measured, by -running `pnpm exec vitest run` from every directory that carries a config: 8 package -configs import the root config and were refused, 2 packages carry no config and resolve -upward and were refused, and **11 standalone configs never mention the root file at all** -(`plugin-calendar`, `-charts`, `-detail`, `-form`, `-gantt`, `-grid`, `-kanban`, `-list`, -`-map`, `-timeline`, `-view`) — nothing imported the guard from there, so it never ran. -From `packages/plugin-grid`, one such run printed `Test Files 1 passed (1)` / -`Tests 5 passed (5)` and exited 0, under a config carrying no `@object-ui/*` alias table -at all where the root config maps ~40 specifiers at sibling `src/`. The guard's hole sat -exactly where the divergence — and therefore the false-green risk — was largest. - -Those 11 configs now call the guard themselves, through a new -`repoRootFrom(import.meta.url)` landmark search rather than a hand-counted `../..` (which -resolves to a real directory when the count is wrong, so the guard would keep issuing -verdicts computed against the wrong root). The docstring and the root config's call-site -comment now describe the three routes a config can take instead of asserting one of them, -and the claim is enforced rather than restated: the guard's own test walks every -`vitest.config.*` in the repo and fails on any that takes neither route. - -No package `src/` is touched and the configs' test semantics are unchanged, so no -`@object-ui/*` package changes behaviour and there is nothing here for a consumer to -upgrade to. diff --git a/.changeset/widget-config-panel-locale-map-5301.md b/.changeset/widget-config-panel-locale-map-5301.md deleted file mode 100644 index 511b33d162..0000000000 --- a/.changeset/widget-config-panel-locale-map-5301.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/i18n': minor ---- - -`WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. - -The dashboard widget config panel carried a private `resolveLabel` documented as -resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference -form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline -per-locale map `I18nLabelSchema` actually admits has neither limb, so -`{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy -of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, -`MetricWidget` and `MetricCard`. - -This was not a display bug. The resolved value seeds the panel's editable draft, -so a widget whose stored title was a map opened with an **empty** Title field and -the next save wrote `''` over the author's map — on the ordinary path, not an -exotic one: open the widget, change anything, save. - -Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: - -- **Reading** goes through `pickLocalized(value, language)`, so the panel shows - the active locale like every sibling surface post-objectui#4032. -- **Writing** replaces only the active locale's entry and carries every other - locale across. A title the author never touched round-trips the stored object - itself through an unrelated config edit; an edited one merges into the entry - that was displayed. The live-update callback (`onFieldChange`) forwards the - merged map for the same reason — hosts feed it back into the widget the panel - re-opens from, so a bare string there dropped the map before a save ever ran. - -`@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side -inverse of `pickLocalized`, so the rule is stated once instead of re-derived per -panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, -region-qualified sibling — and deliberately stops there: the `default` / `en` / -first-value limbs are display fallbacks that hand back *another* locale's string, -and writing to one would let an author editing in `fr` overwrite English. With no -entry for the active locale the edit adds one. The pairing -`pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a -write that lands where the read does not look is how a "saved" string disappears. - -A full multi-locale editing UI remains out of scope (objectui#4163). diff --git a/.changeset/widget-input-control-vocabulary-5675.md b/.changeset/widget-input-control-vocabulary-5675.md deleted file mode 100644 index 43d23f920d..0000000000 --- a/.changeset/widget-input-control-vocabulary-5675.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`WidgetInput.type` now uses the shared arm vocabulary `ComponentInputControlType` -instead of restating its eleven literals inline (objectui#5675). - -That vocabulary has had a name since objectui#3832, and objectui#4972 converged the -last structural copy of the surrounding `ComponentInput` interface onto one -declaration. `WidgetInput` was the remaining site spelling the arms out — a third -copy of one list, with nothing tying it to the other two. - -**No value a widget author may write changes.** The inline restatement was measured -member-equal to the shared declaration in both directions before it was replaced -(eleven arms each, same set, and in the same order), so this is a convergence rather -than a widening or a narrowing. `WidgetInput.type` also stays the SINGLE-kind form: -`ComponentInput.type` additionally accepts an array of arms for a union-typed key -(objectui#3832), and importing that capability here would have been a widening, so -it was deliberately left out — the same disposition objectui#4972 recorded when it -left this face alone. - -What the convergence buys is that one of the two drift directions was **silent**. -`WidgetRegistry.load()` in `@object-ui/core` translates each `WidgetInput` into a -`ComponentInput` and passes `type` straight through, so an arm REMOVED from the -shared vocabulary would have broken that assignment loudly at compile time — but an -arm ADDED to it produced no error anywhere. Widget authoring would just have stayed -narrower than component registration, with nothing in the tree saying so. After this -change neither direction is expressible. - -The pin is source-text, deliberately: a TS type alias erases at runtime, so a -member-identical restatement is indistinguishable from the derived type by any -assignability or runtime check. Both kinds of assertion are kept in -`__tests__/widget-input-control-vocabulary.test.ts`, and the ablation showing the -value assertions stay green on the defect while the identity pin turns red is quoted -in the PR body. - -Two divergences between `WidgetInput` and `ComponentInput` are deliberately **not** -repaired here, and are now recorded in `WidgetInput`'s doc block instead of living -only in a closed card's body: the enum slot is spelled `options` on one face and -`enum` on the other (adapted at the `WidgetRegistry` seam, so nothing fails to -arrive), and `ComponentInput` carries five keys — `inputType`, `min`, `max`, `step`, -`placeholder` — that a widget manifest cannot express. Both are surface questions -about published keys and are raised on objectui#5675 rather than answered by this -change. diff --git a/.changeset/wild-charts-explain-themselves.md b/.changeset/wild-charts-explain-themselves.md deleted file mode 100644 index e44172901c..0000000000 --- a/.changeset/wild-charts-explain-themselves.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -`ObjectChart` now renders a self-describing empty state when its query succeeds -and returns no rows, instead of falling through to a bare chart frame. - -The frame was measured in a browser rather than assumed: recharts derives its -ticks from the data, so with an empty result the bar and line families emit two -hairline axis rules and no `text` nodes at all, and pie/donut emit nothing — -there are no labelled axes to tell the reader what would have been plotted. -Beside the component's own red "Failed to load chart data" box, a blank tile -gives the reader nothing to distinguish a young chart from a broken one. - -The copy is the one `plugin-dashboard` already shows on the dataset-bound path -("No data yet" / the load succeeded / the source name), so the same chart over -the same empty result no longer reads two different ways depending on which -widget drew it. Charts with inline authored data are unchanged — they ran no -query to report on. diff --git a/.changeset/wild-donkeys-shake.md b/.changeset/wild-donkeys-shake.md deleted file mode 100644 index e50f7c70c5..0000000000 --- a/.changeset/wild-donkeys-shake.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@object-ui/plugin-map": patch ---- - -`ObjectMap` no longer serializes its data config on every render. - -`getDataConfig(schema)` was called bare in the render body and its result -re-serialized with `JSON.stringify` on every render, purely to give `dataConfig` -the stable identity its fetch effect depends on. `getDataConfig` is a pure -function of `schema`, so `useMemo(() => getDataConfig(schema), [schema])` gives -the same identity with no serialize and no per-render rebuild. - -This also fixes a crash. `JSON.stringify` throws on a value it cannot serialize, -and the config's passthrough branch returns the author's own `schema.data` -object verbatim — inline rows included. A map handed inline records carrying a -back-reference (an `$expand`-ed lookup) or a `BigInt` id threw from the render -body and took the whole map subtree down with it. Comparing identities never -serializes, so the config no longer has to be serializable at all. diff --git a/.changeset/wild-tables-shout.md b/.changeset/wild-tables-shout.md deleted file mode 100644 index bcd0b80d11..0000000000 --- a/.changeset/wild-tables-shout.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: `RelatedList.lookupLabelResolution` waits for the batch lookup-label map with a side-effect-free predicate and renders the cell once afterwards, instead of rendering inside the `waitFor` callback. No published behaviour changes. diff --git a/.changeset/wire-dropped-fields-shape-6889.md b/.changeset/wire-dropped-fields-shape-6889.md deleted file mode 100644 index 51a6de28fd..0000000000 --- a/.changeset/wire-dropped-fields-shape-6889.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -Parse a `droppedFields` wire entry's `fields` elements and its `object` at the -write-warning boundary instead of asserting them. - -The structural gate checked `Array.isArray(fields) && fields.length > 0` and then -asserted the entry into a type declaring `fields: string[]` and a required -`object: string` — reading neither. A response carrying `fields: [42]` reached -`onWriteWarning` subscribers typed as a field name (the shell rendered it as the -label `42`), and an entry that omitted `object` arrived claiming a string that was -not there. - -Now the wire type declares only what the gate establishes, and the notice is -parsed: non-string `fields` elements are refused, an entry naming no field at all -is dropped as `fields: []` already was, and a missing or non-string `object` is -healed from the object the write targeted. Warnings are never silenced for a -field the server really did name. No published type changes — `WriteWarningEvent` -and `DroppedFieldsNotice` keep their shapes, and a subscriber's `fields: string[]` -is now true rather than asserted. diff --git a/.changeset/wise-pandas-pin.md b/.changeset/wise-pandas-pin.md deleted file mode 100644 index 0a9ba4fdbc..0000000000 --- a/.changeset/wise-pandas-pin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Test-only: pin that `record:alert`'s CTA honours an action's `resultDialog` through the -surrounding provider's shared `ActionRunner`, and that with no provider the CTA falls back -to a local, unwired runner that discards the value and warns. No published behaviour changes. diff --git a/.changeset/zod-array-element-reader-5872.md b/.changeset/zod-array-element-reader-5872.md deleted file mode 100644 index 0fdacdbfcd..0000000000 --- a/.changeset/zod-array-element-reader-5872.md +++ /dev/null @@ -1,10 +0,0 @@ ---- ---- - -Internal test-support only: `@object-ui/test-support` gains `arrayElementSchema` -(objectui#5872 class (2)), and the three disagreeing hand-written array-element -walks in `plugin-detail`, `app-shell` (previews) and `app-shell` -(clientValidation) converge onto it, along with one `.unwrap().options` read in -`data-objectstack`. No published runtime source changes: the diff is test files -plus a `private: true` workspace package that ships in no bundle, so this -declaration carries an EMPTY frontmatter — nothing to version. diff --git a/.changeset/zod-base-schema-mirror-parity-4605.md b/.changeset/zod-base-schema-mirror-parity-4605.md deleted file mode 100644 index 1d0ba8c9fe..0000000000 --- a/.changeset/zod-base-schema-mirror-parity-4605.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The zod `BaseSchema` mirror now accepts everything its TypeScript declaration -declares — five keys had drifted narrower (objectui#4605). - -`@object-ui/types/zod` is a published runtime validator hand-written to mirror the -`BaseSchema` interface. As the interface widened, the mirror did not, so five keys -refused at parse time a spelling the published types invite and the renderer -implements — "declared = enforced" inverted. `.passthrough()` rescued none of them: -passthrough admits UNDECLARED keys, and all five are explicitly declared, so the -narrow declaration won. - -Measured against the unmodified mirror before the change, these were the refusals: - -| key | authored input | old mirror said | -|---|---|---| -| `visible` | `'${data.status === "open"}'` | `expected boolean, received string` | -| `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` | -| `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` | -| `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` | -| `description` | `{ en: 'The record owner' }` | `expected string, received object` | - -`visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts, -no wider. `ariaLabel` takes the KEYED reference through a new exported -`KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema` -BY REFERENCE, so a change to the spec's label contract is picked up rather than -re-typed. Every spelling that parsed before still parses. - -The two i18n vocabularies are kept apart rather than merged into "some object". -`label`/`description` are the spec's INLINE locale map (resolved by -`resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by -`resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render -an EMPTY aria-label). Widening both slots to accept either shape would have -reproduced objectui#4167's confusability hazard inside the validator that exists to -catch it, so each slot admits only its own vocabulary and both cross pairings are -pinned as rejections. - -The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own -`.shape` and compares each key against the declaration, so the next widening of -`base.ts` that forgets this file turns it red with no list to maintain. It reads -`.shape` and not `keyof z.input<…>` because that spelling was measured vacuous — -`.passthrough()` collapses the inferred key union to bare `string`, and a pin written -over it resolved `never` while five keys were demonstrably narrow. Two guards pin the -derivation against both degenerations (`never` and `string`). diff --git a/.changeset/zod-internals-reader-confinement.md b/.changeset/zod-internals-reader-confinement.md deleted file mode 100644 index 4fc9998c3e..0000000000 --- a/.changeset/zod-internals-reader-confinement.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change. Four spec-parity suites each carried a byte-for-byte identical -hand-written walk into Zod's internals to read a key's enum vocabulary; they now -import one `shapeEnumOptions` reader from the private, never-published -`@object-ui/test-support`. No published package's runtime behaviour changes — -the only edits to released packages are a `devDependencies` entry each. diff --git a/.changeset/zod-mirror-parity-population-5684.md b/.changeset/zod-mirror-parity-population-5684.md deleted file mode 100644 index e1b81105b1..0000000000 --- a/.changeset/zod-mirror-parity-population-5684.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Test-infrastructure only, no shipped code touched: the derived anti-drift parity -construction that objectui#5680 built for `BaseSchema` now covers every hand-written -zod mirror in `@object-ui/types`'s `src/zod/` directory instead of that one schema. - -A mirror restates a TypeScript declaration by hand, and when the declaration widens -and the mirror does not follow, the published validator refuses a spelling the -published types invite — `declared !== enforced` on a shipped surface. Two instances -were found independently before anything looked for them (objectui#4605, #5186); -nothing detected the class itself. - -`packages/types/src/__tests__/zod-mirror-parity.test.ts` registers 164 mirror/declaration -pairs and applies one construction to all of them: it reads each mirror's own `.shape` -and compares every key against the declaration, so a widening that forgets a mirror -turns red with no key list to maintain. A runtime census reads the directory off disk -and fails when an exported const is in neither the registry nor the reasoned exclusion -list, so a mirror added later cannot join the population silently. 18 pairs carry -measured drift today and are pinned to their exact drifted key sets as a ratchet — new -drift fails, and so does a stale entry once the drift is fixed. - -`base-schema-zod-mirror-parity.test.ts` keeps its objectui#4605 runtime pins and no -longer restates the type-level construction; `BaseSchema` is one registered row. diff --git a/.changeset/zod-mirror-parity-population-derived.md b/.changeset/zod-mirror-parity-population-derived.md deleted file mode 100644 index 5dea8c3b6e..0000000000 --- a/.changeset/zod-mirror-parity-population-derived.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: `packages/types/src/__tests__/zod-mirror-parity.test.ts` now derives its -pair-population figure and both of the differences its header quotes, instead of restating -them as prose literals that nothing checked. `@object-ui/types` publishes nothing from -`__tests__/` — the package `tsconfig.json` excludes the directory by name, and the build -program lists zero files under it — so no published behaviour changes. diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md index 1454601a3a..8a235b2040 100644 --- a/QUICK_REFERENCE.md +++ b/QUICK_REFERENCE.md @@ -124,7 +124,7 @@ release path runs the sync itself: `changeset:version` bumps every manifest and this block in the same commit, so a release can no longer leave the block a version behind (objectui#5394 — that had happened once per release, three times). -- **Version:** 17.6.0 (the version every `@object-ui/*` manifest carries — they are one +- **Version:** 17.7.0 (the version every `@object-ui/*` manifest carries — they are one `fixed` group in `.changeset/config.json`, so a release moves all of them together) - **Spec:** `@objectstack/spec` ^17.0.0 (declared by the root `package.json` and by `apps/console/package.json`) diff --git a/apps/console/CHANGELOG.md b/apps/console/CHANGELOG.md index 8ef1818ba4..77ddc413ff 100644 --- a/apps/console/CHANGELOG.md +++ b/apps/console/CHANGELOG.md @@ -1,5 +1,1248 @@ # @object-ui/console +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- 26fbe09: Console telemetry can now be hard-disabled on an already-built artifact + + `/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting` + (objectstack#11382), and the Console now reads it. The Sentry decision becomes a + conjunction of two independent grants — a DSN injected at **build** time AND a + positive permission from the **runtime** — so the single pre-built SPA that both + the hosted SaaS console and the on-premises / air-gapped EE images embed can be + silenced by the deployment it lands in, with no rebuild and without editing files + inside a published bundle. That was the half objectui#5522 could not close before: + every other input to the gate is a Vite build-time variable frozen into the bundle + as a literal, which is how an air-gapped EE Console came to send 14 Sentry + envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for + the customer to turn it off (objectstack-ai/cloud#1508). + + The permission fails **closed** in every direction: absent key, `telemetry` block + absent, malformed payload, failed fetch, or a runtime predating the key all read as + *do not send* — which is precisely the set of runtimes leaking today. It is a + permission and never a source: the server supplies no DSN and cannot turn telemetry + on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1` + and other truthy lookalikes do not. + + Behaviour change for deployments that already inject a DSN: reporting now also + requires the runtime to grant permission, via + `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s + `allowClientErrorReporting`). A build that opted in but whose runtime says nothing + will go quiet — deliberately, since that is the same artifact an air-gapped + customer runs. + + `@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and + the `RuntimeTelemetry` type, so consumers read the permission through the one + fail-closed accessor instead of writing their own optional-chain against the + payload. +- 5173a5e: ⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on + the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read + this before upgrading if any of your form metadata gates on the session user. + + objectui#6010 bound the host predicate scope on the five authored-predicate call + sites in the components form renderer, so `current_user` (plus the ADR-0068 + `user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn` + there. Two other authored-predicate evaluators were still passing `undefined` + for that argument, so the same authored text meant two different things + depending on which surface opened the form (objectui#6110): + + - **`apps/console`'s form renderer**, on the authed internal route + `/forms/:name`. The internal route is a runtime record surface by ADR-0089 + D1's own words (*"runtime record surfaces bind `record` + `current_user`"*), + and its `visibleWhen` metadata is the same `*.view.ts` FormView the + object-view chain renders — so a role gate authored once behaved differently + depending on which route opened the form. + - **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`), + the gate that re-checks the whole declared field set at final submit because + `allowSkip` can jump past a step. Its docstring promises *"the same verdict + from all three rather than a second, divergent dialect"*, and since #6010 it + was the divergent one. + + **Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails + OPEN: a field on screen is what you get when the predicate resolves TRUE, when + the scope was never bound so the predicate faulted, *and* when the predicate is + broken. Those worlds were indistinguishable, so an app that authored a + `current_user` gate saw the field render and had no way to tell the rule was + inert. After this change the predicate is evaluated for real, and fields and + sections that have always been visible will disappear for the users the rule + excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user` + requiredWhen that has been silently not applying will now start holding submits. + + In the wizard the change is a fix in the user's favour as well: a required field + the wizard HID from this user was still counted as visible by the submit gate, + so the submit was refused on a control the submitter could neither see nor fill + in. + + **Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in + your form-view and object metadata that names `current_user`, and confirm each + predicate says what you actually want evaluated against `record` + + `current_user`. + + **The public anonymous form `/f/:slug` is deliberately unchanged.** It is + mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is + no authenticated principal, and no provider is mounted above it — so its scope + is empty and a `current_user` predicate authored on a public form still faults + and still fails open, exactly as before. Nothing new is declared to say so: the + two routes are told apart by which component mounts them. + + `@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider` + user normalisation, so every console surface that mounts the provider publishes + the same `current_user` shape rather than re-deriving it. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. +- 14ef9f5: Console error reporting is now configured entirely by the **server**: the DSN and every + knob that travels with it arrive on `GET /api/v1/runtime/config`, and the build-time + `VITE_SENTRY_DSN` path is retired (objectui#5522, consumer half of objectstack#12681). + + ObjectStack's users consume a **prebuilt** Console. Under the previous two-key gate — + a build-time DSN **and** a runtime permission — a build-time key was unreachable for + them, so a self-hosting operator could not enable client error reporting at all: the + permission was reachable and the source was not. The maintainer named it on 2026-08-27: + + > 「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」 + + ``` + send ⇔ the runtime served a DSN + ``` + + **The DSN's presence IS the grant.** There is no companion permission flag, and the one + that briefly existed is removed rather than paralleled. Two knobs in two places produced + two silent dead states — "permission on, no DSN" and "DSN in, permission off" — that look + identical from the browser; one knob cannot disagree with itself. Turning reporting off is + unsetting the server DSN, and there is deliberately no build-time force-off left, because + nobody consuming a prebuilt console could reach one. + + The fail-closed posture is unchanged and structurally stronger. Absence of a *source* is + not a value that can be misread, so a runtime predating the key, a third-party host, a + 404, a network failure, a malformed body and a config that has not arrived yet all read as + off — where the boolean needed a strict `=== true` plus a written argument about why a + negative `disabled` flag would have been vacuous on exactly the runtimes that were leaking. + + ### What moved, and the one thing that did not + + `sendDefaultPii`, `environment`, `tracesSampleRate` and the error-session replay rate move + into the runtime payload. They were build-time variables, so a prebuilt-console consumer + could set none of them — including the one deciding whether IP and User-Agent leave their + network. This is not new surface; it is the same surface moved to the side that can + operate it. + + `VITE_SENTRY_RELEASE` **stays build-time**, and is now the only `VITE_SENTRY_*` variable + that exists. A release identifies which bundle produced a stack trace and must match the + source maps that bundle's pipeline uploaded — a property of the build, which no server can + know. `VITE_SENTRY_ENABLED`, `VITE_SENTRY_ENVIRONMENT`, `VITE_SENTRY_TRACES_SAMPLE_RATE` + and `VITE_SENTRY_REPLAY` are retired along with `VITE_SENTRY_DSN`. + + ### Breaking + + | FROM | TO | + |:--|:--| + | `VITE_SENTRY_DSN=…` in the Console build environment | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=…` on the ObjectStack runtime | + | `VITE_SENTRY_SEND_DEFAULT_PII=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true` | + | `VITE_SENTRY_ENVIRONMENT=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT=…` | + | `VITE_SENTRY_TRACES_SAMPLE_RATE=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE=…` | + | `VITE_SENTRY_REPLAY=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1` | + | `VITE_SENTRY_ENABLED=false` | unset the runtime DSN | + | `isClientErrorReportingAllowed(): boolean` | `getClientErrorReporting(): RuntimeClientErrorReporting \| null` | + | `resolveSentryGate(env, runtimeAllows)` | `resolveSentryGate(runtimeErrorReporting)` | + | `RuntimeTelemetry.allowClientErrorReporting: boolean` | `RuntimeTelemetry.errorReporting?: RuntimeClientErrorReporting` | + + One-line fix for a deployment: move your `VITE_SENTRY_*` values onto the ObjectStack + server as the `OS_TELEMETRY_CLIENT_ERROR_REPORTING_*` variables above, and drop them from + the Console build environment. One-line fix for a consumer of `@object-ui/app-shell`: + `const sink = getClientErrorReporting(); if (sink) …` in place of + `if (buildTimeDsn && isClientErrorReportingAllowed())` — the build-time conjunct is gone, + because the server now supplies the source. + + **Landing order is safe in both directions.** A Console built before this change meets a + new server, reads an absent `allowClientErrorReporting` and stays off; a Console built + after it meets an old server, reads an absent DSN and stays off. Neither half can turn + reporting on by itself, so the two repos' PRs can land in any order. + + The `committed-telemetry-endpoint.test.ts` ratchet is unchanged in rules and unchanged in + job: nothing endpoint-shaped may be committed to this repo. Its rules key on the + variable's suffix and on the value rather than on the `VITE_` prefix, so they already + cover the runtime-side spelling — now pinned, so a later tidy-up cannot narrow them to the + retired names and reopen the hole under a new one. + +### Patch Changes + +- ff8587f: Setup's settings selects now follow the specifier's `valueDomain` declaration instead of + treating the curated `options` table as the domain (objectui#3719). + + Since objectstack#5712 / PR objectstack#6581 a settings specifier may declare + `valueDomain` (`iana_time_zone` | `iso_4217_currency` | `iso_3166_alpha2`), and when it + does the **standard's membership is the enforcement boundary** — the server accepts + `timezone: 'Europe/Zurich'` and `currency: 'CHF'`, neither of which is in the manifest's + list. The console kept drawing those keys as closed dropdowns, so an admin could author + only the 17 curated zones and 9 curated currencies while the contract took the whole + domain; every other legal value was reachable by API or `OS_LOCALIZATION_*` env only. The + keys' own descriptions had promised "IANA zone" / "ISO 4217 code" all along. + + `case 'select'` in `SettingsField` now keys the control off the declaration. Declared → + an editable combobox: the curated options stay on as suggestions (native `<datalist>`, the + same suggest-but-allow-anything affordance `FlowReferenceField` uses — no new dependency), + free text is committed verbatim, and an out-of-domain value is refused by the server with + `invalid_value` + `constraint: { valueDomain }` into the field-error slot that already + exists. + + **Undeclared → the closed dropdown is untouched**, which is half the change rather than a + caveat. Those `options` are still exhaustive under objectstack#5131 (the sms/mail provider + selects), and `localization.locale` had its domain declaration deliberately **rejected** in + objectstack#6515 because its options *are* the shipped catalogs. Widening those to free + input would be a regression wearing this fix's clothes, so the two branches are pinned + against each other from the specifier data rather than from a list of key names — a key + that gains a domain server-side joins the right side of the pin with no edit here. + + Root cause, because it will recur: `Specifier` in `pages/settings/types.ts` is a + hand-written **local mirror** of the server's shape, not an import, so nothing tells it when + the schema grows — and TypeScript reports nothing, because a narrower mirror is a + structurally valid reading of a wider object. `valueDomain` is added there and the file + header now says to check the mirror first when a settings feature "doesn't render". +- baac3f4: Fix four `find()` calls that passed a query option without its `$`, and gate the shape. + + `QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies + exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw, + no warning, and it type-checks because the type carries `[key: string]: any` for + adapter-specific params. For a dropped cap the result is an **unbounded** read rather than + a truncated one: the platform's GET list route has no default page size, so the query + returns the whole match set and stays invisible until the object is large. + + - `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one + **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so + the dropped key turned "count only, fetch nothing" into "fetch every row in the object", + on every mount and every refresh of every list view. It now sends `$top: 0` and reads + the count off `total` only; the row-counting fallbacks are gone rather than repointed, + because once zero rows are requested an empty `data` means "you asked for none", not + "the object is empty", and counting it would assert a confident `0`. With no total the + footer line is omitted instead. + - `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`, + one line from three correct `$top` calls. + - `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so + a filter's option list read every row and every column of its source object while its + own comment described it as capped at 200. The same call read `records.items`, which is + not a `QueryResult` member, so against a real adapter the fallback produced no options + at all. + - `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the + result in its page-source metadata. + + A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time: + a known query-option name missing its `$` in the second argument of a `find`/`findOne` + call. It is narrow on purpose — a closed list of spellings, anchored to the call — because + the index signature exists so adapters can take adapter-specific params, and a rule that + flagged any unprefixed key would report the shape the type was written to allow. Its + sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged. +- 7a72422: Publish the create-payload rule from `@object-ui/plugin-form`'s entry, so a + second form renderer can call it instead of composing it by hand + (objectui#6059). + + Newly importable from `@object-ui/plugin-form` — two functions, nothing else: + + ```typescript + import { omitServerResolvedDefaults, isRequiredInForm } from '@object-ui/plugin-form'; + ``` + + - `omitServerResolvedDefaults(values, objectSchema)` — drop the keys a CREATE + payload must leave to the producer: a field whose declared `defaultValue` is a + runtime instruction (`NOW()` / `current_user`, or a CEL envelope) and whose + submitted value is empty. `ObjectQL.applyFieldDefaults` resolves a declaration + only for a field that arrives absent or null, so submitting a blank stores + `''` and silently defeats it. **Create-only** — the caller keeps the mode gate. + - `isRequiredInForm(field, isCreateForm)` — the `required` a form should + enforce, given the mode. Published as the pair's other half on purpose: + excusing a server-owned field from `required` and then submitting the key + anyway is not half a fix, it is no fix. + + Both are pure functions over plain data (no React, no registry). The rest of + `schemaDefaults.ts` — `seedCreateValues`, `schemaDefaultValues`, + `isSeedableDefault`, `isCreateFormMode`, `SeedContext` — stays module-private, + and `isRuntimeDefault` stays `@object-ui/core`'s to publish. + + No behaviour change. The console's `FormPage` now calls the published helper + instead of composing `isRuntimeDefault` + `isMissingForRequired` locally; its + create payload is decided identically before and after, pinned against the + deleted implementation over the full matrix of default shapes, value spellings + and both modes. +- 6630b37: The console boot no longer flashes a fully-white frame after the splash has painted + (objectui#6378). + + Cause, established by measurement before any fix was written — the card named + `LoadingScreen`'s unmount timing and `RouteFader` as suspects and both are exonerated. + A CDP `Page.startScreencast` frame ledger (every frame classified with the card's own + rule: white when no colour channel falls below 242) was correlated against a DOM-state + ledger on the same clock (`performance.timeOrigin`), against the production + `apps/console` bundle with the boot endpoints mocked. `RouteFader` never mounts on the + boot path at all, and `LoadingScreen` unmounts exactly when its own gate says to. What + is wrong is what REPLACES it: every readiness gate renders the splash while it waits and + a bare `<Navigate>` the moment it decides. `<Navigate>` renders `null` and react-router + runs the navigation as a transition, so the destination tree renders while the commit + that already dropped the splash is what the compositor is showing — 41–147 ms during + which `#root` holds no view and the viewport is the bare page background. The flash is + intermittent only because it depends on a frame being swapped inside that window; the + window itself was present on every measured boot. + + `RedirectWithSplash` (new, `@object-ui/app-shell`) pairs the same `<Navigate>` with the + same `LoadingScreen` the gate one line above was already rendering, so the handoff + changes no pixels and the transition runs underneath an unchanged screen. The console's + three boot redirects use it: the auth gate's `/login` bounce, the `/` landing resolver, + and the catch-all route. The nested organization `index` redirect deliberately does not — + it fires under an already-painted layout, where covering the screen would be the + regression. + + Acceptance campaign — same instrument on both sides, the two arms INTERLEAVED inside one + process and one browser so drift in this shared container's load lands on both equally. + 102 paired boots per arm across five cells (signed-out `/`, signed-in `/`, an unmatched + entry, and the card's two throttled network profiles). The empty-viewport window: 102/102 + pre-fix, 0/102 post-fix. The white frame itself, pooled over the three cells where the + pre-fix build actually flashed: 67/87 (77%) pre-fix, 0/87 post-fix — 95% upper bound on + the residual rate 3.4%, against a card-reported defect rate of ~1/3. The two throttled + cells are reported but NOT pooled: the pre-fix build flashed 0 times there, so before and + after agree and those cells prove nothing about the pixels (they still separate 15/15 vs + 0/15 on the DOM window). + + `e2e/console-boot-indicator.spec.ts` gains the deterministic half as a gate — after + React's first commit the viewport centre must never stop being covered. That reading is + what makes an intermittent defect gateable: the flash needs a frame to be swapped inside + the window, but the window itself was present on every measured boot. Verified red-first, + 6/6 red on the pre-fix bundle and 6/6 green on this one. +- 1db5b76: Console: free the marketplace routes `AppContent` declares lazy, and pin the metadata-admin surface that cannot be freed + + `AppContent` declares four surfaces with `lazy()` that the emitted bundle loaded on + every console page load anyway. Measured on `b98352a15` from + `apps/console/dist/eager-closure.json` and the emitted chunks' own module lists — + not from a source-level search, which cannot see the mechanism at all: + + | chunk | gz, eager before | after | + |---|---|---| + | `metadata-admin` | 172,651 B | unchanged — pinned, with the module list that proves it | + | `MarketplacePackagePage` | 7,647 B | lazy | + | `MarketplaceInstalledPage` | 1,836 B | lazy | + | `MarketplacePage` | already lazy | lazy | + + Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: + rolldown had parked `components/SuggestedBindingsPanel.tsx` (statically imported by the + eager `views/studio-design/StudioDesignSurface.tsx`) in the first, and + `console/marketplace/InstalledListWidget.tsx` (bare-imported by the package barrel for + its SDUI registration) in the second. An `app-shell-eager-leaves` group in + `apps/console/vite.config.ts` isolates those co-tenants so the three declared-lazy pages + chunk by their own dynamic-only reachability. The console eager closure moves + 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three + per-chunk ceilings unmoved. + + `metadata-admin` is NOT freed and is now pinned in + `DECLARED_LAZY_VIEWS_STILL_EAGER` with the reason: it is statically imported by the + package barrel and by `services/builtinComponents.tsx`, which registers + `MetadataDirectoryPage` and `MetadataResourceRouter` by value, and it performs five + load-bearing top-level registrations. Freeing it would change what + `registerAppComponent` accepts and what the barrel re-exports — a published-contract + decision, not a bundling one. +- 4704aa4: A form no longer ends on a screen asserting both a failure and a success (objectui#7252). + + A refused submit raised an error toast that nothing ever retired, so when the user + fixed the input and submitted again the confirmation of that second attempt appeared + *beside* the refusal of the first — a wizard's last step showing "Invalid project + status transition." and "Your new project is ready…" at the same time. + + Every outcome toast a form raises now travels under one stable per-form id, so the + later outcome supersedes the earlier one instead of stacking beside it: + + - `@object-ui/components`' form renderer publishes its three outcome toasts (the + field-level rejection, an `onAction` error, and a rejected write) under that id, and + retires the previous attempt's toast in the same place it already cleared the + previous attempt's in-form banner. This is what fixes the reported wizard flow: the + refusal comes from this renderer while the success toast is raised by the host + (`WizardForm` / `ObjectForm`), so no single raiser could supersede the other before. + - the console's own `FormPage` publishes its confirmation and its submit failure under + one id, for the same reason. + + Toast durations are unchanged — this is about supersession, not lifetime. The + objectui#4190 arm is deliberately excluded: a refused redirect *destination* still gets + its own toast, because the write succeeded and that refusal has to stay readable beside + the confirmation it qualifies. +- d4c6a86: The console chrome's Home affordances follow the product's declared landing (objectui#7256). + + `/` has honored `app.isDefault` since the hardcoded `PREFERRED_APPS = ['cloud_control']` + redirect was retired; the chrome had not. The top-bar logo, the sidebar's Home row, the + mobile sheet's Home row and the app-switcher's Home entry each named `/home` literally — + the ENVIRONMENT layer's launcher (ADR-0075). So a deployment that declares a landing + offered the customer two homes in two voices, and one click on the logo left the declared + one. + + On cloud's control plane that second home is actively wrong: its "Build an app" / "Start + from a template" cards are environment-side actions that cannot work from the control + plane, and its "Your apps" tiles are the control plane's own internal management apps. + + - `@object-ui/app-shell` adds `resolveDeclaredHomePath()` — the one reader of the + declaration — plus `useHomePath()`, which the four chrome sites now consume. The signal + is the App metadata the server already sends: no hostname sniff, and no product name + baked into the shared bundle. + - `@object-ui/console`'s `/` resolver is unchanged, and a behavioural matrix now pins its + answer equal to the chrome's for every declared app list, so the post-login landing and + the logo cannot drift apart. + + Deployments that declare no landing are unaffected: every Home affordance still resolves + to `/home`. The "this app is gone" recovery redirects in `AppContent` / `ConsoleShell` are + deliberately untouched — they are error paths, not Home affordances. +- ef1ffc2: `MetadataHmrReloader` stops flooding a production-posture deployment with + `GET /api/v1/dev/metadata-events` 404s (objectui#7257). + + The dev-only HMR component subscribes via `EventSource`, gated on + `import.meta.env.DEV`. That gate is not airtight against every rig: a + "prod-like" build/serve setup that forces `NODE_ENV=development` for the + *build tooling* while running the server itself in production posture can + bake `DEV === true` into the shipped bundle even though the server never + mounts the dev route there — and the old reconnect loop treated every closed + connection as transient, retrying on a fixed `reconnectDelayMs` (2s) + forever. On an env host that is ~30 requests/minute of 404s per open + record/list page, drowning out the legitimate `sys_inbox_message` / + `sys_notification_receipt` polling in the same console. + + The first `connect()` attempt now doubles as the real capability probe: if + the stream closes before it ever reaches `open`, the component gives up for + good instead of retrying (and specifically not a longer interval either — + that would still spam 404s, just slower). A stream that DID open at least + once and later drops — a real dev-server restart or network blip — keeps + reconnecting exactly as before. + + No production-side replacement is introduced here: this component's only + job is turning "a metadata file changed on disk" into a full reload, and + production deployments have no such file-system event to watch. The + separate Studio-left-nav-doesn't-refresh caching issue does not go through + this SSE stream and needs its own fix. +- 9b9af8d: The Approvals Inbox stops remounting every row on every render. + + `ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions` + inside its own component body. React identifies a component by the identity of + its function, so each render produced three brand-new component *types* and React + unmounted and remounted every row's subtree instead of updating it — and the page + holds its clock in state and ticks it every 60s, so this fired on a timer whether + or not anyone was touching the page (objectui#5348). + + Two consequences were reproduced against `origin/main` before the fix, in + `apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`: + + - **Transient subtree state is discarded.** Focus placed on a row's Approve + button moved to `<body>` on the next clock tick. + - **Input is silently swallowed.** A pointer sequence that spans a re-render — + press, tick, release — left the confirmation dialog unopened: the captured node + had been replaced, so React's delegated listener never saw the click. This is + the failure objectui#5211 hit and worked around at its call site + (`Unable to find role="alertdialog"`). + + The three cells are now at module scope beside `StatusBadge`, which was moved + there for the same reason and already carries the explanation. Everything they + closed over is passed in: `RequestCell` and `InlineActions` take the page's + scoped translator, and `RecordCell` takes `href: string | null` — one prop rather + than two, so the objectui#5211 readable/unreadable decision and the URL cannot be + handed in disagreeing with each other. + + The verification asserts the consequence, not the placement. A test that checks + the three functions now sit at module scope stays green for a refactor that moves + them and introduces a fourth inline component beside them; these cases compare + DOM-node identity for all three cells across a clock tick, which no remount can + pass, and re-drive the swallowed click. + + That guard is load-bearing because lint cannot supply one here. + `react-hooks/static-components` exists for exactly this class and is `error` in + this repo via the plugin's recommended set, yet it reports nothing on this page: + measured on `origin/main`, an arrow-form inner component injected into + `ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same + shape in a ten-line file produced two. The rule's analysis bails out on this + component, which is how three of them shipped. +- 9180fd1: Approvals inbox: the queue rows and the amount sort now honour each request + object's own `hidden: true` field declaration (objectui#6020). + + The `hidden: true` trim added for the drawer summary card reached only the + drawer — the desktop queue row, the mobile card and the amount comparator + still read the field, so an amount an app author declared hidden rendered + inline in the queue and ordered the list, which leaked its relative magnitude + even to a viewer who never saw the figure. + + The queue spans many objects, so the trim is a per-object lookup and every row + is answered about its own object; a row left with no renderable amount now + sorts with the other amount-less rows. `hidden` stays a UI contract + (objectstack#10749) and the filter still fails open: an unanswered or failed + metadata read renders today's figure. +- 7c0e417: The Approvals Inbox no longer shows a business approver the submitted record's raw + row JSON. + + The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no + principal check of any kind — so every approver could expand (and one-click copy) the + complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`, + `organization_id`, bare lookup ids, and **the fields the object's metadata declares + `hidden: true`**. Reported from a live EHR deployment on 17.1.0 + (objectstack-ai/objectstack#10734), where that declaration is a patient-data control. + The app author had no legitimate lever to remove the panel — field `hidden`, view + columns, app navigation, permission sets and env vars are all ineffective against it — + so the remedies available in the field were patching the shipped bundle or injecting + CSS. + + The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's + `studioEntry` module: `studio.access` is a declared platform-scope capability that a + tenant org owner does not hold by design, and it already reaches the browser in + `systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served, + computed or made authorable — no new config key, no new i18n copy, and the panel is + byte-for-byte unchanged for the platform operator it was written for. A business + approver keeps the structured record summary, the approval chain, the activity feed + and the decision actions; only the raw snapshot is gone. + + The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from + `usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a + holder's button while the server still refuses the write is the worse outcome for an + action. This panel has the opposite stake, since the measured defect is a non-holder + seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend + predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200` + with no `systemPermissions` at all, and a reported empty array. A deployment whose + permission layer just failed must not be the one that leaks the snapshot. + + `ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the + acceptance condition is that something does *not* render — which an empty render + reproduces perfectly — every denial case also asserts the drawer it denies inside, and + the `studio.access` case drives the same fixture through the same helper and finds the + panel. `created_by` and `organization_id` are the witnesses: both are in the page's + `PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can + reach the DOM only through the raw panel. Ablating the gate (restoring the bare + `payload != null` condition) turns the three denial cases red on exactly that + assertion and leaves the holder case green. + + Out of scope, tracked separately: trimming the summary by object metadata, and the + server-side residual that sends the unfiltered snapshot to the client at all. +- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop + clipping their tail steps. + + Both occurrences were a single non-wrapping flex row whose steps were each + `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable + items, so the bar's intrinsic width grew without bound with step count and + label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK + step names measured **1070px inside a 527px container** (objectui#5554). + + The two hosts failed differently, and neither failure was recoverable by the + reader: + + - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not + scrollable, so the nearest scroller was the drawer *panel*. Reaching steps + 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the + record card, the activity timeline and the action buttons off-screen and left + a near-blank panel. + - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one + carried `overflow-x-auto`, so it scrolled itself rather than its container. + Better, but the tail steps still sat behind a scroll gesture with no visible + affordance. + + In both, readers took the clipped bar for the end of the data; the reporting + customer acceptance tester said so verbatim. Widening the window does not help: + the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. + + Both now render as a column: one row per step, a badge-and-rail gutter, and a + label that may wrap. Width is capped by the container at every step count and + every label length, which also suits both hosts' tall-and-narrow aspect. The + rail segment below each step keeps the tint rule the horizontal connector used + — it is coloured by the step it leads *into*. + + **Always vertical, with no step-count or measured-width threshold**, because + the overflow is driven by intrinsic content width (labels x count), not by + count alone: three 16-character CJK labels already crowd a 527px drawer, so any + count threshold picks a cutoff that is wrong for some real flow, and a measured + one reintroduces a viewport-dependent branch. The card's requirement is a fix + that cannot break at an untested viewport or flow length, and a layout with no + breakpoint and no measurement is the form that satisfies it. Horizontal-with- + scroll was ruled out for both occurrences: it leaves steps behind a gesture. + + Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and + `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is + green against the broken code too — every step was always in the DOM, and the + clipping was layout — so the suites assert the property the defect names + instead: no row is `shrink-0`, every label is `min-w-0` and none is + `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no + axis, overflow or width-pinning class carries a breakpoint prefix (so there is + no viewport with untested behaviour). The reported failing regime is exercised + directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins + that the layout classes are byte-identical across all four, so no count + threshold can put some other flow length back on the old path. + + The two steppers are kept identical by hand rather than extracted to a shared + component: they live in different packages, and deduplicating them is a + refactor with its own surface. Filed separately. +- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) + + Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no + deprecation window, and the protocol-17 session face emits no `roles` key at + all. Three client sites still read it: + + - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's + Approvals tab and Home's To-do card. It read nothing else, so it sent **no + `role:` identity at all**: an approval addressed to a position rather than to + a person matched nothing and vanished from all three surfaces, silently. + - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the + Approve/Reject enablement. It also splits the scalar `user.role`, so it + degraded rather than dying: it still yielded `role:user` while dropping every + business position name (`manager`, `finance_approver`, …). + - **`AppContent`'s expression user** — forwarded a `roles` key that was always + `undefined` into every CEL predicate context. Removed; `positions` and + `isPlatformAdmin` were already forwarded correctly beside it. + + The retired spelling is **not** kept as a fallback — pairing the two is what + ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the + declaration. + + `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately + untouched: it is a semantics decision, not a rename, and is deferred to a + maintainer ruling. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- 631d81d: fix(console): paint a boot indicator during the pre-React white frame + + The console's `index.html` shipped an empty `<div id="root">`, so a hard + navigation showed a pure-white page until the module graph had downloaded, + evaluated and resolved the two round trips `main.tsx` awaits before + `createRoot().render()`. Measured on this repository's production build with + Playwright + CDP screencast (frames classified pixel-by-pixel, corroborated by + the Paint Timing API): 1224-2016 ms of pure white on an unthrottled localhost, + 2289-2297 ms at 20 Mbps/80 ms RTT, and 5808-6167 ms on a Fast-4G profile. + + The document now carries the indicator itself — an inline style block and the + same gradient tile `LoadingScreen` opens with — so it paints from the HTML + parser, before the first chunk is requested. It is removed on React's first + commit into `#root`, so it never doubles up with `LoadingScreen`, which + continues to own the init screen and its ten-language copy. +- 71ee495: The two form CONTAINER contracts now have ONE declaration each, derived from + `@objectstack/spec`, and the console reads them instead of its own copies. + + objectui#5542 converged the LEAF of this contract — the field spec — and left the + two containers above it untouched, because converging them was a bigger call than a + mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared + twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once + in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean + subset — these two had **already drifted, in both directions**, so neither copy was a + subset of the other and there were two live answers to "what may an author write": + + - `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; + the console declared none of them. The console's `columns` admitted the string arm + (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. + - `FormViewSpec` — the console declared `label` / `groups` / `sharing` / + `submitBehavior`; app-shell stopped at `type` plus `sections`. + + The drift is decided by asking the **contract**, not by picking a side. `columns` + does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` + with the four numeric literals, folded to a number by its own transform), so + app-shell's numbers-only declaration was rejecting metadata the platform accepts — + objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is + the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured + against the installed `@objectstack/spec` 17.0.0), because a form config is titled, + not labelled. The value that read actually finds is the VIEW's identity label, which + arrives on the `ExpandedViewItem` envelope or beside the config on a flattened + runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to + the body it unwraps, rather than smuggled onto the form contract. + + Both types are therefore **derived from the spec's own `FormSection` / `FormView` + with named narrowings** — the repo's sanctioned form for a spec-shaped local type + (`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the + two layers agree on comes from the spec and cannot fall behind it; the four positions + where this layer is deliberately narrower are each named in an `Omit` list and + restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving + it would silently re-open #5542), and `label` / `description` / `visibleWhen` / + `visibleOn` keep the shapes this repo's renderers and evaluators actually consume + rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s + `submitBehavior` union — previously hand-written under the comment "Mirrors the spec + FormView.submitBehavior union" — is now read back off the shared type, making the + mirror structural. `@object-ui/app-shell` re-exports both names from its package root + (type-only, erased at build — nothing is added to the bundle), because a type that + cannot be imported is a type that gets retyped. + + The pins are what make future drift loud, and each half is pinned on both sides. + `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the + non-narrowed half of each type against the spec's own symbol, so re-hand-writing + either declaration fails `type-check` the day the spec moves rather than years later + when someone reads two files side by side — and the console's pins read both types + back out of the **exported** `buildSections` signature rather than naming them, so a + re-inlined local copy fails even if it agrees on every key on the day it is written. + Their liveness controls are what stop them being phantom checks: the removed copies + are pinned NOT equal to the shared types (proving the `Equal` helper still + discriminates), the renderer's honoured `RenderableSection` is pinned not equal + either (so the authored-document and honoured-row types cannot be collapsed again), + and an undeclared key is still rejected (so the derivation smuggled in no index + signature or `any`). Every narrowing carries a matching negative pin, so "derived" + cannot quietly become "widened to whatever the spec says". + + Behaviour is unchanged — the runtime always accepted these keys. The vitest halves + prove it: a section spelling its column count as the string `'3'` lays out identically + to the numeric `3` on both sides, and a section carrying the keys only one side used + to declare builds the same rows. +- cebdfe7: The form-field authoring contract now has ONE declaration, and the console reads it + instead of its own copy. + + objectui#5040 was not a missing key. It was that **two hand-written descriptions of + one contract drifted**, and nothing could notice, because each was only ever checked + against itself. PR #5537 converged the two app-shell descriptions into + `views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: + `FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same + name, in a different package — so the same failure mode stayed fully available. + + Measured key by key before choosing a route, because the two honest outcomes are + "same contract, import it" and "genuinely narrower layer, rename it and pin the + subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, + every one identical in type, none console-only — and it sat in a position that + describes an **authored document**: `FormSectionSpec.fields`, read straight off the + `/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both + files even spell the same six-member `type` union and call the element type + `FormFieldSpec`). The narrow, renderer-honoured shape is a different type that + already exists in that file, `RenderableField`. So this was one contract described + twice, and the console's description was wrong about the document: legal metadata — + `visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, + and ten more keys — was undeclared there. That is #5040's own symptom, "the type + rejects the configuration the runtime accepts", which no runtime test can see. + + `@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root + (type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` + imports it and deletes the local declaration. Reachability is the load-bearing half: + a type that cannot be imported is a type that gets retyped, and retyped copies drift. + `form-spec.ts` itself is untouched. + + `FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the + field-spec type back out of the **exported** `buildSections` signature rather than + naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even + if the copy agrees on every key on the day it is written — which is exactly what did + not happen to the copy this change removes. Its liveness controls are what stop it + being a phantom check: the removed nine-key shape is pinned NOT equal to the shared + type (so the `Equal` helper is proven to still discriminate), `RenderableField` is + pinned not equal to it either (so the honoured-row and authored-document types cannot + be collapsed again), and an undeclared key is still rejected (so the import did not + smuggle in an index signature). Behaviour is unchanged: the runtime always accepted + these keys, and the vitest half proves the same rows are built. +- 07cd685: The console's form routes no longer seed a RUNTIME `defaultValue` into the + control, so the server-side resolution the declaration asks for actually happens + (objectui#5727). + + `readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared + default unconditionally. A `defaultValue` may be a literal, or an *instruction* + the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`, + `current_user`) or a CEL Expression envelope. Seeding one of those literally put + the text `NOW()` into a datetime input on both `/forms/:name` and the public + `/f/:slug` route, and submitting it sent that string as the field's value — + which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved + the declared default and the column stored the token text instead of a timestamp. + + The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same + published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its + seeding with, and the one this renderer already reads once removed (through + `isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default + leaves the key ABSENT rather than empty, because absent is precisely the case the + engine resolves. + + Nothing else about the prefill precedence moves: a literal default still seeds, a + stored record value still wins over a default, and an explicit `prefill_<field>=` + param still wins over both — including for a field whose default is a runtime + token, since a value a producer supplies is not a declaration awaiting + resolution. +- b63a9a3: The console's standalone form renderer now evaluates conditional field visibility. + + `apps/console/src/components/FormPage.tsx` is a **second, independent form renderer** + — its own `buildSections`, its own JSX — and it serves both the public + `/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of + the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` / + `visibleOn` *read* inside that file returned zero. So a field an author conditioned on + `record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec` + normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both + authors and honours — rendered unconditionally on both routes. Fail-open and silent: + the author saw the field always, with no diagnostic. + + objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different + chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form` + `sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx` + is on that chain at no point, and #2212's regression pin lives with the chain it fixed, + so nothing in the suite could see this copy. One contract, two implementations, each + only ever checked against itself. + + The wiring is **#2212's ruling applied verbatim** rather than a second predicate + semantics invented for this renderer, because two form renderers disagreeing about what + `visibleWhen` *means* would be a worse defect than one renderer ignoring it. The + predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`, + `evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and + `{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*` + = the stored record an edit form started from), and the fail-open-but-loud behaviour on + an unevaluable predicate are the shared ones by construction. Resolution is + canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers: + `sectionFields.ts` and app-shell's `readVisibility`. + + Two things deliberately did **not** change. A field hidden by its predicate still + submits its value — conditional visibility is a rendering rule in both renderers, and + making it a submit-payload rule would be a new contract decided once for both, not + invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain: + the second-renderer question is real, but it belongs with the #5596 convergence track, + not with a predicate that is dead today. + + `FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this* + renderer on purpose — a pin that cannot see the second copy is how the first gap + survived. With the fix reverted and the pin in place the suite reports + `11 failed | 1 passed (12)`; the one green is the control that has to be green (a field + with no predicate still renders), without which every "the field is absent" assertion + would be equally satisfied by a renderer that draws nothing at all. +- fb934fb: The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to. + + `AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the + app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same + three statically for the platform portal at `/docs` (ADR-0046 section 6), so all + of them sat in the eager graph regardless and the `import()` moved nothing — + three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build` + (objectui#5467). A static import on either side silently defeats the split for + both, and the only signal is a build warning that fails nothing. + + `App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`, + matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even + though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot, + so nothing imported `DocsIndex` dynamically, but left static it alone would keep + `DocShell`, `use-book-data` and `book-nav` eager and the portal would only + half-leave the closure. + + Measured on this branch with the `dist/eager-closure.json` gauge added by + objectui#5324, both builds exiting 0: + + | | before | after | + |---|---|---| + | `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 | + | eager closure, gzipped | 3,881,609 B | 3,870,058 B | + | eager chunks | 58 | 52 | + + Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz), + `CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B), + `componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown + folds into the entry chunk rather than dropping — which is why the entry chunk + grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by + 11,551 B. The entry stays far under that budget's 350 KB line, and the eager + closure is the number a page load actually pays. + + What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this + looked like a bigger win than it is. Three eager chunks import it statically, + and only one of them was this portal: `plugin-chatbot` reaches it directly, and + `packages/fields`' `MarkdownContent` — lazy in source — is folded into the + eagerly imported `ui-components` chunk by the `advancedChunks` group that claims + every `packages/fields` module. That is objectui#5325's mechanism, not this + card's, and it is why the saving here is 0.30% rather than 4%. +- 8549453: The console's pre-boot branding script now resolves its server origin from + `VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of + `window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set + (objectui#5660). + + The two callers of `GET /api/v1/runtime/config` share one in-flight request, and + that sharing keys on the FULL URL. So the origin was half of the contract: the + inline script in `index.html` read one spelling and `src/main.tsx` read another, and + whenever they disagreed the two never found each other. A same-origin production + build hid it completely — both spellings collapse to `''` — so the split surfaced + only in a dev pointing the console at a separate server, where it cost two requests + to two different servers and let pre-boot branding paint from the wrong one. + + The pre-boot fetch is kept, not deleted: it is the request the module side JOINS. + `sharedGetJson()` can only hand an earlier request to a later caller, and this + script is the earlier one by construction — it runs during HTML parse, before the + bundle is fetched, while `initRuntimeConfig()` is awaited before + `createRoot().render()`. Deleting it would not remove a request; it would move the + single remaining one later, onto the critical path to first paint, and leave the + page's empty `<title>` and favicon unbranded until React mounts. + + Vite substitutes its HTML env token only when the variable is set and leaves it + verbatim otherwise, so the unset case is read as same-origin `''` rather than + allowed to reach the URL as a path segment. +- 7493bff: `registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing. + + The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a + `Suspense` fallback — naming the same barrel the line above it imports + statically for `registerAppComponent`, and the same barrel `App.tsx` pulls + `BuilderLanding` from to render the standalone `/studio` landing full-screen. + Either reason alone makes the `import()` unable to move a module into another + chunk (objectui#5486). + + **This moves no modules and is not a bundle improvement.** `BuilderLanding` was + already in the eager graph via `App.tsx` and still is. Measured on + `dist/eager-closure.json`, both builds exiting 0: the eager closure holds the + same 52 chunks with the same names, and the only difference is 130 B gzipped + (413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback + text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure, + because nothing could. + + What it does fix is honesty. The old code told every reader the builder was + deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every + console build — the console's count of those drops from 44 to 43, with the 43 + remaining ones all belonging to the `packages/fields` barrel (objectui#5325). + A permanent warning that fails nothing is how a team learns to skim past build + warnings, and a decorative `lazy()` is how the next reader learns something + false about the chunk graph. + + The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx` + lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in + statically, and is genuinely deferred; it is untouched. Making the *builder* + genuinely lazy would mean taking `App.tsx` off the static import too, changing + how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind + the barrel — a separate measured card, not folded in here. +- d8afbe5: `FormPage` — the console's own form renderer, serving both the public `/f/:slug` + route and the internal `/forms/:name` one — now honours the three conditional-rule + surfaces it still dropped after objectui#5594: section-level `visibleWhen` / + `visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` / + `requiredWhen` (objectui#5627). + + This is the second form renderer in the repo, and it honoured exactly one of the + four surfaces the sibling chain does. A section an author conditioned away rendered + in full — heading and every control — on both routes including the anonymous one. + The object-level half was worse than fail-open hiding: `readonly` was whatever the + static flag said, so a field a `readonlyWhen` should have locked stayed editable and + paired with the server's fail-closed unbound-scope behaviour into "the user edits, + the save reports success, and the value never lands". + + Both halves evaluate through the SHARED machinery rather than a fourth consumer-side + copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three + field rules — which brings the settled rulings with it, including the `serverOwnedValue` + carve-out that keeps a create form from requiring a producer-owned control (#4069 / + #4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first + `visibleWhen ?? visibleOn` read every sibling reader spells. + + Visibility stays a RENDERING rule at both granularities: a hidden section's fields + still submit their values, exactly as a hidden field's have since #5594. +- cdda37a: `buildSections` now honours a FormView field's `maxLength` override instead of always + taking the object's ceiling (objectui#5595). + + The function merges a form's field overrides with the target object's field definitions, + and its own docstring states the rule: *"Field-level FormField overrides take precedence + over object defaults."* Every key in the loop is built that way — `override.label ?? + def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder` + — except one, which read `def.maxLength` unconditionally. So an author who set a tighter + per-form limit (a short public intake form over a column whose object-level ceiling is + generous) got the generous one. + + The failure was silent in the worst direction: no diagnostic, no warning, and the form + still submits, so the symptom is a value the author believed the input refused being + accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two + `maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"` + arm. + + `override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys, + so an explicitly declared `0` stays a value the author wrote rather than falling through + to the column's ceiling. This narrows only what the input allows; the object's storage + ceiling still decides at submit time, so nothing that was accepted before is now rejected + anywhere but at the keyboard. + + Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all + until objectui#5542, so no one typing a spec in this app could write the override in the + first place, and the inert merge branch was never exercised. #5542 converged that type + onto the shared app-shell declaration, which does declare the key — making the gap + expressible, and therefore findable. + + The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in + `FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than + assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered + evidence for this fix, and it is what made the gap findable in the first place, so it + keeps its place and names the honoured answer. +- 60b2211: Console form pages no longer submit a cleared server-owned field as a blank. + + A field whose declared `defaultValue` is an instruction the server resolves per + insert (a `NOW()` / `current_user` token, or a CEL expression envelope) opens + with an empty control on a create form, and its key stays out of the payload + while nothing touches it. But a submitter who typed into that control and then + cleared it put the key back holding `''` — and `ObjectQL.applyFieldDefaults` + resolves a declared default only for a field arriving absent or null, so the + blank was stored and the declaration silently defeated. + + Such a key is now dropped from a CREATE submit on both the internal + `/forms/:name` and the anonymous `/f/:slug` route. A blank cleared from a field + with no runtime default — or with a static one — is still submitted, because + that is the user removing a value; and an edit submit is untouched, where a + cleared column is a deliberate removal. +- 343c598: The active organization id is now stored per user, and a change of session user drops the + previous user's client state wholesale (objectui#5664). + + `auth-active-organization-id` was a single un-namespaced `localStorage` key while its + siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, + `flow-palette-recents:u:`). On a browser handed from one account to another — a shared + machine, a kiosk, a handover, a support session — the arriving user's console read the + PREVIOUS user's organization id. The header workspace chip rendered the previous user's + workspace for a user whose `organization/list` was empty, and the consequence past the + cosmetics is the one worth stating: the polluted org context suppressed + `RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so + a brand-new user on that browser silently never got the new-user flow at all. + + Nothing about row visibility rode on this. With the stale id the server answers + `403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero + environments; the damage was entirely in what the client believed about itself. + + Three changes, and the third is the one that closes the class rather than the instance: + + - The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention + its siblings already use. + - It can no longer be written un-namespaced at all. Where no session user is known yet the + value lives in memory for that page-load only — a namespacing that kept a bare-key + fallback would re-open the defect the first time a write happened before the user id + resolved. + - **A change of session user drops the previous user's client state wholesale.** This is an + allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so + the NEXT storage key someone adds without a `:u:` scope is covered before it is written. + Only device-scoped entries survive: the arriving session's own bearer token, the pointer + recording whose state the browser holds, and the UI theme. + + Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a + non-null `localStorage` read and falls back to the in-memory value, and the memory value is + nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge + too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- + sign-in path that never reloads the page. + + Existing browsers are not migrated. A value sitting under the retired bare key is + unattributable — nothing recorded whose org id it is — so migrating it is precisely the + defect it would be migrating away from, and it is deleted instead. A signed-in user loses + nothing durable: the active organization is a server-owned fact that + `AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no + active org is held, including the ADR-0081 single-membership repair. One boot re-supplies + it; users with no organization land on the guided first-run flow, which is the outcome this + card is about. + + `apps/console`'s pre-render auth preflight purges every spelling of the active-org key — + the retired bare one and each `:u:` scope — when it finds a dead bearer token, and + deliberately leaves the session-user pointer in place so the next sign-in can still tell + that the browser changed hands. +- 9093de8: Approvals drawer: the business summary card no longer renders a field the object + declares `hidden: true`. + + `payloadSummary` built the card from the request's `payload_json` snapshot behind + five filters (system keys, the lead amount key, null/object/empty values, + unresolved opaque ids, a six-field cut) and no field-visibility filter, so a + hidden field that survived to the first six survivors rendered in the card, + labelled. The drawer now reads the open request's object metadata and drops the + declared-hidden keys before the six-field cut, so the next business field is + promoted into the freed slot rather than the card silently shrinking. The lead + amount figure at the top of the same card takes the same trim. + + Per the platform ruling, `hidden: true` is a UI-only contract and `internal: true` + is the serialization primitive, so this is the UI enforcing the only contract + `hidden` has — not a client-side compensation. Field-level security is unchanged + and remains the server's answer. The metadata read is the same cached + `GET /meta/object/:name` the record form already performs, once per object per + page visit, and an unanswered read leaves the card exactly as it renders today. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- e067173: Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains. + + **The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`. + + The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists. +- 7d0143c: The Console now gates the `/studio/*` routes on the `studio.access` ENTRY + capability, not just on the backend's refusal of the writes behind them + (objectui#5519). + + `/_console/studio/` rendered the full Studio pillar builder — Data / + Automations / Interfaces / Access, with Publish and Save draft — to any + authenticated principal who typed the URL, on deployments where the Studio nav + tile is deliberately absent and every metadata write is refused. A plain tenant + user was walked through the entire "new package" form and only refused at + submit (403). The lockdown criterion for that deployment shape is two-part — UI + entry hidden AND API refused — and only the API half was met; what stood on + this side was a write-level gate where an entry-level one belongs. + + The whole `/studio` subtree now hangs off one route element that reads + `systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this + app already consumes) and admits only a principal whose LOADED set carries + `studio.access` — the capability declared as "Enter the Studio metadata-design + surfaces", which a tenant org owner does not hold by design. Everyone else is + sent to `/home` without the builder ever mounting. + + The fail direction is deliberately inverted from this app's other capability + gates: those fail OPEN on an unknown answer because their bad outcome is a + holder losing a button, whereas a route gate's bad outcome is a non-holder + seeing the builder. So the loading window renders the console splash (never the + builder), an outright fetch failure renders the retryable error splash, and a + `200` that carries no `systemPermissions` at all is refused rather than waved + through. The server-side refusals are untouched. +- Updated dependencies [0db4fb3] +- Updated dependencies [4703651] +- Updated dependencies [9e37d9b] +- Updated dependencies [cf1d29e] +- Updated dependencies [8d58f46] +- Updated dependencies [0b1326d] +- Updated dependencies [305205a] + - @object-ui/sdui-parser@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/apps/console/package.json b/apps/console/package.json index eeb86d7b08..544cc1dc08 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/console", - "version": "17.6.0", + "version": "17.7.0", "description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.", "license": "MIT", "type": "module", diff --git a/packages/app-shell/CHANGELOG.md b/packages/app-shell/CHANGELOG.md index c3876b79ac..395a710288 100644 --- a/packages/app-shell/CHANGELOG.md +++ b/packages/app-shell/CHANGELOG.md @@ -1,5 +1,6113 @@ # @object-ui/app-shell — Changelog +## 17.7.0 + +### Minor Changes + +- 2a23000: One composer for makers — the built-in `ask` folds into `build` for authoring + principals (cloud#1674 maker convergence, Phase B). + + Ruling: `build` is the higher-privilege admin agent and, since cloud#1673, a + strict data superset of `ask` — it answers records/aggregation/chart questions + with the same tools. So Ask/Build stop being peer modes for a maker: + + - `surfaceAgent.ts` (the ONE ADR-0063 resolver) gains rule (3b): an `ask` want + upgrades to `build` for a principal with `manage_metadata` when the catalog + serves it — including over an app's explicit `defaultAgent: 'ask'` pin (the + pin serves the app's business users; the maker always gets the superset). + New `makerConvergedOnBuild` / `makerVisibleAgents` carry the same predicate + to the picker sites. + - ChatPane's agent launcher lists `makerVisibleAgents`: the built-in ask leaves + the list for makers (custom agents stay), computed inside ChatPane so every + host — full `/ai` page, Studio copilot, console dock — converges identically. + - A maker's bare `/ai/ask` redirects to `/ai/build`; `/ai/ask/:conversationId` + keeps rendering, so ask history stays readable. + - `ConversationsSidebar` gains `includeAskConversations`: the converged surface + lists BOTH built-in groups' threads whichever one is open (merging only ask + made the build history vanish the moment an ask thread was opened — measured + in-browser on the first pass). + - Console home shows the single Build entry for makers; non-authoring sessions + (the objectstack#8270 hosted posture) keep Ask AI, and every layer is a no-op + for them and for deployments without a build agent. + - `useCanAuthorMetadata` extracted from HomePage to `hooks/` so the CTAs and + the chat surfaces answer the same per-principal question. +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- d796c8d: Auto-derived related lists consume the field-level `relatedListFilter` + declaration — the list query AND-composed, the tab badge counting the same set + (objectui#4664). + + A `lookup` / `master_detail` field may now declare `relatedListFilter`, a + canonical Query-DSL `FilterCondition` such as `{ status: { $ne: 'deleted' } }` + (`@objectstack/spec` 17.1.0 — objectstack#8704 / PR #8955). Until now this repo + accepted that key at every gate and consumed it nowhere: a record page's derived + related lists answered wider than the metadata asked, and the driving scenario — + soft-deleted child rows on auto-derived record pages — had no way to be + expressed at all. + + What ships: + + - `deriveRelatedLists` reads the key off the FK and carries it on the derived + descriptor; `RecordDetailView` forwards it into the page synthesizer, which + emits it onto the `record:related_list` node's **existing** `filter` key. That + key already had a read site (objectstack#7118): `RelatedList` ANDs it with + `{ [referenceField]: parentId }`. The declared filter is therefore an authored + constraint that may only NARROW this parent's children — never a replacement, + which would leak other parents' rows — and no second filter dialect appears + for derived pages. + - The **tab badge honours the same composed filter**. `page:tabs` reads the + `filter` off the `record:related_list` node it is badging and the count store + composes it with the parent scope through the same `mergeFilterNodes` sink the + row query uses, so the badge and the rows send one `$filter`. Badge-count + parity is normative in the spec key's own contract text; without this half the + feature would ship the defect it exists to prevent — a badge saying 7 above a + list showing 3. + - Counts cache per (object, relationship, parent, **scope**), so a filtered and + an unfiltered probe over the same relationship are separate entries rather + than one wrong number. + + With no `relatedListFilter` declared, the synthesized node, the row query and + the badge probe are byte-identical to before. Consumption only — this change + adds no authoring UI for the filter. +- 053fdc8: Console half of `ActionSchema.onSuccess` post-success navigation. + + `@objectstack/spec` declares `onSuccess` as a closed strict object + `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to `type: 'api'` and + `type: 'script'` — the two action types whose success event carries a server response. + Nothing in this renderer read it, so an action declaring the hop navigated nowhere: the + block fell into `ActionRunner`'s older `ActionDef.onSuccess` chained-callback channel, + was dispatched as an action, and failed inside `executeNavigation` with "No URL provided + for navigation action" — a red toast and no jump. The motivating report is a clone action + that leaves the user sitting on the record they cloned from. + + `ActionRunner.handlePostExecution` now performs the declared hop through + `navigationHandler` — the same SPA seam every other navigator in that file uses, which + the console wires to react-router's `navigate`, so `openIn: 'self'` is a real in-place + route hop rather than a full-page load. `interpolateTarget` gains a `${result.*}` scope + alongside `${param.*}` and `${ctx.*}`, resolved against the handler's own return value + (the level `readActionPayload` reads, one below the action envelope) and supplied only by + this call site, so a target interpolated before its request still has no `result` to + name. `openIn` is read as the one member that changes the branch and no default is + written here — the spec materialises `.default('self')`, so parse output always carries a + resolved member — and the two `openIn` spellings stay apart: this reads + `onSuccess.openIn` (`'self' | 'newTab'`), never the top-level `type: 'url'` switch + (`'self' | 'new-tab'`), each of which spec refuses in the other's position. + + The console's server-action wrapper gains the matching handler-return half: a handler may + now return `openIn: 'self'` next to its `redirectUrl` to ask for the same-tab jump, while + a `redirectUrl` **without** `openIn` keeps its shipped new-tab behaviour unchanged. When + an action declares an `onSuccess` block, the wrapper defers to the runner and only tidies + its pre-opened tab, so one navigation happens rather than two. + + The pre-existing `ActionDef.onSuccess` chained-callback channel is unchanged. It is told + apart by the spec's own declaration — a non-array object whose `navigate` is a string — + and keeps running for every other shape. +- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's + ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a + list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user + dragging a column or cycling density was re-styling that view for the entire organization. Nothing + in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, + v18) and is deliberately not built here — which is precisely why the write has to be gated rather + than narrowed: there is no second, private store for it to fall back to. + + `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability + set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` + (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the + `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — + before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. + It is on the write rather than on the toolbar button on purpose: withholding the affordance would + leave the method still accepting the call from anything else holding the adapter, whereas a gate on + the write is inherited by every caller, present and future. + + `manage_metadata` is not a newly minted name. It is the capability this repo already treats as + metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself + refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very + same ADR-0005 metadata door, so this applies the authority the server is already applying instead of + inventing a parallel one. + + **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating + ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a + client-side refusal on missing data cannot protect anything and can only break a permitted user. A + *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new + `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. + + The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure + into `console.error`, which for a debounced toggle whose UI has already moved would have left the + operator looking at a density they did not get; a denied write now raises a toast. And the "View + settings" popover — where density and field visibility are actually changed — now states the scope + before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who + uses this view."*, translated in all ten packs. +- 26fbe09: Console telemetry can now be hard-disabled on an already-built artifact + + `/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting` + (objectstack#11382), and the Console now reads it. The Sentry decision becomes a + conjunction of two independent grants — a DSN injected at **build** time AND a + positive permission from the **runtime** — so the single pre-built SPA that both + the hosted SaaS console and the on-premises / air-gapped EE images embed can be + silenced by the deployment it lands in, with no rebuild and without editing files + inside a published bundle. That was the half objectui#5522 could not close before: + every other input to the gate is a Vite build-time variable frozen into the bundle + as a literal, which is how an air-gapped EE Console came to send 14 Sentry + envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for + the customer to turn it off (objectstack-ai/cloud#1508). + + The permission fails **closed** in every direction: absent key, `telemetry` block + absent, malformed payload, failed fetch, or a runtime predating the key all read as + *do not send* — which is precisely the set of runtimes leaking today. It is a + permission and never a source: the server supplies no DSN and cannot turn telemetry + on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1` + and other truthy lookalikes do not. + + Behaviour change for deployments that already inject a DSN: reporting now also + requires the runtime to grant permission, via + `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s + `allowClientErrorReporting`). A build that opted in but whose runtime says nothing + will go quiet — deliberately, since that is the same artifact an air-gapped + customer runs. + + `@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and + the `RuntimeTelemetry` type, so consumers read the permission through the one + fail-closed accessor instead of writing their own optional-chain against the + payload. +- 6d63cd0: An auto-derived related list now orders its rows by the CHILD object's default list view + `sort`, instead of falling to the server's primary-key order (objectui#5795). A task + version's "check items" tab whose child object declares `sort: [{ field: 'seq_no' }]` + renders 10/20/30/40; before this it rendered whatever order the ids happened to give — + 20/30/10/40 in the reported case — while the child object's own list page obeyed the + declaration. + + **Declared as user-visible, deliberately, even though no key was added.** The contract + question ("where does a derived related list's sort declaration live?") was ruled on + objectstack#11345 (maintainer, 2026-08-23) as **direction 1**: inherit the child's list + view sort, and add **no** new spec key — the field-level `relatedListSort` the issue also + proposed was explicitly not approved. So there is nothing new to author, and + `record:related_list.sort` was already declared, parsed and consumed; this fills it. What a + host observes is nonetheless new: a derived related-list descriptor gains a populated + `sort` where it had none, and the query it issues gains an `$orderby`. An app whose child + objects declare a default list order will see those tabs re-order on upgrade — which is the + point of the change, and is why this is not a patch. + + Nothing is inherited where nothing was declared: a child object with no default list-view + sort produces the same descriptor, the same node and the same `$orderby`-free query as + before. + + The two `sort` surfaces declare the same union and mean different things by its string arm + — a `ListView` string is the legacy space-separated `'seq_no desc'`, while the related + list's own reader takes `'field'` / `'-field'` — so the inherited value is normalized to + the array arm once, at the derivation, through `@object-ui/core`'s + `convertSortToQueryParams` (the repo's single definition of both authored dialects). An + un-normalized inherit would have ordered by a field literally named `seq_no desc`. + + Known and unchanged: `$orderby` is only assembled while the related list is in windowed + (server-paged) mode, so a declared *or* inherited sort still disappears while the built-in + client text filter is active. That hole pre-dates this change and affects the authored prop + identically; it is now pinned as a recorded fact in + `plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx` rather than + fixed here. +- 4da5109: One `sys_activity` row → `FeedItem` constructor, and the console record page + stops dropping author-extended activity types in silence (objectui#5896). + + **The defect.** `RecordDetailView`'s `sys_activity` merge read the shared type + table (objectui#5878) and then built the `FeedItem` itself, ending in + `if (!feedType) continue;`. That one `continue` collapsed two different + situations: a type the table maps to `undefined` **on purpose** (`commented` / + `mentioned` / `login` / `logout`), and a type the table has never heard of. The + second is an **author-extended** value — `sys_activity.type` is + author-extensible (objectstack#11507 direction 4, ruled 2026-08-24), every + column on that table is `readonly` so objectql never validates a write, and + ADR-0052 §5b.2 forwards an author's `activityMilestones[].type` into it + verbatim. So an activity that happened, was written and is queryable had no row + on the console record page: no placeholder, no empty state, no console message. + Stored, queryable, invisible — objectui#5840's failure mode reached by another + route, and one objectui#5969 (PR #6112) had already removed from the block + side, leaving the two surfaces disagreeing about the same row of the same table. + + **The fix is convergence, not a second decision.** `@object-ui/plugin-detail` + now exports the whole reading — `activityRowToFeedItem`, + `UNMAPPED_ACTIVITY_FEED_TYPE` and the `resetUnknownActivityTypeWarnings` test + seam alongside `ACTIVITY_TYPE_TO_FEED_TYPE` — and `RecordDetailView` calls the + constructor instead of paraphrasing it. Publishing the table alone had left the + mirror one level up, and it had already drifted three ways: the silent drop, a + timestamp fallback that could leave `createdAt` `undefined` where the helper + yields `''`, and a second hand-written system-actor lookup. + + **Behaviour change on the console record page** (breaking in the objectui sense, + shipped `minor` — objectui's `major` tracks `@objectstack`): + + - an unmapped `sys_activity.type` now **renders** through the generic + `UNMAPPED_ACTIVITY_FEED_TYPE` (`'system'`) presentation instead of vanishing, + and is announced once per distinct value on `console.warn` — a **missing + decision**, not lost data: the row is visible, what it lacks is its own icon + and colour. `FeedItemType` is a closed spec enum, so minting a kind for "we + don't know" would be a platform change, not this surface's. + - `createdAt` is always a string for a row with neither a usable `timestamp` + nor a `created_at`. + + **Unchanged, deliberately:** the four exclusions still produce no row and no + warning. They are decisions — comment content lives in `sys_comment`, and + login/logout are account events rather than record activity — and a warning + about a decision teaches authors to ignore the channel. +- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). + + Seven modules resolved authored icon names into lucide's runtime `icons` record, each + with its own copy of the logic: **three different tokenisers** (`split('-')` on five of + them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` + rename on only **four** of the seven. The same authored name therefore rendered on one + surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened + with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, + and the other six call it. + + **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it + was measured rather than chosen.** Its regression set is empty three independent ways: + against the authored population, against a maximally-pessimistic every-authored-name x + every-surface cross-product, and against a bound-free differential over 8,298 spellings + derived from all 1,767 live record keys — each with a discrimination control that fired + in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface + pairs in that last reading, stripping two surfaces of every snake_case and + space-separated spelling they resolve today. + + **What changes for you — all of it widening, none of it removal.** No name that resolved + before stops resolving: no key of lucide's record contains `_`, whitespace or `-` + (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the + wider one produces the same key. Sixteen name-surface pairs start resolving where they + rendered a fallback or nothing before: + + - `layout_dashboard` and `building_2` (and every other snake_case or space-separated + spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, + `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and + the related list. + - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried + no rename map. `Home` is not a live record key, so this could only ever be a widening. + + **What does NOT change: what each surface draws when a name does not resolve.** The seam + answers `name -> component`, returning `null`, and decides nothing else (maintainer + ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at + the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning + (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, + `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` + and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and + then dropped once the tree was measured to have four such behaviours rather than two: a + lookup function is the wrong place to publish a presentation decision. + + `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface + this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: + its census goes from seven sites to one, and its normalisation stops being a + widest-common approximation of three disagreeing resolvers — so the under-reporting that + gate disclosed at objectui#5932 is closed rather than merely bounded. +- d3d0ad3: `asArray` in the two app-shell `find()` readers now reads exactly what + `QueryResult` declares — the `records` and `items` arms are removed + (objectui#5945). + + `useApproverDirectory.ts` and `views/metadata-admin/AssignedUsersSection.tsx` + both normalised a `find()` answer with + + ```ts + Array.isArray(res) ? res : res?.records ?? res?.items ?? res?.data ?? []; + ``` + + `QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` — + alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`. + `records` and `items` are not members of it, and both were tried *before* the + one that is. That is AGENTS.md #0.1 in miniature: a tolerant reader that lets a + non-conforming producer keep working, so the wrong shape is never rejected + anywhere and hardens into a second de-facto contract. The same + `records`/`items` confusion was live in three other places that objectui#5458 + had to fix, each reading a key no adapter returns — a helper that quietly + accepted all three spellings is why nobody found out two of them were wrong. + + **What stops being accepted.** A `find()` answer shaped `{ records: [...] }` or + `{ items: [...] }` now reads as **no rows** at these two seams instead of + silently resolving. Both call sites degrade rather than throw: the approver + directory reports the reference unresolved (falling back to the prettified + machine name, and staffing as probed-empty), and the assigned-users section + renders its empty state. + + **Nothing produces those shapes today**, which is why this is a removal rather + than a migration. Measured across the repo, per arm: + + - **`records`** — no producer at the `DataSource.find()` seam. + `ObjectStackAdapter.normalizeQueryResult` maps the server's `records`/`value` + envelope to `data` before returning, so the spelling exists only *below* the + adapter, on the wire and in the client SDK. The two remaining `records` + producers in the repo are on different seams: `ViewDataProvider` returns its + own `ResolvedData` interface, which declares `records` legitimately, and is + not a `QueryResult`. + - **`items`** — no producer at any seam. Every `items` in the repo is the + unrelated UI-schema key (dropdown menus, timeline, accordion). + + **The bare-array arm is kept**, because it is live: fakes at these seams answer + with a plain array (`AssignedUsersSection.test.tsx` is one). It is pinned in the + same tests as the deletions, so the live and the dead shapes cannot drift into + each other. +- 864154e: The Field Designer no longer offers a formula-expression textarea, and no designer write + path emits a `formula` key (objectui#6043). + + **This is a behaviour change on an authoring surface: a control is removed.** A field's + `type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the + palette — but the expression itself is no longer authored here. Authors write formula + expressions in metadata-admin's field inspector, where they are checked. + + The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against + the installed `@objectstack/spec` 17.2.0: + + ``` + FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) + => success = false + => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?" + ``` + + so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because + the key was then stored, it blocked **every later save of that object**, not just the one + that introduced it. + + **The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges + the key name and never the expression LANGUAGE — measured, it accepts + `expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the + empty string is refused. Spec `expression` is CEL rooted at `record` + (`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity` + — bare field refs, which under the scope formulas bind evaluate to null silently. A rename + would therefore have converted a loud, immediate 422 into a formula that saves clean and + then quietly computes nothing, which is strictly worse than the bug it appears to fix. + + Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType` + inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and + app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a + dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a + feature, not this fix. `returnType` is likewise not authored here: it is only derivable by + inferring the CEL result type, and with no expression control there is nothing to infer + from. + + `formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already + carrying the key is stripped clean on its next save instead of staying blocked forever — + which matters more than usual here, because with the control gone an author would + otherwise have no way left to clear it. It is dropped rather than migrated to `expression`, + for the same reason the rename was refused. A `expression` authored in metadata-admin is + **not** touched: it is a real `FieldSchema` key and rides through the designer's + round-trip untouched. + + Also removes the now-unreachable `formula` read/write from + `views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for + the key that neither the card nor the parity gate named. + + The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s + `KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that + left a stale entry behind would be as red as a new offender. +- b023625: The field metadata payload no longer emits `sortOrder`, the key `FieldSchema` refuses by + name (objectui#6045). Field-level sibling of objectui#6223, same objectui#5761 family. + + Measured against the installed `@objectstack/spec` 17.2.0, whose `FieldSchema` accept set + is 71 keys: + + ``` + FieldSchema.safeParse({ type:'text', label:'L' }) => success = true (control) + FieldSchema.safeParse({ type:'text', label:'L', sortOrder: 3 }) => unrecognized_keys ["sortOrder"] + + FieldSchema.safeParse({ type:'text', label:'L', sortable: true }) => success = true (control) + FieldSchema.safeParse({ type:'text', label:'L', sortable: 3 }) => success = false + ``` + + The control is what makes that a key-by-key result rather than a schema refusing + everything, and the `sortable` pair is what shows the near-spelling is a *different + concept* — a boolean ("whether field is sortable in list views"), not this key's spec + name. + + **The resolution was deletion, not a rename**, which is objectui#4687's shape rather than + objectui#6041's. The spec has no field-level ordering key at all: it models field order by + **declaration order** in the object's `fields` record, so a designer that wants explicit + ordering reorders that record rather than carrying an index. There was nothing to map onto, + and nothing was invented to map onto. + + **It was latent, and that is confirmed on today's tree.** Neither of the two sites that + construct a `DesignerFieldDefinition` — `FieldDesigner`'s create/update handlers and + `MetadataFieldsPage.toDesignerField` — ever named the key, so `toFieldPayload` emitted + `sortOrder: undefined` and `JSON.stringify` dropped it. The key never reached the wire. It + was one reorder feature away from doing so, which is the objectui#4644 shape: a hard 422 + `INVALID_METADATA` that blocks every subsequent save of the object, with nothing in the UI + to say which key caused it. + + Removed in one go from the wire shape (`FieldMetadataPayload`), its writer + (`toFieldPayload`) and the UI model (`DesignerFieldDefinition`), so no declaration is left + behind that no writer fills and no schema accepts. + + **Breaking for TypeScript consumers**: `sortOrder` is gone from `DesignerFieldDefinition` + (`@object-ui/types`) and from `FieldMetadataPayload` (app-shell), so code that set either + stops compiling. + + Two keys share this spelling and are untouched, which is why the census was on the *shape* + — a field-metadata payload key `FieldSchema` refuses — rather than on the identifier: the + **object-level** `sortOrder` (`ObjectSchema`'s, removed from the object wire shape by + objectui#6223 and deliberately kept on the `ObjectDefinition` UI model) and the + **saved-view** `sortOrder` in `ObjectView`, which is per-view display order on a different + document entirely. + + The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes + with the fix — that ledger ratchets in both directions, so an entry left behind for a + resolved key is as red as a missing one. +- 5173a5e: ⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on + the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read + this before upgrading if any of your form metadata gates on the session user. + + objectui#6010 bound the host predicate scope on the five authored-predicate call + sites in the components form renderer, so `current_user` (plus the ADR-0068 + `user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn` + there. Two other authored-predicate evaluators were still passing `undefined` + for that argument, so the same authored text meant two different things + depending on which surface opened the form (objectui#6110): + + - **`apps/console`'s form renderer**, on the authed internal route + `/forms/:name`. The internal route is a runtime record surface by ADR-0089 + D1's own words (*"runtime record surfaces bind `record` + `current_user`"*), + and its `visibleWhen` metadata is the same `*.view.ts` FormView the + object-view chain renders — so a role gate authored once behaved differently + depending on which route opened the form. + - **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`), + the gate that re-checks the whole declared field set at final submit because + `allowSkip` can jump past a step. Its docstring promises *"the same verdict + from all three rather than a second, divergent dialect"*, and since #6010 it + was the divergent one. + + **Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails + OPEN: a field on screen is what you get when the predicate resolves TRUE, when + the scope was never bound so the predicate faulted, *and* when the predicate is + broken. Those worlds were indistinguishable, so an app that authored a + `current_user` gate saw the field render and had no way to tell the rule was + inert. After this change the predicate is evaluated for real, and fields and + sections that have always been visible will disappear for the users the rule + excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user` + requiredWhen that has been silently not applying will now start holding submits. + + In the wizard the change is a fix in the user's favour as well: a required field + the wizard HID from this user was still counted as visible by the submit gate, + so the submit was refused on a control the submitter could neither see nor fill + in. + + **Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in + your form-view and object metadata that names `current_user`, and confirm each + predicate says what you actually want evaluated against `record` + + `current_user`. + + **The public anonymous form `/f/:slug` is deliberately unchanged.** It is + mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is + no authenticated principal, and no provider is mounted above it — so its scope + is empty and a `current_user` predicate authored on a public form still faults + and still fails open, exactly as before. Nothing new is declared to say so: the + two routes are told apart by which component mounts them. + + `@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider` + user normalisation, so every console surface that mounts the provider publishes + the same `current_user` shape rather than re-deriving it. +- 1e7fe0a: `record:details` section headings converge on the declared `label` slot; the + `title` alias limb is gone (objectui#6190, maintainer ruling 2026-08-31 — + option A, three producers plus the consumer in one change). + + **Breaking for anyone reading `deriveFieldGroupDetailSections`' output — the + emitted key moves from `title` to `label`.** That function is public API + (exported from `@object-ui/plugin-detail`), so this is an output-shape change, + not an internal refactor. A caller that reads `section.title` off its return + value reads `undefined` after this release and must read `section.label`. + `BuildPageOptions.sections` and `ObjectDefLike.sections` declare the same move, + so a caller passing `sections` into `buildDefaultPageSchema` supplies the + heading as `label` too. + + **The retired authoring spelling is `title` on a `record:details` section.** + `RecordDetailsRenderer` read `s.title ?? s.label` — a strict-priority second + spelling of one slot, with byte-identical localization on both limbs, so a + producer emitting both silently disagreed with itself and `title` won. It now + reads `label` only. Nothing an author could publish is affected: `@objectstack/spec` + REFUSES `title` inside a `sections[]` entry (`unrecognized_keys`, pinned by + objectstack#11902), and `@object-ui/types` plus the authoring inspector have only + ever declared `name` / `label` / `columns` / `fields`. The declared and authoring + faces were already converged; only three runtime producers lagged, and all three + move here: + + - `buildDefaultPageSchema`'s `deriveFieldGroupDetailSections` section literal; + - `RecordDetailView`'s re-map of that output through the per-object i18n + convention; + - `RecordDetailView`'s auto-grouped "More details" section, authored in + app-shell and unreachable from the synthesizer. + + **No rendered heading changes.** Every existing producer yields byte-identical + headings, asserted by rendering rather than by inspection — including the + "More details" bucket, which the earlier two-step scope would have degraded to + the literal `details` in every shipped locale while every existing guard stayed + green. A new pin (`RecordDetailView.sectionHeadingsRenderPath-6190.test.tsx`) + walks the tree app-shell actually renders and closes that blind spot. +- d18a0d3: Object-level metadata payloads no longer emit the three keys `ObjectSchema` refuses by + name — **group**, **sortOrder** and **relationships** (objectui#6223). + + Measured against the installed `@objectstack/spec` 17.2.0, whose `ObjectSchema` accept set + is 42 keys: + + ``` + const base = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } }; + + ObjectSchema.safeParse(base) => success = true (control) + ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control) + ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control) + + ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"] + ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"] + ObjectSchema.safeParse({ ...base, relationships: [ … ] }) => unrecognized_keys ["relationships"] + ``` + + The two controls are what make that a key-by-key result rather than a schema refusing + everything. Each key was resolved on its own, as the objectui#5761 family ruling requires: + + - **group** — the Object Manager's grouping is a UI-only display category. The spec has no + object-level grouping key (`fieldGroups` groups the fields *inside* one object), so the + grouping control and its column stay, and the value is now DERIVED from the spec key that + is accepted (`isSystem`) instead of round-tripped. `MetadataObjectsPage` also strips a + `group` already stored by an earlier build, because its save-back spreads the server + document verbatim and would otherwise keep re-sending it forever. + - **sortOrder** — what populated it was the array index the converter happened to be at, + i.e. the order the list was already in. The declaration is removed from the object + payload. The field-level `sortOrder` is a different key with a different card + (objectui#6045) and is untouched. + - **relationships** — the spec models relationships on the FIELD (`reference` / + `master_detail`, plus object-level `indexes`). The object payload stops declaring and + sending an object-level relationship array; what the designer should author for a + relationship is a data-model question this change does not settle. + + **Breaking for TypeScript consumers of `ObjectMetadataPayload`** (exported from app-shell): + the three properties are gone from the published type, so code that set them stops + compiling. That is the point — setting any of them produced a payload the metadata route + refuses. `ObjectDefinition` (the designer's UI model) is unchanged and still carries all + three. + + The parity gate built for objectui#5761 now has a **second oracle**: every shape in + `PAYLOAD_SHAPES` names the schema that judges it, `ObjectSchema` alongside `FieldSchema`, + and reach is resolved within an oracle rather than across one — `group` is a legal + `FieldSchema` key and a refused `ObjectSchema` key at the same time. That extension found a + fourth object-level key (`enabled`, objectui#6238) and a value-level rejection the key-name + check cannot see (`fields` sent as an array where the spec wants a map, objectui#6240); + both are filed and ledgered rather than fixed here. +- 622f33c: `MetadataService`'s two delete methods no longer PUT a hand-written tombstone. They call + the metadata API's own delete door instead, and the latent `enabled?: boolean` on + `ObjectMetadataPayload` is gone with it (objectui#6238). Object-level member of the + objectui#5761 family, surfaced by the `ObjectSchema` oracle objectui#6223 added to + `scripts/check-designer-field-key-parity.mjs`. + + **What the tombstone actually did.** `deleteObject` and `deleteMetadataItem` wrote + `{ name, enabled: false, _deleted: true }` through `client.meta.saveItem`, i.e. + `PUT /api/v1/meta/:type/:name`. Measured against the installed `@objectstack/spec` 17.2.0 + using `getMetadataTypeSchema` — the registry the framework's own `saveMetaItem` resolves a + PUT's validator from — across all 26 registered overlay schemas: + + ``` + ObjectSchema.safeParse({ name, label, fields }) => success = true (control) + ObjectSchema.safeParse({ name, label, fields, isSystem: true }) => success = true (control) + ObjectSchema.safeParse({ name, enabled: false, _deleted: true }) => unrecognized_keys ["enabled","_deleted"] + + 25 of 26 registered overlay schemas refuse `enabled` and/or `_deleted` BY NAME + 1 of 26 (`view`) tolerates them; 4 kinds have no registered schema at all + 0 of 26 strip them + ``` + + So there were two failure modes, not one. Where the type has a strict schema — `object` + among them — the delete was a hard `422 INVALID_METADATA`, so nothing was ever recorded. + Where the schema is tolerant or absent, the framework stores the request item **verbatim** + (it deliberately persists the body rather than `parsed.data`), and `_deleted` has no reader + anywhere on the platform — so the "soft delete" was a silent no-op that left the item live + carrying two junk keys. Neither outcome deleted or disabled anything. + + **The resolution is a mechanism change, not a rename**, and there was nothing to rename to. + `ObjectSchema`'s 42-key accept set has no on/off flag; the near-spelling `enable` is + `ObjectCapabilities`, a system-features *module object*, so `enabled: false` → `enable: + false` fails on the value where it passes on the name. No wire key was invented: a metadata + soft-delete convention would be a `@objectstack/spec` contract addition, and the platform + does not have one. + + **Both sites now call `client.meta.deleteItem(type, name)`** — `DELETE + /api/v1/meta/:type/:name`, the same request `MetadataClient.reset` issues, which is the + mechanism `MetadataObjectsPage.handleObjectsChange` and `ResourceEditPage` already used for + deletes. Two mechanisms for one operation had disagreed; now there is one. The delete route + is generic over `:type` on the same route family and capability gate as the PUT, so this + holds for every category the generic `deleteMetadataItem` serves, not just `object`. The + doc comment claiming the API "exposes `saveItem` but no dedicated `deleteItem`" was stale: + `@objectstack/client` 17.2.0 declares `meta.deleteItem` on the very client this service + already holds. + + `reset` semantics are the overlay's, and that is the governed answer rather than a + shortfall: it removes the customization row — which *is* deletion for an object the + designer authored — and restores the artifact for one a package declares, an object you are + not allowed to delete. Which of the two an item is, is what the API's own `deletable` / + `resettable` verdicts report, not something a client-side flag should decide. + + **No published type changed.** `ObjectMetadataPayload` is exported from its module but that + module is not re-exported by `packages/app-shell/src/index.ts`, the package's only entry, so + the removed `enabled?: boolean` was never on the published surface and no `**/src/index.ts` + is touched. What consumers *can* observe is behaviour: `MetadataService` is reachable + through the published `useMetadataService()` hook, both method signatures are unchanged + (`Promise<void>`), and the HTTP request they issue changes from a `PUT` with a body to a + `DELETE`. + + The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes + with the fix — that ledger ratchets in both directions, so an entry left behind for a + resolved key is as red as a missing one. It is now empty for the first time, which is the + ratchet arriving where it was pointed; the self-test's non-vacuity guard moved onto a + fixture accordingly, so an empty ledger reads as success rather than as a demand that some + key stay unresolved. +- 88b15fd: `ConditionBuilder` now takes a caller-supplied **subject vocabulary** instead of hardcoding a + record-scoped one (objectui#6296). + + The builder built every row subject as `record.` + field name, plus a fixed `record.id` / + `user.*` / `org.*` context list. That is correct for all five files that mount it today — six + mount sites, since `ActionDefaultInspector` mounts it twice — because every one of them is a + record-scoped site. It is wrong for a **flattened**-scoped site such as the flow designer's + entry condition, where the trigger record's fields *are* the top-level evaluation context + (bare `status`) and the prior values arrive as `previous.FIELD`. This repo's own + `flow-scope.ts` already computes that distinction (`fieldPrefix: onStart ? '' : 'record.'`, + `includePrevious`), and objectstack's `packages/formula/src/validate.ts` defines the two + scopes. + + A new optional `subjects` prop declares what a site actually binds: + + - `fieldPrefix` — defaults to `'record.'`; `''` declares a flattened scope. + - `includePrevious` — also offer `previous.FIELD` per field, plus the whole-record `previous` + token, which is what makes the create-path idiom `previous == null` **selectable** rather + than something the author has to recall from help text. + - `context` — replace the context subjects, so a flattened site does not inherit `record.id`, + a root it does not bind. Offering it there would make this editor emit the one spelling its + own sibling ref-check flags as out of scope. + + Declared, never inferred: the component does not guess a site's scope from the value it is + handed. **A caller that declares nothing gets exactly the previous behaviour** — the option + list, the `record.` prefix on a compiled row, and the single-quoted value spelling are all + pinned positively against that default, so changing it fails rather than re-baselines. + + **Double-quoted string literals now round-trip into row mode.** `unfmtValue` stripped only + single quotes while `fmtValue` re-emitted only single quotes, so `status == "done"` could + never survive the builder's byte-for-byte adoption check and was handed to the raw CEL editor + — even though double quotes are what the entry-condition placeholder teaches and what every + shipped example flow uses. Each row now remembers the quote character it was parsed with and + re-emits that one, so the author's own spelling is preserved rather than normalised, and the + byte-for-byte safety rule is kept exactly as it was rather than loosened. Rows built in the + builder still emit single quotes, unchanged. + + Measured against the shipped corpus — every start-node entry condition in objectstack's + example apps plus the HotCRM example from objectui#6226 — row-mode adoption goes from 3/17 to + 15/17. The two that remain on raw mode are `&&` mixed with a parenthesised `||` group: a + grammar limit of the row model, unrelated to subjects, and out of this card's scope. + + The component is not re-exported from the package index, so no external caller can pass the + new prop yet; the wiring that will (objectui#6226) is a separate card. Scored `minor` for the + added capability rather than `patch`, since the widening is real even while its only future + caller is in-repo. +- b362c1b: Setup gains a **Packaged automation** page — the operational surface for the flows an + installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other + framework-contributed Setup surface is: the page registers the component-registry ref + `automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it + at `/apps/<app>/component/automation/packaged`. No bespoke route is added — a second way + in would be a URL the app metadata does not know about. + + Per packaged flow the page does exactly two things: + + - **on/off for this scope** — reads the activation state the engine reports + (`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation` + ledger) and flips it through `POST /api/v1/automation/<name>/toggle`; + - **clone** — `POST /api/v1/automation/<name>/clone` with a mandatory new machine name and + label (§7.1). The carried-over definition is never offered as editable form fields; the + copy is edited in Studio like any other flow. + + Authoring stays in Studio. The list is scoped to packaged flows by the server's own + three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the + `_packageId`-only shortcut, which classifies a tenant overlay bound to a package as + packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an + install-wide switch. + + **Server refusals reach the operator verbatim** — no client-side softening or rewording. + Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose + message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard + (409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a + list nothing on the client could reconstruct), and the §7.1 clone name conflict (409). + + ⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no + base-moved notice, no link from a clone back to its source. Cloned-without-disabled and + disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including + the case where a response carries a `clonedFrom` key anyway — the platform does not track + that lineage, so a page that displayed it would be displaying something it invented. + + `@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real + translations in all nine other packs, matching each pack's existing vocabulary for "flow" + and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket, + fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and + each one's quotation convention. The group deliberately carries no drift or ancestry + wording, and no server refusal text: those arrive as server-authored prose and are + rendered verbatim. +- 9602dc8: Setup › **Packaged automation** gains a packaged **actions** section beside its flows one — + the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch + doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated: + 「动作 可能是需要开关的,因为有的 action 我不想启用。」 + + Per packaged action the section does exactly **one** thing: **on/off for this scope**. That + is all the `sys_metadata_activation` ledger knows about an action, and the section claims + nothing more. + + - **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic + data API list the object itself sanctions for exactly this + (`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer + 'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows + everything armed. + - **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded + `actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with + the object-embedded declaration winning a `<object>:<action>` key clash. Listing only the + first source would leave an administrator with no off-switch for a packaged standalone + action. + - **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly + the one key its body declares (`{ enabled }`); `global` is the object segment for an + object-less action, the same spelling both dispatch doors take. + + **⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3 + charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered + until real pull appears. A clone control here would advertise machinery that does not exist — + which is also why the server's §5 refusal for actions recommends the platform operator and an + ordinary sibling action where the flow refusal recommends a clone. + + **⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no + base-moved notice. The platform tracks no such lineage, so a surface showing it would be + showing something it had to invent. The absence is pinned against a response that smuggles + `clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire. + + Server refusals reach the operator **verbatim** — no client-side softening, no retry loop. + Three shapes are reachable in tests, each transcribed character-for-character from the + runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the + posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`, + naming the objects a machine name collides across — a list nothing on the client could + reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`). + + One further refusal is the section's own and it points the same way: a `hasMore` on the + ledger read is treated as a **load failure** rather than rendered. A dropped row reads as + "active", so a partial ledger would show a switched-off action as armed — the one direction + this section must not fail in. + + The flows section is unchanged in behaviour. It gains a heading beside the new one, and the + page subtitle now says "Flows and actions" — that string moved in all ten packs together. + Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real + translations; the two toggle-failure keys are artifact-neutral by wording and are reused + rather than duplicated. +- c71e14d: **The unresolvable-visibility-predicate report now names the roots of the tier the + predicate was actually evaluated against** (objectui#6487). An app-shell author + whose nav, area or field `visible` faulted was told to check `record` and + `page.<var>` — two roots that tier does not bind at all. + + `formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate` + (both exported from `@object-ui/react`) take a new **optional sixth argument**, a + `PredicateScopeTier` — also exported — selecting the closing advice paragraph. + Everything above that paragraph is unchanged on every surface, and so is every + verdict: this is diagnostics copy only. + + **The published signature grew; nothing existing breaks.** The argument defaults + to `'page-component'`, so a five-argument call keeps printing the bytes it + printed before. All three in-repo call sites pass their tier explicitly rather + than lean on that default. + + Each tier's root set was derived from the code that builds the bag, not from the + prose that described it: + + - **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item + predicates. Both bind `record`, `current_user` and `page.<var>` (the roots + `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is + byte-for-byte what it was. + - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` + runs, wired onto this reporter by objectui#6443. Its evaluator is built from + `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so + the line now names `current_user` with its three ADR-0068 alias spellings, + `app`, and `features` — the deployment-flag root that provider documents for + exactly this kind of predicate — and states outright that `record` and + `page.<var>` do not exist there. + + **Why not generalise the copy instead.** Dropping the concrete root names would + have made one paragraph true everywhere at the cost of making it useful nowhere: + an author who mistyped a root needs to know which roots exist *at their tier*, + which is the whole reason the paragraph is read. + + `data` is bound at the app-shell tier but is deliberately not advertised there — + every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it, + so naming it would point an author at a root that answers nothing. +- 2c26335: **BREAKING (in name only):** `MetadataService.saveObject(obj, existingFields)` now requires + its second argument (objectui#6490). Calls that omitted it no longer compile. + + **Runtime behaviour is unchanged, and that is the whole justification.** A call that omitted + the field list was already a guaranteed `422` — every time it ran, against every backend. + `ObjectSchema.fields` is not merely typed, it is REQUIRED: measured against the installed + `@objectstack/spec` 17.2.0, `ObjectSchema.safeParse({ name: 'account', label: 'Account' })` + fails with `invalid_type @ fields`, and `metadata-protocol`'s `saveMetaItem` parses the whole + item against that same schema and throws `422 INVALID_METADATA` **before** it persists. The + method cannot build a valid document without the argument, so the only calls this break + breaks are calls that already failed. The signature is now honest about it, and the diagnosis + moves from a round trip at runtime to the compiler. + + Nothing new is exported and nothing new is accepted — this narrows the published surface + rather than widening it. In-repo production call sites were measured at **zero** (only tests + called it), so the migration for an external consumer is to pass the field list it was + already required to send: `saveObject(obj, fields)`. + + ⛔ Two readings were considered and declined, recorded so neither is taken later as a + shortcut. **Not a `{}` default** — `{}` parses GREEN and `PUT /api/v1/meta/object/:name` is + an upsert, so defaulting would delete every field of the object on a save that only meant to + rename it, trading a loud, harmless 422 for silent data loss; the anti-wipe control from + objectui#6240 (`omits fields entirely when the caller supplied none — it does NOT write {}`) + moves with the signature and still guards the path a JavaScript consumer can reach. **Not + fetch-and-merge** — an object save could GET the current document and preserve its stored + `fields` the way `saveFields` does, but that builds capability for a path with zero measured + pull and makes the parameter redundant. + + An EMPTY list stays a different statement from a missing one: `[]` means "this object has no + fields", writes `{}`, and under the upsert performs the wipe the caller asked for — the same + authoritative reading `saveFields` gives its own empty list. Unchanged, and now pinned, + because the required parameter is what routes a caller with nothing to hand toward it. + + Scored `minor` and not `major` per AGENTS.md §版本号策略 — objectui's major is pinned to the + `@objectstack` major so that "same major ⇒ compatible" holds across the two repos, and every + publishable package sits in one `fixed` group, so objectui's own breaking changes ship as + `minor` with the break spelled out in the body. That is the convention, which is why the + break is stated in words above. +- 08c3da9: ⚠️ **Behaviour change: `current_user` and `features` gates on an object field's + `visible` that have been doing nothing on the record form page and in the + console's record modal now TAKE EFFECT.** Read this before upgrading if any of + your object metadata gates a field on the session user or on a deployment flag. + + objectui#6010 and objectui#6110 bound the host predicate scope on the form + renderer and on the console form routes. `evaluateVisibility` was still being + reached with a THIRD and FOURTH evaluator that neither of those touched: + `RecordFormPage` and `AppContent` each built a private + `new ExpressionEvaluator({ user, app, data })` for the field-visibility filter, + beside — not from — the `ExpressionProvider` each of them mounts. Those bags + bound `user`, but not the canonical `current_user` nor the ADR-0068 `ctx.user` / + `os.user` spellings of that same object, and not `features` at all. So one + authored predicate meant two different things depending on which evaluator + reached it: `current_user` resolved on a nav item and was unbound on a field. + Both sites now build their scope with the same `buildExpressionScope` the + provider uses, which is the only declaration of what an app-shell predicate may + name. + + **Why nobody noticed, and why the fix is felt as a change.** A field `visible` + predicate fails OPEN: a field on screen is what you get when the predicate says + TRUE, when the root was never bound so the predicate faulted, *and* when the + predicate has a typo. Those worlds are indistinguishable, so an app that + authored a `current_user` gate saw the field render and had no way to tell the + rule was inert. After this change the predicate is evaluated for real, and + **fields that have always been visible will disappear for the users the rule + excludes** — and a `features` gate whose flag is off will hide its field once + `/api/v1/auth/config` resolves. + + `AppContent`'s bag also hand-rolled its user as `{ name, email, role }`, without + `positions`. It now uses the same `buildExpressionUser` normaliser every other + console surface publishes, so `'sales' in current_user.positions` — the gate the + server enforces on write — reaches the same verdict client-side instead of + faulting open. + + **Before upgrading**, audit any `visible` predicate in your object metadata that + names `current_user` (or `user` / `ctx.user` / `os.user`) or `features`, and + confirm each says what you actually want evaluated. Measured on the metadata + shipped in this repo and in the framework at the time of the change: **nothing + in it authors such a gate**, so no shipped surface changes behaviour today — + the audit is for your own object metadata, which this cannot see. + + **The error path is deliberately unchanged.** A predicate that throws still + fails open, exactly as objectui#6443 / objectui#6487 left it. This change is + about which roots are BOUND, not about what happens when evaluation fails. +- 6d68491: metadata-admin inspectors: name the "inactive values retained" state instead of + rendering it as live configuration (objectui#6499). + + `showWhen` gates rendering only, and `isFieldVisible` additionally re-shows any + field that already holds a stored value — deliberately, "so existing config is + never hidden". The consequence on screen: an author who enabled a controller, + filled its dependent fields, then switched the controller back off keeps seeing + those fields as ordinary, live-looking controls. The stored config and the + switch beside it disagree, and nothing said which one was in effect. + + Per the maintainer ruling of 2026-08-27 (Option C), the values are KEPT and the + state is made explicit. Pruning on save was rejected: it silently discards + config an author entered, and inverts the very rule that stops config from + vanishing unseen. + + - New `inactiveRetainedKind(field, node, fields)` in `flow-node-config.ts` — a + pure read that reports a field rendered ONLY because the stored-value re-show + rule fired. It distinguishes `'controller-off'` (a real toggle the author can + switch back on) from `'no-controller'` (the `__legacy__` render-only keys, + where no such toggle exists and saying otherwise would be a fresh lie). + - `FlowNodeConfigField` renders the notice beside the affected control, with a + "Clear value" action so the author can discard the residue **deliberately**. + Read-only inspectors show the notice without the action. + - Coverage is every `showWhen` group in the inspectors, pinned mechanically: + all 33 gated fields across the descriptor tables, plus the two runtime + producers (an engine-published `configSchema` and a connector input schema) + that mint groups no source file contains. + + Render-layer only: no save-path change, no data deletion, and `isFieldVisible`'s + stored-value re-show rule is unchanged — clearing is an ordinary author-initiated + field commit, the same write as emptying the control by hand. +- 5af67c2: Object-field visibility on the record form page and the console's global + record-form modal now reads the keys `@objectstack/spec` DECLARES — the static + `hidden` and the `visibleWhen` predicate — instead of `visible`, a key the + contract refuses (objectui#6514; maintainer ruling 2026-08-27, Option A). + + `FieldSchema` is a `strictObject` and `visible` is not one of its keys: it + appears in `FIELD_KEY_GUIDANCE` as prose that REFUSES the spelling, deliberately + not as an alias, because — quoting the guidance — "this surface declares BOTH + forms and the two answers have opposite polarity". So both call sites were + gating on something no author could legally write: metadata carrying a + field-level `visible` never survives validation, and a census re-run for this + card over the framework's 113 `*.object.*` files (`aef1b7e64`) found zero of + them, against 113 `label` / 107 `required` / 76 `readonly` field-level hits from + the same query shape. + + What changes for an author: `hidden: true` on a field now actually withholds it + from these two forms, and a `visibleWhen` predicate now decides there — both + previously inert. The dead `visible` read is deleted rather than kept beside the + declared keys, so the refused spelling cannot survive as a second, renderer-side + contract. + + `hidden` is INVERTED relative to the key it replaces (`visible: false` is + `hidden: true`), and an inversion read backwards raises no error — a field + either vanishes with no diagnostic or leaks to a principal it was hidden from. + Both directions are therefore pinned per call site (hidden/shown, predicate + false/true), and the suites were measured red against the pre-fix code AND + against a deliberately inverted implementation. + + The two keys compose as AND: `visibleWhen` is documented as "shown only when + TRUE (else hidden)", a necessary condition and never a licence to un-hide a + statically hidden field — the same shape `resolveFieldRuleState` already uses + for `readonly`/`readonlyWhen` and `required`/`requiredWhen`. + + Field-level visibility keeps the contract's current scope: no `current_user` + binding is added, and the tier's documented fault-open is unchanged. Per-user + field hiding still goes through the option/form layers that bind the user. +- a4d39a8: **Breaking (compile-time only):** `buildExpressionUser`'s PARAMETER is now the session + contract — `ExpressionUserSession | null | undefined` instead of `unknown` — so every call + site is checked against it (objectui#6559). + + objectui#6551 narrowed the CAST the normaliser read its input through, so the module began + DECLARING what a signed-in session is: `id`, `name` and `email` required, mirroring + `@object-ui/auth`'s `AuthUser`. The parameter behind that cast stayed `unknown`, so the + declaration bound nothing — every call site satisfied it vacuously and + `buildExpressionUser({ name: 'B', email: 'b@c.d' })` still compiled. A declaration nothing + checks is indistinguishable from no declaration at all (AGENTS.md #0.1). The cast is gone + and the shape is stated once, on the parameter, so the declaration and the check are the + same statement rather than two that merely agree. + + WHAT BREAKS, AND FOR WHOM. This tightens a signature published from the package entry + (`packages/app-shell/src/index.ts`), so an external caller that passes an unchecked or + under-declared value stops compiling on upgrade. That is accepted (maintainer ruling + 2026-08-27, option A): the only calls it refuses are calls that were never conformant with + the contract the module already declared. It ships as `minor`, not `major` — objectui's + major tracks `@objectstack`'s, so its own breaking changes ship as a minor with the break + written down (`scripts/check-changeset-no-major.mjs`). ⛔ Keeping `unknown` and ⛔ adding a + second, wider entry point were both declined. + + NO RUNTIME BEHAVIOUR MOVES. All four in-repo production call sites pass `useAuth().user`, + typed `AuthUser | null`, and type cleanly unchanged — two in `console/AppContent.tsx`, one + in `views/RecordFormPage.tsx`, one in `apps/console`'s `InternalFormRoute.tsx`. The body is + byte-equivalent: the same keys, the same `??` defaults, the same anonymous branch. ⛔ No + consumer-side fallback was added; `id: u.id ?? null` remains the rejected shape (triage + ruling 2026-08-26), because a lenient default in the consumer is what AGENTS.md #0.1 + forbids and it silently equates "signed in, no id" with "signed out". + + Note for callers holding the SPEC's `AuthUser` rather than `@object-ui/auth`'s: the spec + type is an `interface` with no index signature, and TypeScript infers an implicit index + signature for type aliases only, so it is not assignable to a contract declaring + `[key: string]: unknown`. `@object-ui/auth`'s `AuthUser` extends it and adds that index + signature, which is what every call site here passes. + + Pinned by the new `expressionUser.parameterContract.types.test.ts`, compiled by the + package's `tsconfig.test.json` (chained off `type-check`, which CI runs). Its refusals are + `@ts-expect-error` directives, so a re-widened parameter makes them UNUSED and TS2578 turns + the type-check red; a type equation on `Parameters<typeof buildExpressionUser>[0]` reds + alongside them. Every refusal is routed through a non-fresh value, so what is measured is + the parameter and not excess-property freshness. +- dc4365c: Retire the discovery-wire preview mode — the console no longer turns + authentication off because a server said `mode: 'preview'` (objectui#6654). + + `@objectstack/spec` retired the `RuntimeMode` value `'preview'` and the whole + `PreviewModeConfig` block (objectstack#11846). This console still read that + surface back off the runtime discovery payload, which is a different layer from + the retired compile-time type — so the consumption could not simply be assumed + dead, and its removal was ruled deliberately (2026-08-29). + + - `ConditionalAuthWrapper` (`@object-ui/app-shell`) drops the branch gated on + `discovery.mode === 'preview'`. That branch called `setAuthEnabled(false)` and + simulated an identity out of `discovery.previewMode`, every field behind a + default. Auth availability is now decided **only** by the ADR-0076 D12 service + reading (`isServiceUsable(discovery.services.auth)`), exactly as for any other + mode. + - `DiscoveryInfo` (`@object-ui/react`) drops the `previewMode` block and stops + documenting `'preview'` as a runtime mode; the package README's discovery + section is updated to match. + + **Accepted failure direction:** a deployment that still emits `mode: 'preview'` + or a `previewMode` block now falls back to the ordinary auth reading — it + requires login. That is loud, diagnosable and more secure than keeping a dormant + auth-off path keyed on a spelling the platform no longer produces. + + **Not affected:** `AuthProvider`'s `previewMode` prop, `useAuth().previewMode` + and `PreviewBanner` in `@object-ui/auth` are a separate published capability + with a different producer (a host passing the prop). Only the discovery-wire + producer of that prop is retired; hosts that pass it explicitly are unchanged. +- 969ba84: Renderers for the `app:launcher` and `nav:menu` page blocks (objectui#6661). + Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two + `PageComponentType` members that are purely metadata-driven, so nothing had to + ship before their renderers could. Phase 2 (`global:search` / + `global:notifications`) landed in objectui#6757 and set the pattern this + follows. + + A page that declared either member drew a dashed box. The two symptoms were not + the same, which is worth recording because it decides what "fixed" looks like + for each: + + - `nav:menu` is in `PALETTE_PLACEHOLDER_BLOCKS`, registered eagerly, so it drew + the literal "Component Placeholder" scaffold in every host. + - `app:launcher` is only in `PROTOCOL_COMPONENTS`, registered when a host opts + in via `registerPlaceholders()` — which just `apps/console` does. So it drew + the scaffold in the console and `SchemaRenderer`'s red OBJUI-001 "Unknown + component type" panel everywhere else. + + Neither block adds a data layer — each mounts plumbing that was already live, + and neither issues a request or touches an adapter: + + - `app:launcher` reads the metadata app registry (`useMetadata().apps`, which + `MetadataProvider` fetches eagerly) through the shared `filterActiveApps` + predicate, and draws it with `HomeAppsStrip` — the console's own launcher + grid — so an authored launcher and the Home launcher cannot drift into two + looks for one thing. + - `nav:menu` reads the active app's navigation tree from that same registry and + renders it as page content, taking every derived fact from `@object-ui/layout`: + hrefs from `resolveHref`, labels from `resolveNavItemLabel`, the active row + from `resolveActiveNavItem`, and the item-level guards (`visible`, + `requiredPermissions`, `requiresObject` / `requiresService`) in the order + `NavigationItemRenderer` applies them, wired to the same console providers + `AppSidebar` wires them to. `action` items dispatch through + `useNavActionDispatch`, so framework#4509's "renders but dead-clicks" shape is + not reintroduced. + + `nav:menu` does not mount `NavigationRenderer` itself: that renders through + `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's + `SidebarProvider`, and a page block has to render standalone. `@object-ui/layout` + therefore exports `resolveNavItemLabel`, which was module-private — an additive + export with no behaviour change, so the sidebar and an authored menu cannot show + one nav entry under two names. + + Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an empty + shape for each, and both use `skipFallback: true` so neither claims the bare + `launcher` / `menu` keys. This does not change the Studio page palette — + `app:launcher` remains recorded there as a shell singleton, which is a palette + decision independent of whether a declared type renders. + + Three new strings — the launcher's and the menu's accessible names, and the + menu's empty state — are declared under `console.nav` in `en.ts` and its nine + sibling packs. An inline `defaultValue` alone is not a fix: it renders English + at one call site and leaves the string untranslatable everywhere + (objectui#3517). +- faac0d9: `@object-ui/app-shell` now publishes a precise `sideEffects` ARRAY. + + **What this means for a consumer.** Until now the package declared no + `sideEffects` field at all, which every bundler reads as "assume every module in + this package does something when it is imported" — so nothing in the package + could be tree-shaken, and importing one named export from the barrel pulled in + the barrel's whole reachable graph. The package now names exactly the modules + that DO something on import: its entry forms (including `./styles.css`, which a + bundler must never drop) and the ten modules that register SDUI widgets, admin + components and metadata resources at load time. Everything else is now + shakeable, so a consumer's bundler may drop the parts of `@object-ui/app-shell` + their app does not use. + + ⚠️ **If your build depends on a module of this package being evaluated for its + side effects without importing anything from it, and that module is not one of + the ten named**, it may now be dropped from your bundle. Import the value you + need by name, or call the registration explicitly. Measured on this repo's own + console: 56,668 gzipped bytes left the eager closure and every SDUI registration + stayed present. + + `"sideEffects": false` was NOT adopted and remains disproven by measurement: it + drops three live SDUI widget registrations (`mcp:connect-agent`, + `cloud:onboarding-next`, `cloud:ai-model-status`) to zero chunks on a green + build with no warning anywhere. + + Two gates ship with the array, because an INCOMPLETE array fails silently inside + a consumer's bundle and would otherwise have no witness: + `scripts/check-side-effects-array.mjs` re-derives the enumeration from the module + bodies and fails when the array and the derivation disagree in either direction, + and `scripts/check-sdui-registration-pins.mjs` weighs the built console for every + registration the array promises to keep. +- dbd5194: The shell's `sys_activity.type` reading stops calling every unrecognised type an + update (objectui#6730). + + `mapActivityRows` in `hooks/sharedUserFeeds.ts` — the feed behind the AppHeader + bell's Activity tab, Home's activity card and the exported `ActivityFeed` panel + — carried the third hand-written reading of that column in this repo, and it + bucketed every value outside `created` / `deleted` / `commented` / `mentioned` + as `update`. That is not a missing decision; it is a wrong one stated out loud: + a `scheduled` meeting, a `login`, a nightly `system` rollup and an author's + `contract_countersigned` all rendered as "somebody updated this record". + + - New `layout/activityItemType.ts` holds the whole reading — the table, the + generic bucket, the `"NOW()"` timestamp fallback and the row constructor that + applies all three — DOM-free, so what a row becomes is assertable directly. + - `ActivityItem['type']` gains a fifth kind, `system`: the generic bucket, with + its own icon, label and notification toggle. Following + `UNMAPPED_ACTIVITY_FEED_TYPE`'s precedent, an unrecognised value renders + through it and is named once on `console.warn` rather than being dropped — + `sys_activity.type` is author-extensible (objectstack#11507 direction 4), so + an unmapped value is real activity nobody has ruled on, not a mistake. + - The built-ins that had no honest presentation among the four existing kinds — + `system`, `completed`, `scheduled`, `login`, `logout` — now land in that + bucket instead of claiming `update`. `assigned` and `shared` stay `update`: + both write to the record. + + ⛔ The two readings of this column are deliberately NOT converged. + `activityRowToFeedItem` builds a `FeedItem`, and the vocabularies cross: + `FeedItem` collapses create/update/delete into one `field_change` and drops + `commented` / `mentioned` outright, so routing this surface through it would + cost the bell every comment row and every create/delete distinction. What is + shared is a pin, not an import — the new suite reads plugin-detail's real table + (a devDependency; no runtime edge) and fails when the declared vocabulary grows + an entry this side has not read, or when the two readings stop disagreeing in + the three measured ways. +- f99932a: Renderers for the `global:search` and `global:notifications` page blocks + (objectui#6757). A page that declared either member drew the literal "Component + Placeholder" scaffold: both are first-class `PageComponentType` members that the + 2026-08-26 maintainer ruling on objectstack#12183 kept declared once the + readiness read in objectstack#13117 evidenced both data sources shipped, and + the renderer was the remaining half. + + Neither block adds a data layer — each is a new mount point on plumbing that was + already live: + + - `global:search` mounts `useRecordSearch` (`@object-ui/react`), the same hook + the ⌘K command palette and the full-page search results already use. It + prefers the adapter's `searchAll` (`GET /api/v1/search` — cross-object hits + with title/snippet/record) and inherits that hook's fanout fallback for + adapters without it. Scope is the metadata provider's searchable object set, + which is the hook's documented default when no `objectNames` whitelist is + given. + - `global:notifications` mounts `InboxPopover` — the bell ADR-0012/ADR-0030 + defines ("the bell reads `sys_inbox_message`") — over the shared inbox feed. + + To keep that second one honest, the header bell's inbox wiring (rows, badge + addends, and the three mark-read paths) moves out of `AppHeader` into a new + `useInboxBell` hook that both surfaces mount. Copying it would have re-opened + the two defects `sharedUserFeeds` closed — #4225 (two owners of one read issuing + it twice per page) and #4316 (two derivations of read-state disagreeing) — so a + bell in the header and a bell an author declared on a page now have no + representable state in which they disagree about a row. + + Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an + empty shape for each ("declares no props at all" is the recorded intent), and + both use `skipFallback: true` so neither claims the bare `search` / + `notifications` keys. This does not change the Studio page palette — + `global:notifications` remains recorded there as a shell singleton, which is a + palette decision independent of whether a declared type renders. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 2a7ac32: Calendar views no longer render on invented field names (objectui#7029; ruled on + objectstack#13748, director batch #19, option A). + + A view that carried no `calendar:` block used to have a complete-looking calendar + configuration synthesized for it. `ObjectCalendar` has always decided whether it + has a usable configuration by asking whether a start-date binding is PRESENT, so + the fabrication short-circuited its own refusal screen — "Calendar configuration + required. Please specify startDateField and titleField." — which existed all + along and was simply unreachable. Measured on a leave-request object whose real + fields are `start_date` / `end_date`: every record piled onto today's cell under + titles resolved through the display-name chain. A plausible, fully wrong screen, + with zero signal to the author. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/ObjectView` emitted `startDateField: 'due_date'` and + `titleField: 'name'` into `options.calendar` for every object view; + - `plugin-list/ListView`'s calendar branch floored the same two bindings at + `'start_date'` / `'end_date'` one layer down; + - `plugin-view/ObjectView.generateViewSchema` — the authored `object-view` + element route, which bypasses `ListView` entirely — carried its own copy. + + All three now forward only what the author declared. This converges the calendar + on the shape its siblings already had: `timelineViewOptions` (objectui#3129 + retired this very literal from the timeline axis), the kanban lane detector + (ADR-0085, "never invents a field the object doesn't have"), and + `defaultCalendarFromObject` (a binding, or nothing). + + **Behaviour change, loud over silent.** With no binding to forward, ADR-0047's + capability gate stops offering the Calendar toggle to views that configured + none, and a view forced onto the calendar renderer reaches the refusal screen + instead of a wrong one. A view that happened to sit on an object carrying a real + `due_date` field was rendering by luck; it now refuses until its `calendar:` + block is written. Correctly configured calendars are unaffected — same fields, + same render. The same deletion also stops the fabricated name from answering for + the Timeline switcher, which accepts a calendar binding as a legitimate axis. + + The spec half — cross-field validation rejecting a half-written declaration at + authoring time — is objectstack#13817. This half makes the runtime honest + independent of which spec version the host pins. +- 5f4514f: Gantt views no longer render on invented date field names (objectui#7070). + + The half PR #7062 fenced out and reported separately. A view that carried no + `gantt:` block used to have a complete-looking date axis synthesized for it: + all three faces floored `startDateField` at `'start_date'` and `endDateField` + at `'end_date'` — field names no view had written and most objects do not + carry. + + `ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props + are present, so the fabricated pair short-circuited the renderer's own refusal + screen — "Gantt configuration required. Please specify startDateField, + endDateField, and titleField." — which existed all along and was simply + unreachable from every route. The same fabrication answered ADR-0047's + capability gate in `ListView.availableViews`, so the Gantt toggle was live on + every object view in the product. + + ⚠️ The premise was MEASURED before anything was deleted, because #7029's + mechanic is only correct where a refusal path exists and that had never been + established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an + absent binding — it does not render empty, and it does not throw. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/src/views/ObjectView.tsx` — the console object page. The inline + branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and + `timelineViewOptions`: the declared block spread whole, title floored at + `'name'`, no date field invented. + - `plugin-list/src/ListView.tsx` — the render branch AND the capability gate. + - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored + `object-view` element route, which bypasses `ListView` entirely. + + **What changes for an author.** A view that declared no gantt configuration is + no longer offered the Gantt toggle, and one forced onto the renderer reaches + the refusal screen instead of a plausible, fully wrong chart. A view that + declared a binding is unaffected — the declared block is forwarded exactly as + before, every spec key included. + + Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx` + certified the gantt branch below it as already using the safe two-rung shape. + It did not. The note now states each sibling branch as measured, and says + explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the + two plugin faces, which objectui#7070 routes to a ruling rather than settling + per-face. + + Deliberately out of scope, and left in place: `progressField` / `dependenciesField` + (not date axes, different absent-value semantics) and the timeline `'created_at'` + posture conflict. +- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one + spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). + + **BREAKING, deliberately, with no deprecation window.** + + Two published contracts declared OPPOSITE dialects for the same four lookup keys, and + `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object + contract *refuses* outranked the one it *declares*: + + | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) | + |---|---|---| + | camelCase | **declared** | compile error (`TS2561`) | + | snake_case | refused (`unrecognized_keys`) | **declared** | + + `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the + snake members are **removed**: + + | before (removed) | after | + |---|---| + | `display_field` | `displayField` | + | `description_field` | `descriptionField` | + | `lookup_filters` | `lookupFilters` | + | `id_field` | `idField` | + + **Migration.** Rename those four keys wherever you author lookup or user field metadata + — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema` + that returns them. The old spellings are no longer read: a def still carrying + `display_field` falls back to the referenced record's generic name heuristic rather than + the field you named. + + `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name + hydration — committing a record field other than the id as the lookup's stored value — + which is picker behaviour with no `FieldSchema` twin, and none owed. + + **Not renamed** (outside this ruling's four keys, still snake on the widget bag): + `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`, + `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's + `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def. + + Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose + snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the + inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s + `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict + class; its gate is restated to assert the class no longer exists rather than passing + vacuously. +- 1cca678: Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, + maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). + + **What changes for an operator.** A stored dashboard whose header came from a legacy + root `title` now shows its `label`. `label` is the only header source, then the raw + `name`. + + Per surface: + + - Console dashboard page (`DashboardView`) — header falls to `label`, then `name`. + - Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document + with no `label` now shows no header title at all. + - The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to + `label`, then the generic `Dashboard`. + - Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage` + heading) — both fall to `label`, then `name` / the generic heading. + + **Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name** + (`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no + authored dashboard can acquire the key, and what retires is compatibility with documents + stored before that refusal existed. Until now five surfaces read the legacy spelling + independently, which meant a legacy document could show one header in the console and a + different one in the designer. One spelling now answers everywhere. + + **Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard + already carries it and needs no change — it simply starts showing that `label` instead of + the legacy `title`. A document carrying `title` and no `label` was already invalid; give + it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found + 9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'` + component examples that declare no `header`, so none of them rendered a header title + either before or after. + + **Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared** + key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings, + the designer's widget-title input and its per-locale write path all behave exactly as + before. Root and widget arms were separated by receiver, and the retirement's pins carry + widget-level controls on every surface for that reason. +- d80447d: Stop inventing a gallery cover binding, and offer the view switcher only when it + can switch (objectui#7547). + + **The object page no longer floors `gallery.imageField` at `'image'`.** It used + to supply that key for every object view, declared or not. The screen damage was + small — `ObjectGallery` collapses the cover area when no record yields a cover — + but the key fed `ListView`'s ADR-0047 capability gate, which reads + `options.gallery.imageField`, so **Gallery was offered on every object view that + whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now + forwards the view's own declared block (both legacy cover spellings still + cross-fill each other, and `titleField` keeps its `'name'` display floor) and + emits no cover key when the view declared none. Same class and same route as + objectui#7029 (calendar) and objectui#7070 (gantt dates). + + ⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery + toggle. That is the ADR-0047 rule working: it was only ever offered because this + relay answered the gate on the author's behalf. + + **The visualization switcher is drawn for the resolved list, not the whitelist.** + `showViewSwitcher` was computed from the LENGTH of + `appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects + it with the capability gate — so a view whitelisting `['grid', 'timeline']` with + no timeline block drew switcher chrome around a single Grid entry. The predicate + now lives at the one site that holds both halves. Views whose whitelisted types + all resolve are unaffected. +- 91f9276: **The maker's edit-mode starter offers sample data, not an automation v1 cannot + build (objectui#7709).** Bound to an existing app (`?package=`), the maker's + empty state offered four starters: add a field, add an object, add a dashboard, + and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and + notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the + measured behaviour on the sibling chips was not a refusal but a silent degrade + into a view — so the product recommended an automation and would have handed + back a page. + + Rewording it was not available: asking for a field, a view or a dashboard + duplicates one of the three chips beside it. The fourth chip is now + `addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all + ten packs and in the call-site `defaultValue` fallback, which is a second copy + of the same string. The three surviving chips all add STRUCTURE; what an app + that already has objects most often lacks is DATA, and `seed` is on v1's + authoring whitelist. A note beside the keys in every pack and at the call site + says this chip's automation wording comes back when ADR-0112 v2 re-adds flows + and actions, and the retired sentence for each pack is kept in the guard suite + so v2 has it verbatim. +- bdb4dbd: A half-filled relationship field stays in the client and is never PUT (objectui#7714, + maintainer ruling on objectui#7122 item 4, 2026-09-05). + + **Breaking, deliberately, and stated rather than implied.** Both metadata writers — + `MetadataService.saveFields` / `saveObject` in `@object-ui/app-shell`, and + `MetadataFieldsPage` in `@object-ui/plugin-designer` — now REFUSE a `lookup` or + `master_detail` field whose `reference` is missing, empty, blank or not a string. A + caller that previously got a PUT now gets a thrown error and **no request at all**. The + refusal is raised while the wire `fields` map is being built, so nothing is sent and + nothing is partially applied. + + **Why the draft may not leave the client.** `@objectstack/spec` 17.3.0 turned + `reference` from prose into a hard requirement on relationship types (a `custom` + refinement at path `reference`). Driven against a real 17.3.0 backend in a running + designer: creating a `lookup` and leaving its target empty PUT the whole object, + came back `422 INVALID_METADATA` at `fields.<name>.reference` — and then the NEXT + edit, to a different and already-saved field, was refused identically, because the + half-filled draft rides along inside the same document. The author sees that later + edit rendered as applied while the server has none of it, and the only escape that + does not require noticing the lookup is a reload, which discards the work. An editing + session's half-finished state belongs to the client, not to the metadata store. + + ⛔ **Not** "strip the incomplete field from the body and report a successful save". + That shows the author a field the server never received — the silent-drop shape + objectstack#4001 closed. + + ⚠️ **A declared divergence: objectui is stricter than the contract here.** The + predicate is `typeof reference === 'string' && reference.trim() !== ''`. Measured on + the 17.3.0 artifact, at field level and again through `ObjectSchema`, the spec + ACCEPTS a whitespace-only `reference` while refusing an absent or empty one — so + `reference: ' '` is refused by these writers on their own authority, not the + platform's. Kept deliberately: whitespace names no object (the spec's own + `ObjectSchema.fields` key grammar `/^[a-z_][a-z0-9_]*$/` admits no whitespace-bearing + name), so admitting it only moves the identical failure past the PUT and into a stored + document, where it surfaces with no field named. Filed upstream as objectstack#16126; + if the spec trims, behaviour here is unchanged and only the declaration retires. + + The refusal message diagnoses which of the four states it found — absent, empty, + non-string (`invalid_type`, a value of the wrong kind rather than a missing target), + or blank — because the repair and the consequence differ per state. + + `minor` rather than `major` per `AGENTS.md`: objectui's major tracks `@objectstack`'s, + so objectui's own breaking changes ship as `minor` with the breaking semantics stated. +- a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered" + (objectui#7741). + + `ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the + import wizard hides its saved-mapping selector on an empty list. So "the server served + zero mappings" and "the server refused, or broke" produced the identical UI on every + deployment — the feature simply absent — with a `console.warn` as the only + discriminator, in the browser console, with nothing pointing at it. That silence did not + merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter + (objectstack#14026 was filed, routed and worked by two seats against a wizard that had + been correct since `@object-ui/data-objectstack@17.1.0`). + + **The empty-list return is unchanged.** `listImportMappings` still answers + `Promise<any[]>` and still never throws, on every arm including the loud ones — this is + a channel added ALONGSIDE that contract, not a change to it. + + - **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe + channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a + metadata read failed in a way that is NOT the supported "this deployment does not + serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the + object, whether the server `refused` this caller or the answer was `unreadable`, and + the server's own ADR-0112 code, HTTP status and message. + - **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported + so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code` + first, the status only where no code was declared — and never "is the result an empty + array", which is what both conditions produce and so can never tell them apart. + - **The older-server case stays quiet.** A deployment that does not serve the `mapping` + kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list + door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no + event. That is a real, supported deployment shape and it must not become a visible + fault. + - **The console now says so.** `AdapterProvider` subscribes to the new channel and + renders a warning toast naming the object, the remedy and the server's own words, so a + user without devtools open can tell "there are none" from "we could not find out". + Three new `console.importMappings*` keys ship in all ten locale packs. + + This applies framework #13906 decision 1 option A — *a thing that could not be READ is + not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not + a new principle. +- 151facf: **Breaking for the page designer's output:** the page-block inspector's `object-kanban` + panel now authors `groupBy` (the key the board reads) instead of `groupField` (the key it + never read), and offers the `limit` control the schema has declared since objectui#7322. + + **What was measured, on this branch's base (`0c8dbc49`).** `BLOCK_CONFIG['object-kanban']` + offered exactly four controls — `objectName` / `groupField` / `titleField` / `cardFields`. + `ObjectKanbanSchema.groupBy` is REQUIRED on both published faces (`objectql.ts:2765 + groupBy: string`, no `?`; `objectql.zod.ts:1001 groupBy: z.string()`, no `.optional()`) and + had **no control at all**, while `groupField` — the only control able to set grouping — has + been a `retirementTombstone()` on the Zod face and `?: never` on the TS face since + objectui#7322. `ObjectKanban.tsx` reads `schema.groupBy` at thirteen sites and `groupField` + at zero (control: those same thirteen hits in the one query, so the zero is a reading). + + Run against the node those four controls produced, `ObjectKanbanSchema.safeParse` reported + **two** issues, not one: `groupBy` "expected string, received undefined", and `groupField` + carrying the retirement guidance verbatim. So this panel could not author a valid + `object-kanban` node however it was filled in, and the board it produced grouped nothing + with no diagnostic on any face. Both halves are now pinned against the schema itself rather + than against a spelling, in `previews/__tests__/block-config.test.ts` — + `scripts/check-designer-field-key-parity.mjs` judges `PAYLOAD_SHAPES` and does not read + `BLOCK_CONFIG`, so nothing mechanical was watching this table. + + `limit` is not polish: `ObjectKanban.tsx` sends it as a real `$top` + (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and renders every fetched record into a lane + with no pagination, so a board over 100 records was silently truncated with no way to widen + it. The new number box states `100` — `DEFAULT_KANBAN_LIMIT`, read from the renderer's own + source by its pin — as the value that applies while it is empty. + + **Stored pages.** Documents saved by every released build since + `@object-ui/app-shell@17.5.0` can carry `properties.groupField` on an `object-kanban` block. + The inspector's read door now strips it (`stripRetiredBlockProps`, keyed to + `RETIRED_BLOCK_PROP_KEYS`), so an edit-and-save round-trip comes out without the retired + key — no migration pass, and no blanket unknown-key purge: every other key the designer + does not render still survives. It is a strip and **not** a migration into `groupBy` on + purpose: reading the old spelling into the new box would be a second de-facto alias + (AGENTS.md #0.1) for a value no board ever acted on. + + The **view-level** `kanban.groupField` alias (`core/src/utils/normalize-list-view.ts`) is + live and untouched — node-level retirement and the view-level alias are different things. + + The two locale entries move with the control (`engine.inspector.pageBlock.field.object-kanban.groupBy`, + plus a new `.limit`); the English wording is unchanged, so the panel reads the same as it + always did. +- 31a0d05: Emit the spec-canonical kanban lane key from the object page, and teach the + capability gate to recognize it (objectui#8193). + + `ObjectView` built the view-level kanban config it hands to `list-view` and wrote + the deprecated alias `groupField` — never `groupByField`, the key + `@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it + already READ the canonical key first. Its sibling producer in the same package, + `defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and + left the reasoning next to itself ("that read-site now prefers the spec key, so + one key is enough"); the twin was not carried along, so one producer surface + spoke two vocabularies for one concept depending on which entry point you + arrived through. `ObjectView` writes into `options.kanban`, which + `normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing + corrected it downstream and the legacy spelling was what drove the lanes. + + The expression is now the exported `kanbanViewOptions`, the fifth member of the + `timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` / + `galleryViewOptions` family it had been the odd one out of. + + **`ListView`'s kanban capability gate changed with it, and had to.** The gate + consulted the `options.kanban` bag for the LEGACY spelling only, so a producer + writing the spec key into the bag was invisible to it while still rendering + correctly — the render branch merges the bag and resolves + `groupByField || groupField`, so the gate recognized strictly less than what + renders. Measured before and after: a bag of `{groupBy, groupField}` offered + Kanban and `{groupBy, groupByField}` did not, so shipping the producer change + alone would have removed the Kanban toggle from every object view. The gate now + reads both spellings out of the bag — the same one-question-two-sites repair + objectui#5042 made for `map` and objectui#7544 for `chart`. + + **No alias READ was removed, and the alias is not retired.** Stored metadata + still authors `groupField`, and every read site still resolves it; `ListView` + already preferring the canonical key is precisely why the sibling producer could + drop the alias write. What changed is only what this one face WRITES, plus one + added rung on the gate. + + **Migration.** Nothing authored has to change. If you read + `options.kanban.groupField` off the schema `ObjectView` produces, read + `groupByField` (or both) instead — that bag now carries the spec spelling. + + The view-level `groupBy` in the same bag is untouched. It is not a spec key + either — measured against the strict `KanbanConfigSchema`, which refuses it by + name — but `ListView`'s projection collectors read it, so retiring it needs its + own producer census and is filed as objectui#8213. +- d06fba8: The console record page and the `record:activity` block read ONE `sys_activity` + type table, so a scheduled meeting no longer appears on one and vanishes on the + other. + + `RecordDetailView`'s `sys_activity` merge carried a hand-written copy of the + table that `record:activity` exports as `ACTIVITY_TYPE_TO_FEED_TYPE`. Neither + file imported the other and nothing compared them, so the two could drift + silently — and they had. objectui#5840 added `scheduled` -> `event` to the + exported table, because a shipped producer (HotCRM's `schedule_meeting`) writes + that value and the row was being dropped before any filter ran. The copy here + was left untouched, so the same row rendered on a hand-authored record page and + was dropped on the console record page: same record, same row, two answers. + + `RecordDetailView` now imports the exported table, and the copy is gone. The + table is re-exported from `@object-ui/plugin-detail`'s entry point, which is + what makes a single reading possible at all — it was previously reachable only + from inside the plugin. No module enters the eager closure: the module holding + it was already pulled in by the renderer beside it. + + Adding an activity type is now one edit, in one place, that both surfaces see. + A re-fork is caught rather than merely discouraged: the new pins spy on the + shared object and inject a member into it at runtime, so a private copy holding + today's members exactly — the failure this change removes — fails, where a + value comparison would pass on the defect. + + Only the table converges. The row-to-`FeedItem` construction around it is still + written twice, so an unmapped activity type is still dropped silently on the + console surface where the block warns once; that mirror is filed separately. +- 12295b6: `provisionProductionEnvironment` now REFUSES a success payload whose `data` + carries no `environment` row, instead of resolving best-effort (objectui#6707). + + objectui#6629 fixed this consumer to read the created environment from the + nested `environment` key, but deliberately left the envelope check alone: it + catches a **missing** `data` and says nothing about `data`'s **shape**. So a + producer that regressed to a flat payload would once again resolve successfully + with `id` and `hostname` both `undefined` — the same silent outcome #6629 had + just fixed, reachable again by a producer change alone. + + A flat payload is a producer contract violation, not a second dialect to be + tolerated, and tolerating it is how the original defect stayed invisible. The + call refuses it now, which routes a producer regression to this call's already + documented failure path rather than a successful-looking no-op: the sole caller + (`CreateWorkspaceDialog`) already wraps the call in `try`/`catch`, logs a + warning, and lets the onboarding gate re-provision lazily on first navigation. + Workspace creation itself is unaffected — the caller does not re-throw, and it + never read this call's return value. + + The refusal carries its own diagnostic, distinct from the missing-envelope one, + so a logged warning still distinguishes "the control plane did not wrap the + payload" from "it did not put the row where it says it does". + + The wire shape this is written against is confirmed producer-side rather than + inferred from this consumer — the distinction is load-bearing, because before + #6629 the only in-repo artifact pinning this payload was a hand-written mock + pinning the bug shape. Both sources are recorded on the function's docblock. +- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id. + + A record waiting on a position rendered its approver as `positi…ager` — the + engine reference `position:sales_manager`, 22 characters, past the identity + formatter's 14-character truncation arm and middle-truncated to fit its chip. The + step names beside it were human prose; the one line answering *who is holding + this record* was an internal identifier, and not even a complete one. The same + reference reached the admin-override confirm dialog un-truncated, so a paragraph + of plain governance prose ended `— position:sales_manager` (objectui#5414). + + Both surfaces now resolve the reference before rendering, in three tiers, most + authoritative first. The server's own `pending_approver_names` wins whenever it + answers, and a backend that resolves its own slate costs the record page no extra + request. Otherwise the console reads the directory row the spec's approver + binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, + for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no + adapter and no row, the machine name still prettifies into prose rather than + truncating. The raw reference stays on hover, which is where an internal + identifier belongs. + + An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` + is actionable where `positi…ager` is not, and it is the motivating rescue case + for the admin-override path. Staffing is deliberately tri-state — a + `sys_user_position` read the viewer is not permitted to make leaves the seat's + staffing UNKNOWN and says nothing, because "I could not look" is a different + claim from "nobody holds it" and only one of them is safe to print on a + governance surface. + + Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` + and `approvalsInbox.approverNameSeparator`. The separator is a translated + punctuation key rather than `Intl.ListFormat`, which was measured on this tree + joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with + no separator, reading as one person's name. + + The directory-backed kinds and their value columns are read from + `@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new + approver type is covered the day the spec publishes it. Id-valued kinds + (`user` / `team` / `department`) keep the existing middle-truncation: a row id + has no prose to recover, and that arm is objectui#3461's answer, not this card's + defect. +- 6bd343d: The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio/<pkg>/interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app:<pkg>:build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio. +- 1939c96: 设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one. +- b470e91: The metadata designer states its package on the publish step, not only on the save (#5420) + + Studio's designer save→publish loop bound the draft to a software package on the + save (`PUT ?mode=draft&package=<id>`) and then sealed it with a publish that named + no package at all. `objectstack#10354` (shipped in `@objectstack/rest` 17.2.0) taught + `POST /meta/:type/:name/publish` to accept `?package=<id>`, so the second call can now + state the same binding the first one already states. + + - `MetadataClient.publish()` accepts `packageId` and sends `?package=<id>`, the same + wire spelling and the same `encodeURIComponent` treatment `save()` gives it. + - `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation + (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The + `?package=all` "show everything" scope keeps folding to "no package". + + The parameter is **omitted**, never sent empty, when the designer holds no binding. + Empty and absent are the same to the framework's normaliser today, but absent is the + shape the save door already followed, and the framework's promotion path branches on + the key being present downstream. + + What this buys is **reachability**, not speed: it lets `#9612`'s package-closure + narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That + narrowing has a second, independent gate this does not touch — objects carrying no + `_packageId` provenance are kept unconditionally — so on a tenant-authored overlay + corpus stating the package still narrows nothing. +- f352a69: flow designer: the Start node's **Entry condition** now opens in the row-based + condition builder, with raw CEL one click away as the escape hatch (objectui#6226). + + The builder is given the trigger record's own vocabulary — bare field names and + `previous.<field>`, resolved by `flow-scope.ts` — rather than the record-scoped + default its five other consumers use, so the subjects it offers are the spelling + this surface evaluates and teaches. 15 of the 17 entry conditions shipped in the + example apps now open as structured rows; the remaining two mix `&&` with a + parenthesised `||` group, which is a grammar limit of the row model and keeps + them on raw mode by design. + + Compiled output is unchanged: the same CEL the runtime already evaluates, in the + author's own quoting. The legacy `criteria` key, a schedule/manual/webhook + trigger (which binds no record), and every other `expression`-kind flow field + keep the single-line input they render today — the builder is opt-in per field + descriptor and requires a declared vocabulary, never inferred. +- c40f3b8: A screen flow's resume result reaches the user — on both outcomes (objectui#5417). + + A dogfood walkthrough reported that a refused `resume` and a successful one + "render identically: the dialog closes and the page is unchanged", leaving no + gesture that distinguishes "created" from "rejected". Re-measured against `main` + before any change, one half of that was already fixed — `interpretFlowResponse` + reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its + prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors + after the version the report was measured on. There was no interpreter bug and + no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were + real, and they are what changed: + + - **A terminal failure no longer closes the dialog.** The reason it closed is + unchanged and is not reversed: on a `FLOW_FAILED` the engine has already + consumed the suspension, so a resubmit can only reach "No suspended run" and + must not be offered. Closing was one way to withhold that dead retry and the + expensive one — the user had just typed a form they could no longer see, and + the engine's sentence names a value that left the screen with it. The dialog + now stays open with the submit affordance withdrawn: the flat footer swaps + Submit for Close, and an `object-form` step drops its Save (which also stops a + second click from duplicating the record it had already persisted). + - **The refusal has a second, non-expiring carrier.** The toast stays — it is + viewport-fixed, so it still reaches a user scrolled past a tall step's header + — and an inline destructive `Alert` (`role="alert"`) now holds the same + sentence inside the dialog, beside the values that produced it. A retryable + refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, + and its banner clears as soon as the user starts editing. + - **A successful run invalidates what the flow WROTE, not just what the user is + looking at.** Both hosts answered `onComplete` with + `notifyDataChanged({ objectName: <this page's object> })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- f4becce: Console Home stops offering the metadata-authoring front door on a deployment + whose own runtime config says authoring is not offered there (objectui#5521). + + The "Build an app" cover card is now withheld when + `GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed + hosted-SaaS shape that card led a plain tenant into the full authoring flow + behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry + holds zero authoring handlers — the entry was offered and the refusal arrived at + submit. The lockdown criterion for that shape is two-part, UI entry hidden AND + API refused; only the backend half was green. + + - The card is **hidden, not dimmed**, because that is the flag's own declared + meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when + false, the SPA hides the AI authoring affordances", and the serving plugin + documents "set false to force-hide the authoring UI". + - `features.marketplace` keeps the different presentation objectui#5504 gave it + — a dimmed card plus a visible localized reason. That flag means a route is + reachable; this one means force-hide. "Start with a template" is untouched: + installing a marketplace package is not AI metadata authoring and answers to + its own flags. + - No reason line is rendered in its place. `home.build.noCapability` says the + *account* lacks "Manage Metadata"; on a runtime with no authoring at all the + surface is absent for everyone, and pointing a viewer at a permission that + would not help them is the misdirection objectui#5557 is about. + - Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()` + already encodes: a runtime predating the flag, or one whose config fetch + failed, keeps the card exactly as visible as before. + + No new authorable config key, no new server surface, and no new copy — the flag + was already being served and already reaches the browser. +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. +- e2e8e68: **Behaviour change:** the spec's view-level `map` block on a list view is now read at + runtime. `ListMapConfigSchema` (objectstack#9340) has been authorable and validated since + the `@objectstack/spec` 17.1.0 pin — it flows into this repo's own `ListViewSchema` by + reference — but nothing consumed it: `ListView`'s `case 'map'` forwarded only the legacy + `schema.options.map` bag, so declaring `map: { titleField: 'title', locationField: + 'location' }` on a view changed nothing and marker titles fell back to the renderer's + placeholder. + + The block now reaches `plugin-map` and drives every one of its seven reads — coordinate + extraction, marker title and description, and the initial camera. Precedence follows the + convention the sibling visualization blocks in the same file already set: the view-level + block wins over `options.map`, per key, exactly as `kanban` / `calendar` / `gallery` / + `timeline` / `gantt` each merge their spec config over the legacy bag. Both sources go + through the existing objectui#5177 key whitelist, and the branch still emits the flat + form, so `getMapConfig`'s objectui#5018 precedence rule ("neither flattener emits a `map` + key at all") stays true. + + The visualization switcher had the same gap with a sharper consequence: the capability + gate that decides which visualizations are offered also read `options.map` alone, so a + view binding its coordinates in the spec block was filtered out of its own + `appearance.allowedVisualizations` and fell back to `['grid']`. The gate now asks the same + merged config the render seam forwards, so the two cannot disagree — including for a + binding split across the two sources. + + `InterfaceListPage` (ADR-0047 interface pages) forwards the referenced view's `map` block + for the same reason. It is passed alongside the auto-derived `options.map` rather than + replacing it, so a partial authored block — `map: { titleField: 'title' }` — keeps the + derived coordinate binding instead of dropping it. + + No defaults are introduced for `zoom` / `center`: an undeclared camera stays undeclared, + so the fit-to-queried-records behaviour ruled in objectui#5000 is unchanged. +- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a + first-class disabled state, and the load-failure hint describes the control plane + the runtime was actually pointed at (objectui#5504). + + `apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory + default, so a stock self-hosted stack has no marketplace at all. The Console still + recommended one: Home led with "Start with a template" and "Browse App + Marketplace", and the click landed on a red **Failed to load marketplace / Not + found** card whose hint claimed this runtime "points at the public ObjectStack + cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for + exactly the deployment reading them — the operator had not left the default, and + the advice pointed back at the template that told them to set `off`. "Marketplace + disabled by configuration" is a configuration conclusion, not a load failure. + + - `isMarketplaceEnabled()` (`runtime-config`) reads the server's own + `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the + serving app's route table (objectstack#8356). It is never inferred from the shape + of a failed request: a control plane that is merely DOWN leaves the flag `true`, + so an outage still renders as an outage. Unknown fails OPEN. + - The marketplace page renders an informational "App Marketplace is turned off" + state — muted, not `destructive` — and issues no request it knows will 404. + - Home's "Start with a template" cover greys out with a visible localized reason, + and the "Browse App Marketplace" shortcut is withheld, exactly as they already + are for the `manage_metadata` capability gate. + - `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the + configured control plane) and `failedHintSameOrigin`. The "points at the public + cloud by default" sentence is gone: it was rendered unconditionally, including on + every runtime whose operator had overridden `OS_CLOUD_URL`. + + All ten locale packs carry the new keys. +- 1688986: Studio workbench rails converge on an AI copilot apply without a page reload (objectui#7255) + + The copilot is the right dock of the Studio document and already announces + every authoring turn on the assistant bus (`emitMetadataRefresh`), but the + four pillar rails never subscribed: a just-applied object or nav item stayed + invisible until the author reloaded the page. The Interfaces / Data / + Automations / Access rails now list that pulse as a load dependency and refetch + their package-scoped reads in place. The Interfaces rail holds the pulse while + the nav editor has unsaved edits (its load rehydrates the nav edit buffer) and + releases it once they settle, and re-reading the same package no longer flaps + the app status back through `loading`. +- 8b1a8f3: **The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.** + + The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495. + + **The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered". + + One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one. +- 63b4e0e: Fix the three breaks at the AI paywall moment (#7253), measured on a free plan's + second build iteration when the cloud guardrail refuses "Confirm changes" with a + 429 `AI_DESIGN_QUOTA_EXHAUSTED`. + + - **The upgrade CTA no longer opens a 404.** It used to open a client-composed + `${cloudBase}/apps/cloud-control/sys_environment`, which guessed the control + plane's console mount, app slug and route — all three wrong — and landed on + the API's `ENDPOINT_NOT_FOUND` JSON. `cloudInstallDeepLink` / + `cloudPricingDeepLink` are replaced by `cloudConsoleUrl()`, the + runtime-supplied cloud origin with no path appended; the control plane's own + root redirect decides the landing page. The former + `|| 'https://cloud.objectos.app'` default is gone: a runtime with no upstream + cloud now renders no upgrade link at all rather than pointing a self-hosted + user at the vendor's SaaS. + - **The confirm card gets an explicit failure state.** A quota refusal parks the + card on "not applied" with the server's own next step (reset tomorrow / + upgrade) plus the upgrade action, instead of silently rolling back to + "Confirm / Adjust" as though the click had never happened. Transient failures + (offline, per-minute rate limit) still roll back, because retrying is the + right next step there. + - **The composer is no longer refilled with an already-delivered message.** + Only text typed into the composer is restorable now; card-driven sends + (confirm, approve, suggestion chips) send canned text the user never typed and + no longer leave the previous prompt staged as if it needed resending. +- 60d452e: `ReportView` reads a report's data binding through the one key the contract declares — `dataSource.object`. + + The view accepted `resource` as a second spelling of `object`, in two places, and + named that spelling in a warning the user could read: + + ``` + :171 liveReport?.objectName || liveReport?.dataSource?.object + || liveReport?.dataSource?.resource + :273 dataFetchSource.dataSource.object || dataFetchSource.dataSource.resource + :275 console.warn('ReportView: dataSource missing object/resource property') + ``` + + `resource` is not on this binding. `ElementDataSourceConfig` declares `object`, + `view?`, `filter?`, `sort?` and `limit?`; its `@objectstack/spec` twin + `ElementDataSourceSchema` is a strict object, so an extra `resource` key is + *rejected* there rather than ignored; and the binding's own predicate + `isElementDataSourceConfig` decides on `object`. A `resource`-only binding + therefore was never a binding on any other renderer in the system — it rendered + here and silently produced nothing anywhere else, with neither end reporting a + problem. That divergence is what a consumer-side alias buys: one renderer + answering a question the contract says has no answer. + + `resource` is a real key on other surfaces — `CRUDSchema.resource`, the + `DataSource` adapter's first parameter, `LiveExportOptions.resource` — and all + three are untouched. None of them is this one. + + Behaviour, measured by rendering each input shape before and after. Only the + `resource`-only shape moves: + + | binding | before | after | + | --- | --- | --- | + | `object` only | queries that object | unchanged | + | `resource` only | queries it as if declared | not queried; named warning, no rows, fallback field list | + | both | queries `object` | unchanged | + | neither | not queried; warning | unchanged | + + So off-spec report metadata that used to render now fails loudly instead of + appearing to work. A producer census found nothing that would notice: no site in + this repository, and none in the `objectstack` framework repository, writes + `resource` onto a report `dataSource`. The limb was speculative in the commit + that introduced it, and per AGENTS.md #0.1 an off-spec spelling is corrected at + the producer, never taught a second dialect by the renderer. + + The `:275` wording now names only `object`. A diagnostic that lists a key the + contract does not declare is not a small thing: it is the system telling an + author — increasingly, an author's code generator — that the wrong spelling is + supported. +- 14ef9f5: Console error reporting is now configured entirely by the **server**: the DSN and every + knob that travels with it arrive on `GET /api/v1/runtime/config`, and the build-time + `VITE_SENTRY_DSN` path is retired (objectui#5522, consumer half of objectstack#12681). + + ObjectStack's users consume a **prebuilt** Console. Under the previous two-key gate — + a build-time DSN **and** a runtime permission — a build-time key was unreachable for + them, so a self-hosting operator could not enable client error reporting at all: the + permission was reachable and the source was not. The maintainer named it on 2026-08-27: + + > 「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」 + + ``` + send ⇔ the runtime served a DSN + ``` + + **The DSN's presence IS the grant.** There is no companion permission flag, and the one + that briefly existed is removed rather than paralleled. Two knobs in two places produced + two silent dead states — "permission on, no DSN" and "DSN in, permission off" — that look + identical from the browser; one knob cannot disagree with itself. Turning reporting off is + unsetting the server DSN, and there is deliberately no build-time force-off left, because + nobody consuming a prebuilt console could reach one. + + The fail-closed posture is unchanged and structurally stronger. Absence of a *source* is + not a value that can be misread, so a runtime predating the key, a third-party host, a + 404, a network failure, a malformed body and a config that has not arrived yet all read as + off — where the boolean needed a strict `=== true` plus a written argument about why a + negative `disabled` flag would have been vacuous on exactly the runtimes that were leaking. + + ### What moved, and the one thing that did not + + `sendDefaultPii`, `environment`, `tracesSampleRate` and the error-session replay rate move + into the runtime payload. They were build-time variables, so a prebuilt-console consumer + could set none of them — including the one deciding whether IP and User-Agent leave their + network. This is not new surface; it is the same surface moved to the side that can + operate it. + + `VITE_SENTRY_RELEASE` **stays build-time**, and is now the only `VITE_SENTRY_*` variable + that exists. A release identifies which bundle produced a stack trace and must match the + source maps that bundle's pipeline uploaded — a property of the build, which no server can + know. `VITE_SENTRY_ENABLED`, `VITE_SENTRY_ENVIRONMENT`, `VITE_SENTRY_TRACES_SAMPLE_RATE` + and `VITE_SENTRY_REPLAY` are retired along with `VITE_SENTRY_DSN`. + + ### Breaking + + | FROM | TO | + |:--|:--| + | `VITE_SENTRY_DSN=…` in the Console build environment | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=…` on the ObjectStack runtime | + | `VITE_SENTRY_SEND_DEFAULT_PII=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true` | + | `VITE_SENTRY_ENVIRONMENT=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT=…` | + | `VITE_SENTRY_TRACES_SAMPLE_RATE=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE=…` | + | `VITE_SENTRY_REPLAY=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1` | + | `VITE_SENTRY_ENABLED=false` | unset the runtime DSN | + | `isClientErrorReportingAllowed(): boolean` | `getClientErrorReporting(): RuntimeClientErrorReporting \| null` | + | `resolveSentryGate(env, runtimeAllows)` | `resolveSentryGate(runtimeErrorReporting)` | + | `RuntimeTelemetry.allowClientErrorReporting: boolean` | `RuntimeTelemetry.errorReporting?: RuntimeClientErrorReporting` | + + One-line fix for a deployment: move your `VITE_SENTRY_*` values onto the ObjectStack + server as the `OS_TELEMETRY_CLIENT_ERROR_REPORTING_*` variables above, and drop them from + the Console build environment. One-line fix for a consumer of `@object-ui/app-shell`: + `const sink = getClientErrorReporting(); if (sink) …` in place of + `if (buildTimeDsn && isClientErrorReportingAllowed())` — the build-time conjunct is gone, + because the server now supplies the source. + + **Landing order is safe in both directions.** A Console built before this change meets a + new server, reads an absent `allowClientErrorReporting` and stays off; a Console built + after it meets an old server, reads an absent DSN and stays off. Neither half can turn + reporting on by itself, so the two repos' PRs can land in any order. + + The `committed-telemetry-endpoint.test.ts` ratchet is unchanged in rules and unchanged in + job: nothing endpoint-shaped may be committed to this repo. Its rules key on the + variable's suffix and on the value rather than on the `VITE_` prefix, so they already + cover the runtime-side spelling — now pinned, so a later tidy-up cannot narrow them to the + retired names and reopen the hole under a new one. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. +- 8e32da7: AI build surface gains a standing 「未发布改动」 bar (#5694): while the conversation's bound package has pending drafts, a bar floats above the composer — surviving scrolling — counting the unpublished changes and publishing them through the same governed `publish-drafts` route as the inline card button, with probe findings surfaced instead of a blind success toast. Renders nothing when the count is zero or the conversation is unbound. +- f157423: Studio workbench and AI tool cards speak the author's language (objectui#7254) + + - The Interfaces breadcrumb, canvas caption and navigation rail show the + metadata label plus a translated kind; the internal `type · name` pair moves + to the tooltip. An unlabelled nav leaf now falls back to its object name + instead of rendering an empty row. + - The Studio top-bar package switcher reads the package's human name from + either position the packages endpoint serves it in, instead of degrading a + registry-shaped entry to its reverse-domain id. + - The dashboard property panel is localized: the spec's authoring form is + overlaid through the platform's own `metadataForms.<type>` convention, so + section headings, field labels, hints and the `header` composite's sub-fields + render in Chinese (developer vocabulary such as "Tailwind units" is replaced + with something an author can act on, not transliterated). + - AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty + platform-provided tools, ten locale packs), the header status badge is + localized, and the plan count strip is a real plural family instead of an + English `+ "s"` concatenation. + - The tool card's header badge and its body badge now come from one producer: + a proposal that has been confirmed, built or published no longer keeps a + header reading "Awaiting Approval". +- 7a90afd: Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. + + Creating an object through Studio collected exactly two things — display name and + identifier — and saved a draft that declared no `sharingModel`. The draft saved + happily, the form designer worked, and the object was then refused at 发布 → + 全部发布 by `security-owd-unset`: a required decision the surface never asked + for, delivered by failing, as English ADR prose in a toast that then vanished on + a timer. The one actionable word in it named a control three clicks away that + nothing routed to. + + The publish gate is correct and is unchanged — an org-wide default has to be an + authored decision, not an accident. What changes is when the console asks and + when it answers: + + - **The create dialog asks.** A third field collects the baseline, pre-selected + to `private` and glossed with the Settings tab's own strings, so a new object + is publishable by construction. `buildObjectSkeleton` now takes the value as a + required parameter — a future create path cannot omit the baseline without + failing to type-check. `controlled_by_parent` is deliberately not offered at + creation: it derives access from a master relation a brand-new object does not + have yet, so offering it would trade one publish refusal for another. + - **The review sheet reports it.** The pending-changes panel now runs the + framework's own `validateSecurityPosture` over the pending object drafts and + names any blocking finding, with its fix-it hint, next to the Publish button. + It mirrors the producer's rule rather than re-deriving it, and it reports + without blocking — the server door stays the authority. + - **The Settings tab stops calling an unset baseline safe.** It described unset + as "defaults to Private", which answers what the runtime does and not whether + the object can ship. It now reads as the publish-blocking problem it is, + styled like the external-wider warning beside it. +- 19479a3: Studio simplification, first cut (#5813): the Data pillar's object sub-tabs collapse to 记录/表单 with the five power panels (验证/钩子/操作/API/设置) behind one 「高级」 menu (the open panel's name wears the active pill, so the collapsed default never hides where you are); drafts now AUTO-save (debounced 1.5s, CEL-blocking gates the timer per the objectui#4306 rule, a failed save waits for the next edit) and the four 保存草稿 buttons are retired in favor of a quiet saving/last-saved hint; the 权限 pillar moves from the top-level row into a 「更多」 overflow whose items carry the same dirty-guard as the primary pillars — the /studio/:pkg/access route and every page behind it are untouched. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- 4697234: One pending-drafts source of truth (#5801): `usePendingDrafts` / `fetchPendingDrafts` replace the five hand-rolled `GET /api/v1/meta/_drafts` copies behind the home banner, Studio topbar 变更/待发布 buttons, chat pending-drafts bar, chat draft-card resolver, and the preview watermark bar. Every successful publish path (`usePublishAllDrafts`, Studio `doPublish`, the chat bar) now emits the assistant bus's metadata-refresh pulse, and a chat turn that staged or replayed drafts emits it on completion — so a publish or an agent-staged draft anywhere converges every surface at once, and the home banner no longer needs its post-publish `window.location.reload()`. An errored drafts read now uniformly reports UNKNOWN (null), never a fake zero. +- f471b4f: The metadata designer's View **column list** now reads a column's identity — + row label and bound field name — in the ObjectStack canonical spelling only: + `field` and `label`. The legacy TanStack aliases `accessorKey` / `header` are no + longer consulted (objectui#5725). This is the editor-side half of the read + objectui#5344 retired in the column inspector one file over; the two surfaces + render inside the **same panel** and until now gave the author two different + answers about what a column is called. + + Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both + legacy keys by name (`unrecognized_keys`), so a column carrying them has no + field key and no label the spec recognises. Two consequences the list surface + had that the inspector did not: + + - **The label chain was inverted, not merely tolerant.** It read + `label ?? header ?? field ?? accessorKey`, preferring the undeclared `header` + **over** the declared `field`. A perfectly canonical column that also carried + a stray `header` key displayed the alias' value *instead of* its own declared + identity. + - **The field-name read backs more than a label.** It feeds `usedFieldNames()`, + which the Add-field picker consults, so a spec-refused column reserved a field + name and the picker tagged that field "Added" — for a column no accepted + document actually binds. + + **What an author sees:** a stored column shaped `{ accessorKey, header }` is now + named positionally in the list (`col 1`) rather than by its refused spelling, + and the row stays selectable exactly as before — the positional fallback already + existed at the end of that chain. The Add-field picker no longer reports such a + column's field name as taken. Canonical `{ field, label }` columns and + bare-string columns are untouched, and a declared `label` / `field` now outranks + any stray alias beside it. + + **What is deliberately NOT changed:** the write path. These helpers still + reorder and splice the raw `columns` array without normalising it, so no stored + document is rewritten by the act of viewing or editing it — the same fence + objectui#5344 held. +- 0e00da3: The metadata designer's View column inspector now reads a column's identity — + field key and label — in the ObjectStack canonical spelling only: `field` and + `label`. The legacy TanStack aliases `accessorKey` / `header` are no longer + consulted (objectui#5344). + + Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both + legacy keys by name (`unrecognized_keys`), so a column carrying them has no + field key the spec recognises. The inspector nevertheless read them and + displayed `accessorKey`'s value in the field-key box, presenting a spec-refused + key as though it were a valid column identity — the same consumer-side + tolerance alias `ObjectGrid` retired in objectui#5068, surviving one layer up in + the authoring tool, which is the surface that is supposed to teach the correct + shape. + + **What an author sees:** a stored column shaped `{ accessorKey, header }` now + shows an EMPTY field key and an empty header, and is re-authored rather than + silently carried. Canonical `{ field, label }` columns and bare-string columns + are untouched. + + **What is deliberately NOT changed:** the writeback. `patchIdentity` still + re-emits whichever spelling it was handed, so no stored document is rewritten + by the act of editing it. Normalising on write was ruled out once the + maintainer confirmed there is no population of legacy stored documents to + migrate; such a column stays unsaveable against the spec both before and after + an edit, exactly as it did before this change. What this removes is the + invisibility of that state, not the state itself. + +### Patch Changes + +- f66072d: Inline `t(key, { defaultValue })` strings are now held to the one placeholder spelling a + provider-less host can resolve, and five of them are pinned to the pack value for the first + time (objectui#4905). + + `check:i18n-keys`' `default-value-drift` class pins an inline default byte-identical to its + `en` row, and objectui#3512 holds `en` to the one spelling `createSafeTranslation`'s + `fallbackT` interpolates — so most inline defaults were covered transitively. Re-measured + on this tree, 66 of 1003 were not: three literal defaults on dynamic keys, and 63 written + as a computed expression. A new `unresolvable-default-spelling` class in + `scripts/check-i18n-call-site-keys.mjs` now judges the text every inline default carries — + the folded sentence, or a template literal's static segments — so `{{ name }}`, + `{{count, number}}`, `{{- name}}` and `$t(key)` are refused wherever they are written, + rather than only inside a copy table. + + Four call sites gain a real pin because their default carries no placeholder at all: the + record-form submit button now falls back to `Update`/`Create` (the pack's wording) instead + of `Save`/`Create` via a nested `t('common.save')`, the context selector's package label + and the approvals separator now state their literal. One more (`detail.showEmptyFields`) + is behind a `createSafeTranslation` hook, whose fallback does interpolate, so it can safely + say what the pack says. + + The other 61 are deliberately left computed, and the measurement behind that is the useful + part: react-i18next's not-ready `t` returns `options.defaultValue` **verbatim, without + interpolating it**. At a call site bound to a bare `useObjectTranslation()`, a default + written as `` `Signed in as ${user.email}` `` is therefore the only form that renders + correctly with no provider — rewriting it to `'Signed in as {{email}}'` would put literal + braces in front of the user, which is the exact defect this family of cards exists to + prevent. Those sites keep their template literals and are covered by the spelling class + instead. +- baac3f4: Fix four `find()` calls that passed a query option without its `$`, and gate the shape. + + `QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies + exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw, + no warning, and it type-checks because the type carries `[key: string]: any` for + adapter-specific params. For a dropped cap the result is an **unbounded** read rather than + a truncated one: the platform's GET list route has no default page size, so the query + returns the whole match set and stays invisible until the object is large. + + - `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one + **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so + the dropped key turned "count only, fetch nothing" into "fetch every row in the object", + on every mount and every refresh of every list view. It now sends `$top: 0` and reads + the count off `total` only; the row-counting fallbacks are gone rather than repointed, + because once zero rows are requested an empty `data` means "you asked for none", not + "the object is empty", and counting it would assert a confident `0`. With no total the + footer line is omitted instead. + - `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`, + one line from three correct `$top` calls. + - `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so + a filter's option list read every row and every column of its source object while its + own comment described it as capped at 200. The same call read `records.items`, which is + not a `QueryResult` member, so against a real adapter the fallback produced no options + at all. + - `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the + result in its page-source metadata. + + A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time: + a known query-option name missing its `$` in the second argument of a `find`/`findOne` + call. It is narrow on purpose — a closed list of spellings, anchored to the call — because + the index signature exists so adapters can take adapter-specific params, and a rule that + flagged any unprefixed key would report the shape the type was written to allow. Its + sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged. +- 5f97781: Declare `scim?: boolean` on `RuntimeFeatures` and map it through + `initRuntimeConfig` (objectui#5869), mirroring its two commercial siblings + `customDomain?` / `sso?` end to end: same doc-comment style + (server-derived, absent-on-vanilla), same `false` default, same + `body.features.scim === true` derivation. + + This documents and now honestly carries the wire a shipped cloud producer + already emits in the same `resolveFeatures` object literal as + `customDomain` / `sso`; the key already arrives at the SPA today, untyped. + Declaration plus plumbing only — this patch adds no read point, no gate, + and no SCIM UI affordance. Any actual SCIM-gated UI is future work. +- 7c96c94: Four more private copies of the reference-bearing field family converge onto + `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`, and the "fourth and last private copy" claim + that `paramToField` still stated is corrected (objectui#5874, objectui#5875). + + Each copy diverged from the published family in BOTH directions, so this is a behaviour + change on every face and not a refactor: + + - **`user` and `tree` are now treated as relations.** Both carry the same foreign-key + storage as `lookup` and resolve through the same expand path (objectui#2032), and each + face's own stated reason for special-casing `lookup` applied to them verbatim — so + gaining them restores the rule each face already meant. A `user` / `tree` field is now + read-only in the quick-look drawer (`RecordDetailDrawer`), where the drawer wires no + relation picker and a plain text input let a user overwrite the relation with a + free-form string; it gets the wide layout basis in the record header's highlights strip + (`HeaderHighlight`), whose inline editor is a record picker; and a field-backed action + param over one now inherits the picker config it needs (`resolveActionParams`). + - **`master_detail` is now treated as a relation by `resolveActionParams` too** — it was + the only face missing that member as well, so a field-backed `master_detail` action param + inherited no `referenceTo` at all and degraded to the unexplained "paste a record id" + text input that objectui#3405 exists to prevent. + - **The undeclarable `reference` spelling is gone from the three field-type faces.** + Measured against `@objectstack/spec`'s closed `FieldType` vocabulary with live controls + (`lookup` / `master_detail` / `user` / `tree`) and dead ones (the retired `owner`, plus a + nonsense spelling): `reference` is absent, so no spec-compliant object schema could + declare a field that reached those branches. It sat exactly where `owner` sat before + objectui#4814 retired it — dead weight that read as live capability. + + `resolveActionParams` keeps answering for `reference`, deliberately and by a different + route: it is refused by the spec's `ActionParamSchema` too, but the dialog still accepts it + from params already authored with it, and that acceptance belongs to the one alias table in + `paramToField` rather than to a hand-copied membership test. This face now asks the shared + family over the widget key that table produces — the same expression `paramToField` + evaluates one step later, so the half that populates a param's picker config and the half + that forwards it can no longer disagree. + + No face copies the set: each calls `.has()` on the object `@object-ui/core` exports, and + each carries an identity pin (a spy on that `has`) so a member-identical private copy fails + instead of quietly re-forking the table. +- 2cf69e4: The field designer now reads and writes a lookup field's relationship target under the + spec's spelling `reference` (objectui#6041), in both directions. + + `referenceTo` is not in `FieldSchema`'s accept set. Measured against the installed + `@objectstack/spec` 17.2.0, through the whole object document that + `PUT /api/v1/meta/object/:name` validates: + + ``` + ObjectSchema.safeParse({ …, fields: { rel: { type: 'lookup', label: 'Owner', + referenceTo: 'user' } } }) + => success = false + => unrecognized_keys at ["fields","rel"] keys=["referenceTo"] + "Did you mean `referenceTo` -> `reference`?" + ``` + + so authoring a lookup field through the designer returned a hard 422 `INVALID_METADATA`, + and — because the key is then stored — blocked **every subsequent save** of that object, + with nothing in the UI to say which key did it. + + The read direction was broken symmetrically and is the half that would have survived a + write-only fix: `toDesignerField` read `raw.referenceTo` while a spec-parsed server sends + `reference`, so every already-saved lookup field loaded into the designer with an **empty + reference box**. Both wire-bound payload shapes move — `FieldMetadataPayload` + (`MetadataService.toFieldPayload`) and `ServerFieldSchema` + (`MetadataFieldsPage.fromDesignerField`). + + `referenceTo` also joins `RETIRED_FIELD_KEYS`. Renaming the emit sites alone does not + unblock an object whose stored fields already carry the misspelling: `carryOver` spreads + the previous server def verbatim, so the key would ride straight back out to the same 422. + The designer's in-memory `DesignerFieldDefinition` keeps `referenceTo` — that is the + internal prop name every other UI surface in this repo already uses (`LookupField`, + `filter-builder`, `ObjectChart`, `ListView`, `UserFilters`), it reaches no wire-bound + shape, and the parity gate classifies it as `uiOnly` rather than a violation. + + No behavioural change for a half-filled draft: the spec's prose calls `reference` + "required for relationship types", but that is not enforced by the zod parse at 17.2.0 — + `{ type: 'lookup', label: 'L' }` parses green at field level and through `ObjectSchema`, + and `undefined` is dropped by `JSON.stringify` under either spelling, so the wire bytes + are identical before and after. +- 79ebf30: Home renders one agreed label for an item kind that has no translation key. + + The rail (`HomeContinue`), `RecentApps` and `StarredApps` all resolve the same + `home.recentApps.itemType.*` label, and each spelled the lookup itself. They had + drifted: the rail fell back to the bare kind (`report`) where both card surfaces + fell back to the capitalised one (`Report`) — two spellings of the same word on + one screen. All three now resolve through a single `recentItemTypeLabel` helper, + so the fallback cannot drift apart again. + + User-visible: the rail's label for an unkeyed kind changes from `report` to + `Report`. Every kind shipping today carries a key, so no label changes for them; + this is about the next kind added, and any host passing a kind the locales do + not carry. +- 9ea4cde: `MetadataService`'s object writers PUT `fields` as the name-keyed MAP `ObjectSchema` + requires, not an array (objectui#6240). Both of the designer's write paths were affected, + and `saveFields` ran the conversion in the wrong direction outright: the server's own + document arrives with `fields` as a map, and `fields.map(toFieldPayload)` turned it into an + array on every field save. + + Measured against the installed `@objectstack/spec` 17.2.0 and against the framework's own + write door. `ObjectSchema.fields` is a required record: an array — empty or not — is + refused `invalid_type @ fields`, a map parses. `metadata-protocol`'s `saveMetaItem` + resolves metadata type `object` to that same `ObjectSchema`, `safeParse`s the whole item + and throws `422 INVALID_METADATA` **before** persisting, so the array was refused rather + than stripped or stored: every designer object save and every designer field save that went + through this service was a 422 that wrote nothing. + + This is the value-level half of the objectui#5761 parity family and is invisible to that + family's key-name gate — `fields` sits in the accept set under either shape, which is the + gate's own coverage note 4. The pins are runtime assertions on the captured request bytes. + + The conversion refuses, loudly, what it cannot key: a field with a missing or blank `name` + throws instead of writing a `{ undefined: … }` entry (measured: the spec ACCEPTS that + document, so nothing downstream would have caught it), and a duplicate name throws instead + of letting the later field silently replace the earlier — a loss an array does not have. + `saveFields` keeps preserving unknown keys of the fetched server document, which now + actually reaches storage. `saveObject` with no `existingFields` still omits the key rather + than writing `{}`: a PUT is an upsert, so `{}` would delete every field of an object on a + save that only meant to rename it. + + `saveObject(obj, existingFields)` keeps its `FieldMetadataPayload[]` parameter type — the + array is converted inside — so no caller's call site changes. +- 0ea559e: A per-option `visibleWhen` written into a metadata form now actually withdraws that option + in the metadata-admin renderer (objectui#6247). + + `SelectOptionSchema` declares `visibleWhen` (ADR-0068 / objectui#2284) and the schema is + `z.core.$strict` — an undeclared sibling key is refused with `unrecognized_keys` — so the + key is a real declaration, and a `*.form.ts` carrying a per-option predicate parses clean. + The metadata-admin renderer never read it: all three controls that consume + `fieldSpec.options` mapped the authored list straight to items. Accepted, stored, shipped, + ignored — ADR-0049's declared-but-unenforced shape. + + **This is a behaviour change, and it is the point of the fix.** Metadata that was inert + starts hiding options. An author who wrote a narrowing and got no narrowing now gets one. + Both of the old failure modes pointed the same way — the option stayed offered — so + nothing that relied on the old behaviour was relying on a narrowing being honoured; it was + relying on one being ignored. Zero `*.form.ts` in the tree uses the key today, so no + shipped form changes shape on this release. + + Three consuming controls now filter, and each has a pin asserting an option **absent** on a + false predicate: the builtin Select in `SchemaForm.tsx`, `MultiSelectWidget`, and + `colorPaletteOptions` → `ColorSwatchGroupWidget`. Asserting absence is the whole of the + test design — this evaluator fails OPEN, so "the option is shown" is equally the outcome of + predicate-true, predicate-never-arrived and predicate-faulted, and a positive-only pin + passes against the unfixed renderer. + + Per the maintainer ruling on the card (affirmed three times: 2026-08-25 batch 4; + 2026-08-25 upholding A2 over the A1 counter-proposal; 2026-08-27 declining the A0 + reject-the-key alternative): + + - **Scope (A2).** `SchemaForm`'s `evaluatePredicate` ctx now binds the four ADR-0068 D1 + identity spellings — `current_user`, `user`, `ctx.user`, `os.user` — **alongside** `data`, + selected out of the host `ExpressionProvider`'s bag rather than copied, so the alias set + cannot drift from the one builder. `data` stays the **draft**: the provider's bag also + carries a `data` key meaning its own data scope, and adopting that would be + objectui#5926 gap 2's "same key, opposite meanings" one nesting level apart. `record`, + `app` and `features` are deliberately left **unbound** so they keep raising the loud + warn-once diagnostic instead of resolving to a silent `undefined`. That diagnostic now + reads the bound names off the actual scope instead of asserting "the only name is + `data`", which stopped being true the moment identity was bound — a diagnostic that lies + sends the author to un-write a spelling that was correct. No new evaluator: the option + filter routes through the same `evaluatePredicate` the section, field and repeater-row + gates already use. + - **Emptied sets (B1).** The control's **face** keeps reading the **raw** option list — + `resolveFieldFace`'s `hasOptions`, `resolveColorWidgetKey`, and each `options.length > 0` + branch condition — and only the rendered list is filtered. Withdrawing every option + renders an **empty picker**. Filtering the branch condition instead would have degraded + the builtin Select to a free-text `Input` and `MultiSelectWidget` to its comma-tag + editor — "withdraw every option" displayed as "type whatever you like" — and would have + flipped a fully-withdrawn palette from the `color-picker` registration to `color-input`, + making the labelling channel objectui#4871 point 4 fixes in the host predicate-dependent. + - **Stored values (C1).** No pruning. A selected value whose option is now hidden survives + in the stored metadata and the picker shows the placeholder. This includes the quiet path: + `MultiSelectWidget`'s toggle re-orders the selection against the **raw** list, because + ordering against the filtered one would have dropped a hidden-but-selected value on the + next unrelated click. This renderer edits **source metadata**, and objectui#4247's own + reasoning — "missing information is not a reason to destroy stored data" — applies harder + here than it did on the runtime record surface. + + `FormFieldSpec.options` is also no longer hand-written. It derives from the spec's + `SelectOption` with its narrowings named in an `Omit`, per this file's own convention: + `visibleWhen` is re-pointed to the local `VisibilityPredicate` (`dialect` optional, `source` + required — the shape an evaluator here actually takes), and `default` stays dropped **and + now says so**, because nothing on this surface reads it. Two of the spec's five option keys + had been dropped by silence, which is precisely how a legally-authored per-option + `visibleWhen` came to parse clean and render inert. + + **Not fixed here, and it bounds what this change delivers:** this interim evaluator's `in` + operator requires an array **literal** on the right (`role in ['admin','owner']`), so the + ADR's headline spelling `'admin' in current_user.positions` — membership against a *path* — + still falls through to the bare-truthy branch and evaluates TRUE regardless of the user, + silently. That is a pre-existing grammar gap of the whole subset, identical for `data.*` + (`'x' in data.tags` is equally inert) and unchanged by this card; it is filed separately. + The spellings this fix makes genuinely discriminate are the documented subset — + `path == literal`, `path != literal`, `path in [literals]`, `!path`, `path`, `&&`, `||`. +- 944a929: The flow designer's node inspector no longer offers a **Description** field, and strips a + stored `description` off a node the first time an author edits it (objectui#6287). + + `FlowNodeSchema` is `.strict()` (objectstack#4001) and refuses that key by name — measured + on the installed `@objectstack/spec@17.2.0`: + + ``` + FlowNodeSchema.safeParse({ id, type, label, description, config }) + -> unrecognized_keys: ["description"] + ``` + + By this package's own reading of that mechanism, the cost is not untidiness but an + unsavable draft: the key "surfaces as `unrecognized_keys` in the live client validation and + as a 422 on save" (`flow-canvas-layout.withCanonicalGeometry`, on the identical retired `ui` + case). So the field was not merely describing a shape the contract refuses — it was + producing one, on every keystroke, and nothing anywhere read the value back. The spec's flow + node has eleven keys and no note key of any spelling, so there was no reader to grow into. + + Stored flows heal on the author's first edit, the same migrate-on-write boundary the retired + `ui` geometry gets, and for the same reason: with the field gone there would otherwise be no + way left to clear a `description` an author had already saved. + + The three hand-written copies of the node and edge shapes that let this drift go unseen are + now one declaration each — `FlowNodeInspector`'s node and edge types alias the canonical + `FlowNodeLike` / `FlowDesignerEdge`, and `flow-decision-edges`' fourth edge copy aliases the + same canvas edge instead of restating it with a `condition?: unknown` that had already + outlived objectui#3202's narrowing by months. +- 780efc0: `ConditionBuilder`'s row mode now compiles a value that is plainly a **reference** as one, + instead of quoting it into a string literal (objectui#6293). + + `fmtValue` quoted anything that was not a number / `true` / `false` / `null`, and the value + box is free text — so an author building "this field differs from its prior value", the idiom + that *defines* a change-detection predicate, got `previous == 'previous.status'`. That is + syntactically valid CEL, `previous` is a declared root, and a string literal's contents are + deliberately not scanned for references by `flow-ref-check` or by the server-side validator. + The predicate parsed, registered, evaluated — and was always false, with no author-time signal + at any layer, at all five surfaces that mount this builder. + + A value matching a **declared root prefix** now emits as the reference: `record`, `previous`, + `parent`, `user`, `current_user`, `org`. That set is this builder's own vocabulary — `record` + / `user` / `org` are exactly what its subject dropdown offers one control to the left, + `previous` and `parent` are bound by `evalFieldPredicate` and by the server-side hook / + validation evaluators, and `current_user` is the ADR-0068 spelling of the same identity object + `user` names. Roots this builder never offers (`data`, `os`, `app`, `features`, `input`, + `vars`, `page`) are deliberately excluded: `data.csv` is a plausible literal and `data` *is* + bound, so capturing it would trade one silently-false predicate for another rather than for a + loud one. Declaring which roots a mounting surface actually binds is caller-supplied + vocabulary and belongs to objectui#6296. + + The test is "a dotted path under a declared root", not "contains a dot" — a version string + (`1.2.3`), a filename, and a path under an unbound root all stay literal text. The literal and + number controls are unchanged: `done` still compiles to `'done'`, `42` still to `42`. + + **Nothing already stored is rewritten.** A persisted `previous == 'previous.status'` no longer + round-trips byte-for-byte, so the builder's existing safety rule hands it to the raw CEL editor + rather than reinterpreting it — the author sees both readings and decides. In the other + direction a hand-authored `record.status != previous.status` now round-trips *into* the row + builder, which it could not before. + + The repair is at the authoring surface, where the ambiguity is: no consumer-side tolerance is + added, and the emitted reference is now an identifier the existing reference checkers can see, + where a string literal's contents were invisible to them. +- dddb942: Delete the dead `metadata-admin/previews/object-fields-bridge.ts` module, and the three + prose references that still described it as wired. + + The module exported `bridgeFromDraft`, `commitToDraft` and `FieldsBridgeResult` and had + **zero importers** — re-measured on the merged base, not inherited from the filing. Nothing + in the repository could reach it either: `@object-ui/app-shell`'s `exports` map declares + only `.` and `./styles.css`, so the file was not addressable as a deep import even from + outside the workspace. + + Removing it is not the whole change. Three comments — in `types/src/designer.ts`, `types`' + `designer-field-types.test.ts` (twice) and `fields`' `richtext-cell-renderer-5452.test.tsx` + — cited the bridge as a live corroborating source. Left behind, they would have swapped + dead code for false documentation: three in-repo pointers telling a future reader that this + bridge mediates between the framework field record and `FieldDesigner`, and nothing telling + them it is unreachable. The two that named it as the consumer deriving an editable-subset + check from `DESIGNER_FIELD_TYPES` now name `MetadataFieldsPage`, which does exactly that + with the same idiom and the same `objectui#3017` anchor. The third cited the bridge's + `richtext` → `html` mapping as one of three corroborations that `richtext` stores HTML; the + other two (the showcase seed and the field-type decision tree) are live and carry the point + on their own, so that clause is dropped rather than repointed. + + No behaviour changes: nothing imported the module, so there is nothing to migrate. +- 11f23e9: `ActionParam` has one authority again (objectui#6329). The name was declared three times, + not twice as the card counted: `@object-ui/types` publishes it — derived from the spec's + `ActionParamSchema` input, with its own parity suite — and `ActionDefaultInspector.tsx` and + `ActionPreview.tsx` each carried a module-local `interface` of the same name. app-shell + already read the published one elsewhere (`utils/resolveActionParams.test.ts`), so both + locals were shadows. They are deleted, not reconciled against each other, under the + 2026-08-25 family ruling 甲A1. + + Neither shadow needed a member the published type lacks, so nothing was added to the + published surface. What the shadows got wrong was the DECLARATION, in the direction that + lets wrong metadata compile: + + - The inspector's copy carried `[k: string]: unknown`. An index signature admits every key + at type `unknown`, so a commit of a key `ActionParamSchema` rejects by name — `.strict()`, + and `referenceTo` is listed in its alias map — type-checked here and failed on save. It + also made the two copies look compatible when they were describing different authoring + surfaces: `options` / `helpText` / `defaultValue` were declared outright on one side and + swallowed as `unknown` on the other. + - The preview's copy declared `label?: string | { en?: string }`, admitting the `en` tag and + no other, while its own `localize` helper has always read `Object.values(o)[0]`. An inline + locale map keyed `fr-FR` rendered correctly and failed `tsc`. The published `I18nLabel` + admits both authorized forms, so the type now matches what the code already did — this + widens the declaration, not the runtime's acceptance. + + Two behaviour-visible consequences, both of them the local `type?: string` being withdrawn + in favour of the published `ResolvableParamFieldType` (the spec's 49-member `FieldType` plus + objectui's three declared param aliases): + + - `ActionPreview.renderFieldMock` no longer branches on `long_text` or `integer`. Neither is + in that vocabulary — `long_text` belongs to the console form-builder dialect and `integer` + to JSON Schema — so a param spelled either way is a parse rejection on the server and + could never have reached the preview. The two comparisons compiled only because the local + copy typed `type` as `string`. + - The inspector's param-type dropdown narrows its commit through the runtime witnesses + `@object-ui/types` exports (`ACTION_PARAM_FIELD_TYPES` + `OBJECTUI_LOCAL_PARAM_FIELD_TYPES`) + rather than writing the raw DOM string. An unrecognised spelling clears the key instead of + being written into metadata the server would refuse; the eight offered spellings are + unaffected, and are now checked against the vocabulary at compile time. +- 2eb8cae: `StarredApps` now filters out `nav`-type favorites before rendering (objectui#6335). + + `FavoritesProvider` has always documented `nav` favorites (sidebar entries pinned via + the in-tree pin toggle) as "Excluded from Home/Starred and from the generic sidebar + Favorites list so it doesn't render twice" — but `StarredApps` filtered nothing by type, + so a `nav` favorite handed to it rendered anyway. `FavoriteItem['type']` also has no + `nav` key under `home.recentApps.itemType.*` (only five of its six members do), so a + `nav` card that reached `StarredApps` fell through to the raw `"nav"` label instead of a + translated one. + + `StarredApps` now filters `items` to `type !== 'nav'` before rendering — the same + exclusion already applied to the sidebar Favorites list in `AppSidebar.tsx` and + `UnifiedSidebar.tsx` — restoring declared-equals-enforced behaviour and making the + missing locale key correct by construction. No locale packs were touched, and the + `FavoriteItem` / `RecentItem` union types are unchanged. +- 6e88630: One authority per exported type name, batch 3 of objectui#6349: `ComboboxOption`, + `NamedActionDef`, `OrgTranslate`. + + **`@object-ui/components` — `ComboboxOption` now IS `@object-ui/types`' declaration.** + The component declared its own `{ value, label }`, a strict subset of the + `ComboboxOption` that `@object-ui/types` declares for `ComboboxSchema.options` and + mirrors in `form.zod.ts` (`{ value, label, disabled? }`). The component now re-exports + the types declaration (through the `@object-ui/types/form` subpath — the root barrel + does not publish the name), so the name `ComboboxOption` exported from + `@object-ui/components` gains the optional `disabled?: boolean` member. Every value + that type-checked before still does — nothing narrows and no key changes type; the + one thing that moves is `keyof ComboboxOption`, so a consumer that EXHAUSTS the type + (a `Record` over its keys) will need the new key. Note that the `Combobox` component + itself does not read `option.disabled` — that member was already declared on the + `@object-ui/types` face and is now visible on this one too; it is recorded as a + separate finding, not changed here. + + **`@object-ui/plugin-grid` / `@object-ui/app-shell` — internal, surface unchanged.** + `NamedActionDef` was declared identically in `resolveBulkActions.ts` and + `resolveLegacyRowActions.ts`; the latter is now the one authority and the former + re-exports it. `OrgTranslate` was declared identically in `orgErrorMessage.ts` and + `orgRoleLabel.ts`; the former is now the one authority and the latter re-exports it. + Neither name is on its package's public entry, and every deep-`dist` module still + exports the same name with the same shape. + + `FilterBuilderCondition` / `FilterGroup` (the other two names this batch was sized + with) are deliberately NOT converged: their shapes disagree on `id`, `value` and on + `operator`, and the only dependency-legal re-point would retype `operator` — the + vocabulary objectui#7561 is asking a maintainer to rule on. +- 6630b37: The console boot no longer flashes a fully-white frame after the splash has painted + (objectui#6378). + + Cause, established by measurement before any fix was written — the card named + `LoadingScreen`'s unmount timing and `RouteFader` as suspects and both are exonerated. + A CDP `Page.startScreencast` frame ledger (every frame classified with the card's own + rule: white when no colour channel falls below 242) was correlated against a DOM-state + ledger on the same clock (`performance.timeOrigin`), against the production + `apps/console` bundle with the boot endpoints mocked. `RouteFader` never mounts on the + boot path at all, and `LoadingScreen` unmounts exactly when its own gate says to. What + is wrong is what REPLACES it: every readiness gate renders the splash while it waits and + a bare `<Navigate>` the moment it decides. `<Navigate>` renders `null` and react-router + runs the navigation as a transition, so the destination tree renders while the commit + that already dropped the splash is what the compositor is showing — 41–147 ms during + which `#root` holds no view and the viewport is the bare page background. The flash is + intermittent only because it depends on a frame being swapped inside that window; the + window itself was present on every measured boot. + + `RedirectWithSplash` (new, `@object-ui/app-shell`) pairs the same `<Navigate>` with the + same `LoadingScreen` the gate one line above was already rendering, so the handoff + changes no pixels and the transition runs underneath an unchanged screen. The console's + three boot redirects use it: the auth gate's `/login` bounce, the `/` landing resolver, + and the catch-all route. The nested organization `index` redirect deliberately does not — + it fires under an already-painted layout, where covering the screen would be the + regression. + + Acceptance campaign — same instrument on both sides, the two arms INTERLEAVED inside one + process and one browser so drift in this shared container's load lands on both equally. + 102 paired boots per arm across five cells (signed-out `/`, signed-in `/`, an unmatched + entry, and the card's two throttled network profiles). The empty-viewport window: 102/102 + pre-fix, 0/102 post-fix. The white frame itself, pooled over the three cells where the + pre-fix build actually flashed: 67/87 (77%) pre-fix, 0/87 post-fix — 95% upper bound on + the residual rate 3.4%, against a card-reported defect rate of ~1/3. The two throttled + cells are reported but NOT pooled: the pre-fix build flashed 0 times there, so before and + after agree and those cells prove nothing about the pixels (they still separate 15/15 vs + 0/15 on the DOM window). + + `e2e/console-boot-indicator.spec.ts` gains the deterministic half as a gate — after + React's first commit the viewport centre must never stop being covered. That reading is + what makes an intermittent defect gateable: the flash needs a frame to be swapped inside + the window, but the window itself was present on every measured boot. Verified red-first, + 6/6 red on the pre-fix bundle and 6/6 green on this one. +- a672ae9: `app-shell`'s two action runtimes now reset `ActionParamDialog` the same way when it closes + (objectui#6431). `useConsoleActionRuntime` closed with `setParamState({ open: false, params: [] })` — + replacing the whole state object, emptying `params` and dropping `title`, `description` and + `resolve` — while `RecordDetailView`, which mounts a second runtime into the same dialog, + closed with `setParamState(s => ({ ...s, open: false }))`. The console runtime moves onto the + field-preserving shape. + + The user-visible effect is in the fade-out. `DialogContent` carries + `duration-200 data-[state=closed]:animate-out`, so Radix keeps the content mounted through + its exit animation and the dialog goes on rendering off `state` for the whole ~200ms. Under + the blanking shape a params form the user had just filled in re-titled itself from the + action's own label to the generic "Action parameters", swapped the action's description for + the generic one, and dropped every param row — an empty, generically-labelled box fading out + where a form had been. The confirm pair converged on the same shape for the same reason in + objectui#6034; this is that ruling re-measured on this dialog rather than inherited, because + `ParamDialogState` carries a form rather than display text and "blank it on close" could have + been deliberate here. + + It was not, and nothing else changes for the user. The values a user types are not in + `paramState` at all — they live in `ActionParamDialog`'s own `values` state, which its + `useEffect` reseeds from the param defaults on every `state.open` false→true edge, so a + reopened dialog starts blank under either reset shape. The retained `resolve` is inert: the + dialog settles the promise before it asks for the close, and the open path replaces the whole + state object. +- 7fdf74e: A nav / area / field `visible` predicate that FAULTS now says so, in both builds, once per + distinct predicate source (objectui#6443). Observability only — no verdict moves. + + `ExpressionProvider.evaluateVisibility` is the gate behind a navigation item's `visible`, + an area's derived visibility, and the field list `RecordFormPage` renders. It is fail-open: + a predicate that cannot be evaluated returns `true`, so a menu entry whose role gate has + stopped working renders **for everyone — including the role it was written to exclude** and + looks exactly like an entry the author meant to show. + + That fault was swallowed one layer down. `evaluateCondition` is fail-soft: it answers an + unevaluable predicate with `true` from its own `catch` and does not throw, so this site's + `try/catch` never saw a predicate fault at all. Measured per dialect at this site before the + fix — the bare-string dialect, the one a live gate was measured breaking on, printed + **nothing at all**: + + | dialect | console at this site, before | after | + |---|---|---| + | bare string | nothing | one named line | + | `{ dialect: 'cel' }` envelope | one generic line | one named line (the generic one is *replaced*, not added to) | + | `${…}` template | one generic line **per evaluation** | one named line, deduped | + + The fix wires `EvaluationOptions.onFault` (the seam objectui#6038 landed) to + `reportUnresolvableVisibilityPredicate`, exported from `@object-ui/react` — the same + reporter, message, severity, dedupe `Set` and rate limit the node gate and `page:tabs` + already use, so one authored predicate is entitled to one line rather than one line per + package. It costs no extra engine call: the evaluator hands back the reason at the point it + already knows the predicate faulted, with no `throwOnError` double evaluation. + + A nav item is not a schema node, so the reporter's `type` slot — which, with the gate key + and the predicate source, is the dedupe key — is the constant `app-shell:visible`. The rate + limit is therefore **one line per distinct authored predicate source**, not one per menu + entry: a broken role gate copy-pasted across eight entries is one authoring mistake, in one + string, fixed in one edit. + + **Fail-open is unchanged.** The item still renders for everyone on a fault. Flipping that to + fail-closed is a permission-boundary change, not a diagnostic, and is not this change's to + make; the change makes the silence stop and nothing else. +- 830ed58: The record page's approval band offers its **Recall** button to the approval's submitter + only (objectui#6464). + + Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval; + user B — not the submitter, read access, not an admin — opens the record and the band still + lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes + on submitter identity and refuses everyone else, so the only outcome available to that + button was a failure toast. Record state was never at risk; this was purely a + writability-feedback mismatch, the same family as objectui#3794. + + The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at + all" — which is a question about the DataSource, not about the viewer. Identity now joins + it, threaded the way every other signal on that band already travels: the HOST resolves it + and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and + never re-derives who submitted what. + + - `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on + `InlineEditContextValue`. Additive and optional; no existing prop changes. + - `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a + resolved `false`. + - `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing + approvals read and threads it. + + **The signal is tri-state, and the third state is the load-bearing one.** `true` offers + recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity + — renders exactly as it did before this release.** Omission preserving prior behaviour + mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide" + would have traded a cosmetic defect for a functional loss: every host whose band runs off + the record's `approval_status` mirror alone would silently lose its submitter's only way to + unlock their own record. + + **Withdrawn rather than disabled-with-reason.** The card offered either. For a + non-submitter this control is never actionable on any pending record, so a permanently + disabled button is standing clutter rather than a lesson; and the two sibling submitter + levers already hide — the approvals panel's Remind button, and the declared + `approval_recall` action's `visible` predicate. The band, its quorum tally and the + approvals timeline still tell a non-submitter exactly what state the record is in. Only the + lever they can never pull is gone. + + **This changes no permission.** Nothing about what the server allows moves, `canEdit` and + the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an + authorization verdict — the recall endpoint remains the sole authority, and it refused + these callers before this change and refuses them after. There is deliberately **no admin + carve-out** (the reporter ruled that case out, cf. objectstack#9464). + + The derivation itself is now one function, `isSubmitterOf` — server-resolved + `viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends + that predate it, joined with `??` so a server that resolved `false` is believed rather than + re-litigated client-side. The approvals panel's Remind gate, which already carried that + expression inline and whose behaviour is unchanged, now reads the same answer: two copies + would have been two definitions of who submitted. + + The **untranslated refusal text** the reporter also saw ("No pending approval request found + for this record", concatenated after a localized prefix) is a separate defect and is not + addressed here; it is tracked on objectstack#11993. +- 586cfc2: The metadata-admin type-action param dialog no longer blanks itself while it closes + (objectui#6473). + + `MetadataTypeActions` is the **third** consumer of `ActionParamDialog` in this package, + after `useConsoleActionRuntime` and `RecordDetailView` — and it was the last one still + writing `setParamState({ open: false, params: [] })` on close, replacing the whole state + object. `DialogContent` carries `duration-200 data-[state=closed]:animate-out`, so Radix + holds the content mounted through its exit animation and the dialog goes on rendering off + `state` for the whole fade-out: a user who cancelled "Test connection" or a datasource + sync watched the heading revert from the action's own label to the generic + `actionDialog.title` and every param row disappear, for 200ms, on the way out. Close now + flips `open` and keeps every other field, which is the shape objectui#6431 converged the + other two consumers on. + + Not a user-data change: the values typed into the dialog never lived in `paramState` — + they live in `ActionParamDialog`'s own `values`, reseeded from the param defaults on every + open — so a reopen still starts from the defaults, pinned as a control. + + The pre-reset `paramState.resolve?.(null)` is dropped as well, on an enumeration rather + than on "resolving twice is a no-op": `onOpenChange` is reachable from exactly three places, + all inside `ActionParamDialog`, and every one settles the promise before asking for the + close — `handleSubmit`, `handleCancel`, and the Radix root handler that delegates to + `handleCancel` (the single route Escape, an overlay click and the header close button all + take). All four routes are driven in the new test, with a census over + `ActionParamDialog.tsx` so a later call site that skipped the settle is red there instead + of leaving a promise pending forever. +- 16aa044: `MetadataService.saveFields` no longer PUTs the framework's own read decorations back (objectui#6480). + + `saveFields` fetches the current object and spreads it verbatim (`...existingObject`) so that every key the service does not model survives a field save. That spread does not distinguish keys the **author** owns from keys the **framework** adds on the way out: `@objectstack/spec` declares `_diagnostics` and `_draft` as `METADATA_READ_DECORATIONS` — stamped onto served metadata documents by the read path — and `ObjectSchema` refuses both **by name**. A served document carrying either one was therefore spread straight back into the body of `PUT /api/v1/meta/object/:name`. + + The body now passes through the spec's own exported `stripReadDecorations` before it is sent, so the list of decorations stays the spec's rather than a local copy that goes stale the next time the framework adds one. This is the strip-on-write shape `MetadataObjectsPage.handleObjectsChange` already uses for `group`, applied where the spread is — simply not writing the key is not enough when the spread is verbatim. + + The strip is deliberately bounded to those two keys and is not a general "remove whatever the schema refuses" pass: an off-spec key the author owns still goes out and is still refused loudly, where someone can see it. Nothing is lost by dropping the decorations even though a PUT is an upsert — `_diagnostics` is the read-path validation verdict, recomputed on every read, and `_draft` reflects the row's `state` column and the `mode` parameter, never the body. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) *is* write-path state the server merges back, and the spec deliberately keeps it out of the decoration list, so it is untouched. +- 28ed9ce: `MetadataService.saveFields` carries the server's per-FIELD keys through a field save + instead of rebuilding every entry from the designer model (objectui#6488). + + The method preserved unknown keys of the OBJECT document by spreading it, but that spread + is object-level and said nothing about keys INSIDE a field. Every entry was rebuilt by + `toFieldPayload`, so every key the server sent inside a field that the designer does not + model was dropped on every field save: `expression` (a formula authored in metadata-admin), + `precision`, `scale`, `system`, `sortable`, and anything a plugin registered. Measured + against the installed `@objectstack/spec` 17.2.0, `FieldSchema` accepts all five — the + designer's model is a subset of what a field may hold, and the difference was being + deleted. + + The loss is not new but was UNREACHABLE. While `fields` went out as an array the whole + body was refused `422 INVALID_METADATA` before persistence, so nothing `saveFields` + dropped ever reached storage; objectui#6240 made the body parse, and a PUT is an upsert, + so from that fix onward the drop lands. + + `toFieldPayload` now merges onto the previous SERVER entry, read from the document + `saveFields` already fetches for the object-level spread — the form + `MetadataFieldsPage.fromDesignerField` has used one writer over all along, and no extra + request. + + Two properties keep the fix from becoming its own mirror image, both pinned in + `MetadataService.fieldKeyCarryOver.test.ts`: + + - **A property the author CLEARED stays cleared.** Every modelled key is still written + unconditionally, so a cleared property arrives as an explicit `undefined` that overrides + the carried value and is dropped by `JSON.stringify` — absent from the body, which on an + upsert is the deletion. A conditional merge would leave the server's old value standing + and fail the author's deletion silently. + - **Retired designer keys do not ride back out.** `indexed`, `referenceTo`, `formula`, + `isSystem` and `sortOrder` are refused BY NAME by `FieldSchema`; a stored document can + still carry them, and echoing one back is a hard 422 that blocks every later save of the + object with no UI path to clear it. Everything else the server sent still survives — the + strip is keyed to those tombstones, not a blanket unknown-key purge. +- b1a732b: Keep the boot splash painted across seven more console redirects (objectui#6507) + + Every readiness gate on the console boot path renders `LoadingScreen` while it + waits and a bare `Navigate` the moment it decides. `Navigate` renders null and + react-router runs the navigation as a transition, so the destination tree + renders while the commit that already dropped the splash is what the compositor + shows — measured at 41-147 ms of empty `#root` on the three sibling gates + objectui#6506 fixed. + + Converted to `RedirectWithSplash`, which pairs the same navigation with the same + `LoadingScreen` so the handoff changes no pixels: + + - `RequireOrganization` — both decisions (orgs exist but none active; no org at + all with multi-org enabled) + - `RequireAiSurface` — a runtime that serves no agent + - `AuthenticatedRoute` — the signed-out fallback (published for consumers; + `apps/console` converted its own `ProtectedRoute` copy under objectui#6506) + - `RootRedirect` — byte-for-byte the shape that measured the widest window + - `SetupRedirect` — the `/setup` deep link + - `AppContent` — the no-accessible-app bounce, which returns above the single + `ConsoleLayout` mount + + `SystemRedirect` is deliberately left as a bare `Navigate`. It carries the same + shape on a first navigation, but it is the only site in this set that also fires + with the console already painted (`SettingsView` navigates to `/system/settings` + from a button; `AppSidebar` links to `/system`), and a redirect firing under an + already-painted layout must keep that layout rather than gain a splash. The five + URL-rewrite redirects in `AppContent` are excluded for the same reason. +- d4a9a55: Field- and action-visibility gates on the full-screen record form page now see the same + `current_user` every other console surface sees (objectui#6515). `RecordFormPage` built its + own descriptor — `{ name, email, role, positions }` — instead of calling the shared + `buildExpressionUser` normaliser, so `id` and `isPlatformAdmin` were simply absent from the + predicate scope that page publishes. + + An absent key is not `false`. A predicate naming one of them FAULTS, and a faulting + visibility predicate fails OPEN, so the gate silently did not bite: a field gated on + `ctx.user.isPlatformAdmin == true` (the shape `sys_environment`'s "Change Plan (admin)" + action uses) rendered for every user on this page, and an id comparison against + `ctx.user.id` (the shape `sys_user`'s own gates use throughout `platform-objects`) did the + same. Nothing on screen distinguished that from a gate that had said yes. The signed-out + branch diverged on its own account too — it carried no `isPlatformAdmin` key at all, where + `buildExpressionUser(null)` carries `false`. + + Fail-open on a genuine evaluation error is deliberately unchanged (objectui#6443 / #6487 / + #6445); what changed is that these predicates no longer fault in the first place. + + The normaliser moved from `console/AppContent.tsx` to `providers/expressionUser.ts`, beside + the `ExpressionProvider` it feeds. That move is what made the fix available: `RecordFormPage` + is `lazy()`-loaded BY `AppContent`, so importing the normaliser from its old home would have + put a static edge from the split chunk back into the module it was split out of. Both + `console/AppContent.js` and the package entry re-export the name, so `buildExpressionUser` + is published exactly as before. +- c18acb0: The object designer's field-IO read door now strips `referenceTo` and `isSystem` alongside + `indexed`, so a draft authored before those controls were retired can be edited and saved + again (objectui#6519). + + `previews/object-fields-io.ts` is the single read door for `draft.fields` across the whole + object designer — inspector, form designer, design surface, settings / validations / API + panels — and `writeFields` writes each def back verbatim. Its strip set named one key while + `FieldSchema` refuses five by name, so a stored field carrying any of the others + round-tripped straight back out to `PUT /api/v1/meta/object/:name`. Measured on the + installed `@objectstack/spec` 17.2.0, through the whole document that endpoint validates: + + ``` + ObjectSchema.safeParse({ name:'account', label:'Account', + fields: { amount: { type:'number', label:'A', referenceTo: 1 } } }) + => unrecognized_keys at ["fields","amount"] + ``` + + which is the hard `422 INVALID_METADATA` that blocks EVERY later save of that object, with + the control that wrote the key retired and no UI path left to clear it. This is the shape + objectui#4644 closed in this same file for `indexed`, applied to the siblings that were + left open. + + Both added keys were verified to be reachable rather than assumed: `referenceTo` was + emitted by both designer writers until objectui#6041 (`MetadataService.toFieldPayload` and + `MetadataFieldsPage.fromDesignerField`), and `isSystem` was a declared server-field key the + designer read back until objectui#6044. Neither loses anything — the spec spellings + `reference` and `system` are separate, accepted keys and ride through untouched. + + Two keys `FieldSchema` also refuses are deliberately NOT stripped, each for its own + measured reason, and the tombstone on `RETIRED_FIELD_KEYS` carries both in full: + + - `formula` (objectui#6043) — `ObjectFieldInspector` seeds its linting CEL editor from + `def.expression ?? def.formula` and the first edit commits `expression` and clears the + alias. Stripping at the read door empties that editor and the authored source is gone on + the next save; objectui#6043 refused a blind rename precisely because that migration + surface exists. Dropping the text anyway is a maintainer call, raised on objectui#6519. + - `sortOrder` (objectui#6045) — no writer on this tree ever populated a FIELD-level one, so + no draft this door reads can carry one; a strip would be dead code that reads like a + measurement. + + Unifying the three retired-key lists on this seam is deliberately not part of this change: + it spans `plugin-designer/src/MetadataFieldsPage.tsx`, which objectui#6489 owns in flight. +- fa1348f: The object designer's client-side 422 on a draft carrying the retired `formula` + field key is now actionable (objectui#6526, adjudicated option B). The spec's + rejection at `fields.<name>` gains an appended pointer that names the field and + names the destination: select the field and make one edit in its Formula (CEL) + editor, which commits the value to `expression` and clears the retired alias. + + Presentation only — the verdict, issue set and paths are unchanged, and nothing + about what the gate accepts changes. The migration path itself is untouched: + `RETIRED_FIELD_KEYS` still does not strip `formula` (objectui#6043's ruling), + and the object stays unsaveable until the author makes that one edit — the + ruling's accepted cost, now with a signposted way out. The pointer fires only + for `formula`-type fields, where the inspector actually renders that editor + (objectui#4306); any other field type keeps the bare spec message. +- 3e028c8: One tombstone registry for the designer seam's retired field keys + (objectui#6527). Three independently maintained `RETIRED_FIELD_KEYS` literals + — the metadata-admin read door (`object-fields-io.ts`), `MetadataService`'s + carry-over and `MetadataFieldsPage`'s carry-over — become derivations from a + single registry in `@object-ui/types` (`RETIRED_FIELD_KEY_TOMBSTONES` + + `retiredFieldKeysFor(site)`), naming each retired key, the card that retired + it, and its PER-SITE applicability. + + Per-site behaviour is unchanged — this is a consolidation, and each site's + effective strip set is pinned equal to its pre-consolidation literal. The two + deliberate asymmetries a naive union would have destroyed are now recorded as + data and pinned: + + - `formula` stays stripped by the two write-side carry-overs and is NOT + stripped by the read door — ruled on objectui#6526 (option B): the + `ObjectFieldInspector` migration path (objectui#6043) stands, and the + registry test makes that ruling mechanical. + - `sortOrder` stays a single-site strip at `MetadataService`'s carry-over, + now explicitly recorded as the registry's one DEFENSIVE entry (objectui#6045 + measured that no shipped writer ever populated a field-level one). +- d0889e2: Resolve a relationship target from `reference` only — the spec spelling + (objectui#6528). + + `resolveReferenceTo` (dataset designer) and its sibling + `resolveRelationshipTarget` (`chart-series.ts`) each read a relationship field's + target through a four-spelling tolerant chain — `reference ?? reference_to ?? + referenceTo ?? reference_to_object`. Measured against every producer that can + reach them, three of the four are unfounded, so the chain is narrowed to + `reference` in BOTH places in one pass (they must not diverge — a fix leaving + them disagreeing recreates the defect one file over). + + The census, with `reference` itself as the positive control every zero is + measured against: + + | spelling | `ObjectSchema.safeParse` (spec 17.2.0) | producers on the object-metadata surface | + |---|---|---| + | `reference` | ACCEPTED | live — both designer writers emit it; 445 of 565 lookup/master_detail defs in the framework tree | + | `reference_to` | REFUSED BY NAME | 0 (live only on ObjectUI's own view/field schema — a different contract) | + | `referenceTo` | REFUSED BY NAME | 0 (producers retired by objectui#6041; stripped by the read door since objectui#6519) | + | `reference_to_object` | REFUSED (not even an alias) | 0 anywhere in either tree, outside the chain and its own test | + + Behaviour change, and it is deliberate: `chart-series.ts` reads + `GET /meta/object/:name` directly, with no read door stripping retired keys, so + a stored pre-objectui#6041 row spelling the target `referenceTo` no longer + resolves there. The walk is best-effort by construction — no entry is yielded + and the caller keeps the raw value — so such a row degrades visibly instead of + being silently absorbed. Per AGENTS.md #0.1 that row is a producer-side defect, + and a lenient consumer is where it would have stayed hidden. `reference` was + already head of the old chain, so any document carrying both is unaffected. + + The string / array / `{ object }` carriers are untouched: the carrier is a + separate axis from the spelling and narrowing it needs its own census. +- c5037fd: `buildExpressionUser`'s signed-in cast declares `id`, `name` and `email` REQUIRED, so it is + no longer wider than the contract it mirrors (objectui#6551). + + The normaliser reads its input through a cast, and that cast wrote all three keys optional + (`{ id?: string; name?: string; email?: string; … }`) while the signed-in branch forwards + exactly those three RAW. So the declaration said `buildExpressionUser({ name: 'B', email: + 'b@c.d' })` was a legitimate input, and the code answered `{ id: undefined, … }` for it — + present-and-always-`undefined`, which is the shape objectui#5424 removed `roles` from this + same object for ("the shape that teaches the wrong thing") and the one objectui#6534 + refused for the anonymous branch, one key over. The three keys BELOW them already defended + with `??`; the asymmetry sat inside one object literal. + + The contract disagreed with the cast. Every production input is `useAuth().user`, typed as + `@object-ui/auth`'s `AuthUser`, which extends the spec's `AuthUser` + (`@objectstack/spec/contracts`): `id: string; email: string; name: string`, with only + `positions` and `tenantId` optional. `name` and `email` are narrowed alongside `id` because + that same interface declares them required too — the same answer from the same authority, + not a widened scope. `role` stays optional (it is `@object-ui/auth`'s display-only + addition, not a spec key), and the index signature stays (better-auth projects an app's + custom user columns onto this object, and it is how `isPlatformAdmin` / `positions` are + read). + + NOTHING REACHABLE CHANGES, and deliberately so. The only production producer is a + better-auth principal that always carries `id`, which is why this was graded a latent shape + hazard rather than a bug, and why the fix moved a declaration and no runtime behaviour: the + defect was that the cast LIED about the contract. `id: u.id ?? null` was the rejected shape + (triage, 2026-08-26) — a lenient default in the consumer is what AGENTS.md #0.1 forbids and + what objectui#6534 shipped a scope fence against, and it silently equates "signed in, no + id" with "signed out". A producer without an `id` is wrong at the producer. + + Because `id?: string` and `id: string` produce byte-identical output for every input a + producer can supply, no runtime assertion can pin this; the new + `expressionUser.sessionContract.types.test.ts` drives `tsc` itself over the real + declarations and carries its own discrimination leg — the same cases compiled a second time + against the pre-fix optionality, with the five that flip named by index. + + No fault-handling path moved. Fail-open on a predicate that DOES fault stays deliberate + policy (objectui#6443 / #6487 / #6445). +- 12402a9: Drop the undeclared object-level `titleField` from the object page's five + remaining view-config seams, and from the record-search memo key + + `ObjectView` resolves a title field for seven view kinds. Two of them — + calendar and gantt — already read `viewDef.<kind>?.titleField || 'name'`. The + other five (timeline, kanban, map, gallery, tree) carried a three-rung chain + with `objectDef.titleField` in the middle, so one file answered the same + question two different ways. This converges the five on the shape the two + siblings already had; it is a convergence, not a removal. + + The middle rung could never fire for legal metadata. `@objectstack/spec`'s + object schema is a `strictObject`, so + `ObjectSchema.safeParse({ …, titleField: 'x' })` is rejected with + `unrecognized_keys` — the same issue code a nonsense key gets — while + `nameField`, `displayNameField` and `titleFormat` all parse (measured against + `@objectstack/spec@17.2.0`, the dist this repo installs). objectui#6531 + established that measurement and dropped the twin read inside + `getRecordDisplayName`. Reading a key no producer can ship is the + consumer-side alias AGENTS.md Commandment #0.1 bans. + + Behaviour for every legal config is unchanged, and both directions are pinned: + a view that declares its own `titleField` still wins on every kind, a view that + declares none still floors at `'name'`, and an object carrying the + contract-rejected key is now honoured by no kind. Re-pointing the middle rung + at the declared `nameField` was considered and rejected: it would have added a + rung calendar and gantt do not have — increasing the divergence — and, unlike + this change, it would have altered behaviour for legal configs. + + `useRecordSearch`'s candidate signature — the memo key that decides when the + cross-object fanout re-runs — appended `o?.titleField ?? ''` to every entry. + Because no legal object definition can carry the key, that half was permanently + `''`: a constant suffix in a cache signature, and the last thing in the repo + that read as evidence some producer supplies it. The signature is now the object + name alone, which is the only field of an object definition the effect actually + consumes. Change detection is unaffected — a changed candidate name still + re-runs the fanout, and a new array with identical content still does not. +- 5534009: Studio's "duplicate base" now reports a partial or empty duplicate as a failure instead of + a complete success (objectui#6593). + + `duplicatePackage()` read `success` at the **top level** of the response body. That is the + runtime dispatcher's envelope — `deps.success(result)` answers + `{ success: true, data }` — so on every HTTP 200 the flag it read was `true` by + construction. The operation's own verdict lives one level down in `data` and is a real + three-state: the server computes it as `failed.length === 0 && copied.length > 0`. + + Two outcomes therefore answered 200 with `envelope.success: true` while the duplicate had + not succeeded, and both were shown to the author as "created", followed by a navigation + into the new base: + + - **partial** — some items failed to copy. `failed[]` carries a per-item `error` string + that is the only place the reason is ever stated, and none of it was read. + - **empty** — nothing was copied at all, e.g. an all-env-wide source package under a + session that resolves no active organization (`copiedCount: 0`, `failedCount: 0`). + + `duplicatePackage()` now unwraps `data` before reading the operation flag, and rejects with + a message built from what actually happened: the copied/failed counts, plus each + `failed[].error` (the first five by name, then a `+N more` tail). A generic `HTTP nnn` + message is deliberately not sufficient for the partial arm — it is the half an author needs + to act on. The non-2xx arm is unchanged and still surfaces the error envelope's message. + + The unwrap-then-read order is the one `revertCommit` already uses for the sibling + commit-revert route in `preview/commitHistory.ts`; this is one consumer converging on that, + not a new convention. The route's underlying contract absence (it publishes no response + schema, so reading the wrong `success` typechecks perfectly) stays upstream in + objectstack#12038. +- bac7ba4: The metadata-admin permission matrix no longer authors the retired object-permission + bits `allowRestore` / `allowPurge` (objectui#6595). + + The `Re` and `Pu` columns, their two typed fields, the two preview rows, and the + "Purge (hard delete) granted without Delete" sanity check are gone, together with the + two column tooltips in both locale tables. `allowTransfer` is enforced upstream + (objectstack#3004) and is untouched — it stays a column. + + Both removed keys gated `restore` / `purge` ObjectQL operations that **have never + existed**: a dispatched restore/purge is denied unconditionally by the evaluator's + fail-closed destructive-operation backstop. So every tick of those checkboxes wrote a + grant no runtime has ever read, and the preview lint warned about a combination whose + danger was entirely notional. `@objectstack/spec` retired both keys as `retiredKey()` + tombstones (objectstack#12497; maintainer ruling 2026-08-26 accepting objectstack#1883 + recommendation B, ADR-0049 enforce-or-remove), which turns the same checkbox into a save + that hard-fails at publish once the bump carrying that retirement reaches this repo. + + **The return path is named in the code, not just here**: both keys come back with the M2 + lifecycle initiative, whose restart is recorded upstream on objectstack#1883. The + tombstone on `ObjectPerm` in `permission-slice.ts` states it, and the two `retiredLifecycleKeys` + pins name it again — a future reader who wonders where the columns went finds the answer + at each of the three sites the removal touched. + + **A stored legacy value is carried through, not stripped.** It is no longer modelled and + no longer authorable, so it rides through save untouched exactly as any key this editor + does not model does — the record-level index signature on `PermissionSetDraft` states + that rule, and `updateObjectPerm`'s spread applies it per row. Stripping was deliberately + left out: the installed `@objectstack/spec` (17.2.0, measured 2026-08-27) still **accepts** + both keys at permission parse, so a strip today would delete stored data the schema still + honours. Once the bump lands and a carried value becomes a body the schema refuses, + strip-on-load becomes correct — that is objectui#4644's resolution for `indexed`, and it + belongs to the bump PR. The pin that records today's posture says so in its own header, + so the bump replaces it deliberately rather than deleting a red. +- 38268ab: Permission matrix bulk buttons (R / CRUD / All) now merge into the object's + permission row instead of replacing it, so spec-declared keys the matrix does + not author — `allowExport` and the ADR-0057 access-depth axis `readScope` / + `writeScope` — survive a bulk click the same way they already survived the + per-checkbox path. Previously one click on any bulk button silently dropped + them from the saved row, and the **All** button could widen effective read + access by deleting a `readScope: 'own'` narrowing with no diff and no error. + **None** deliberately keeps clearing the whole row, narrowings included: + merging there would leave `allowExport: true` alive after a click on the + button labelled None (objectui#6605). +- 813bf83: **metadata-admin predicates: `in` with a path on the right is now diagnosed in + dev mode instead of failing silently.** No verdict changes — this is a + diagnostic only. + + The Setup/Studio predicate evaluator matches membership as `path in ['a','b']`: + the right-hand side must be a bracketed literal set. A membership test whose + right side is a **path** therefore never matched that branch at all. Carrying no + `==` / `!=` either, it fell through to the bare-truthy tail and the *whole* text + was evaluated as one operand: + + - `'admin' in current_user.positions` — ADR-0068's own headline example, and the + spelling `SelectOptionSchema`'s docblock names as the canonical use of the key + — leads with a quote, so it came back verbatim as a non-empty string. A + non-empty string is truthy, so the predicate read **TRUE for every user**, + whatever `positions` held. The failure direction is **permissive**: an option, + field or section gated to admins rendered for everyone. + - `data.roles in current_user.positions` — path-shaped, so the resolver walked + off the draft mid-path and the predicate read **FALSE for every row** instead. + + Either way the verdict had nothing to do with the membership that was written, + and nothing appeared in the console: objectstack#6936's unresolved-path warning + hangs on the path resolver, which quote-leading text never enters, and + objectui#4049's path-shaped-literal warning only matches text starting with an + identifier character. + + Such a predicate now emits a warn-once dev-mode diagnostic naming the offending + text, the predicate that carried it, and the supported subset — and stating + plainly that **a path on the right of `in` cannot be written on this surface + today**, rather than implying that some other punctuation would work. The + detection reuses the evaluator's existing quote-aware scan, so a predicate that + is itself a quoted literal containing the word (`'plug in adapter'` — correct + code) is *not* accused. + + Nothing is resolved that was not resolved before and no operand handling is + added; every predicate that evaluates today reaches exactly the same answer, + pinned before and after. The semantic fix belongs to publish-time validation of + predicate expressions (objectstack#7010) and to the real CEL runtime — this + evaluator is an interim stand-in for `@objectstack/formula` and this diagnostic + retires with it at ROADMAP M9, the same as objectui#4049's and objectui#4266's. + + objectui#6617. +- 05db23b: `provisionProductionEnvironment` reads the created env from the nested `environment` row + + `POST /api/v1/cloud/environments` answers `{ success, data: { environment, warnings, + durationMs, hostnameAssignment? } }` — the created row sits one level down, under + `environment`. The consumer read `data` FLAT and returned it as a + `ProvisionedEnvironment`, so `id` and `hostname` were always `undefined` and the + envelope's siblings rode along in their place. + + The bug was silent by construction: both fields are optional on the type, the whole call + is best-effort by contract (a 403/409 resolves to `alreadyProvisioned: true`) and the + caller swallows genuine failures — so the function reported a successful provision + carrying no environment at all, which is the exact outcome the strict envelope check in + that file was written to prevent. That check verifies `data` is an object and nothing + about its shape. + + The fix reads ONE dialect: no `data.environment ?? data` alias, and the row is projected + to `{ id, hostname }` rather than returned whole. A wrong-shaped `data` still RESOLVES + rather than throws — tightening the envelope check to reject it would change behaviour on + the best-effort path the caller relies on swallowing, and is deliberately not folded in + here. + + Scored `patch`, not an empty "no release" declaration: this is shipped runtime code in a + published package whose return value is different, not a comment or a test-only change, + so an empty frontmatter would assert something false. Not `minor` — no new capability and + no API surface change; `ProvisionedEnvironment` is unchanged. The blast radius is small + today (the sole in-repo caller, `CreateWorkspaceDialog`, discards the return value, and + the symbol is not on the package barrel), but "small" is not "unreleased". +- 4d5f9b4: Resolve a relationship target from a `reference` STRING only — the carrier axis + (objectui#6648). + + objectui#6528 narrowed both relationship-target resolvers to the single spec + SPELLING `reference` and left the CARRIER — the shape the value may take — + explicitly for its own census. That census is done, and it says the same thing: + `resolveReferenceTo` (dataset designer) and its sibling + `resolveRelationshipTarget` (`chart-series.ts`) each accepted three carriers on + the canonical key, two of which `FieldSchema` never declared. Both are removed + in BOTH files in one pass (they must not diverge — a fix leaving them + disagreeing recreates the defect one file over). + + The measurement, with the bare string as the positive control every zero is + measured against: + + | carrier | `ObjectSchema.safeParse` (spec 17.2.0) | producers at the field-def key position | + |---|---|---| + | `reference: 'crm_account'` | ACCEPTED | live — 587 across both trees | + | `reference: ['crm_account']` | REFUSED — `expected string, received array` | 0 | + | `reference: { object: 'crm_account' }` | REFUSED — `expected string, received object` | 0 | + + The census walked STRUCTURE, not text: JSON/YAML parsed and walked, TS/TSX read + through the TypeScript compiler API, each hit recorded with its ancestor + property chain and its enclosing object's sibling keys so a FIELD DEF is + separated from the other tiers that also spell `reference` (a form field + literally named `reference`, its translation entries, a JSON-Schema property + descriptor, a liveness-ledger row). Every dynamic initializer at the field-def + position resolved to a string-typed source, and every `reference` TYPE + declaration in either tree declares `string`. The detector is not blind to the + shape it hunted — it DID report array and `{ object }` carriers, and every one + was a test asserting this very tolerance plus one framework lint fixture whose + own rule already reads string-only. + + The array branch was also a silent PRODUCT decision: handed a multi-target + value it returned element zero and DISCARDED the rest. Nothing declares such a + value. Polymorphic lookup is an open, unbuilt gap in the spec's own audit report + ("Current `reference` only supports a single target", Tier 3), and the + platform's one polymorphic reference (ADR-0018 `xRef`) is a STRING with a + sibling discriminator, never a list. A multi-target lookup, if it lands, lands + as a declared spec shape — not as a carrier a consumer guesses at. + + Behaviour change, and it is deliberate: a field def whose `reference` is not a + non-empty string now resolves to `undefined` in both helpers. Such a document is + already refused by `ObjectSchema`, so per AGENTS.md #0.1 it is a producer-side + defect, and a lenient consumer is exactly where it would have stayed hidden. The + two unit assertions that pinned the tolerant reads are converted to refusal + pins, so re-widening the carrier turns red. +- 350509b: Take the metadata-admin engine out of the console's eager closure (objectui#6776). + + `AppContent` has declared six `lazy()` imports of `views/metadata-admin/index.ts` + for a long time, and none of them deferred anything: the module ran five + registrations at load, so the package's published `sideEffects` array named it, + an array entry is unshakeable, and the package barrel re-exported 25 runtime + values from it — an ordinary static edge from an entry every consumer imports. + Every page, preview and inspector under `views/metadata-admin/` was therefore + fetched and parsed before first render. Measured from + `apps/console/dist/eager-closure.json`: **3,254,230 → 3,222,314 gzipped bytes, + −31,916 B**, and the 172,945-byte `metadata-admin` chunk leaves the eager set + entirely. + + **Published surface — two contract-bearing changes, no signature change:** + + - `packages/app-shell/package.json`'s `sideEffects` array now names + `views/metadata-admin/register-builtins` (the new leaf that performs the five + registrations) instead of `views/metadata-admin/index`. The five + registrations still run at package load, bare-imported by the package entry, + so nothing a consumer could observe changes — but the array is a contract + every consumer's bundler reads, so the swap is stated here rather than left + to a diff. + - The package barrel's 25 metadata-admin runtime re-exports (and 11 type-only + ones) now point at their leaf modules. **Same names, same types.** They are + unreachable from outside the package by any other path — `exports` is + root-only — so no import an out-of-package consumer can write is affected. + + `registerAppComponent`'s signature is unchanged. `metadata:directory` and + `metadata:resource` are now registered as `lazy()` values, each wrapping itself + in its own `Suspense` boundary, which is the shape the already-lazy + registrations in `apps/console` use; no render site changes. + + Also re-baselined `MAX_EAGER_CLOSURE_GZIP_BYTES` in the same change, from + 3,300,000 to 3,268,000. Taking 31,916 bytes out without moving the ceiling would + leave 0.85x of the 89 KiB regression the gate exists to catch as headroom — + near-blind — so the ratchet advances with the win rather than after it. +- 21993e4: The flow-node inspector now declares the spec's default for + `escalation.notifySubmitter` (objectui#6794). + + `FLOW_NODE_CONFIG`'s approval group declared **no `defaultValue`** for + `escalation.notifySubmitter`, while the installed `@objectstack/spec` (17.2.0) + defaults the key to `true` — `ApprovalEscalationSchema.safeParse({ timeoutHours: 24 })` + returns `notifySubmitter: true`. So the hand-written table stated the opposite of + what an omitted key does at runtime, and it disagreed with the **online half of + the same form**: a backend that publishes the approval `configSchema` sends + `default: true`, which `json-schema-to-fields` turns into `defaultValue: 'true'`. + Offline and online rendered the same key from two different claims about the + spec. + + `defaultValue` is what `isFieldVisible` resolves an unset controller against, so + the missing declaration is what a future field gating on `notifySubmitter` would + have read — and it is what the table asserts about the contract to anyone reading + it. + + A reconciliation assertion in `flow-node-config.spec-reconciliation.test.ts` + keeps the two sides pinned. It reads the expected value **out of the installed + spec** rather than pinning the literal `'true'`: objectui is the consumer of that + contract and must not become a second source of truth for it. A vacuity guard + alongside it fails if the spec ever stops materialising the key at all, so the + comparison can never quietly become a comparison against `undefined`. + + Deliberately scoped to `notifySubmitter`. The sibling controller + `escalation.enabled` is a separate, on-hold card (objectui#6620) whose default + flips on a *future* spec bump; installed spec and table agree on it today, and + generalising this assertion across the block would arm that card's tripwire here. +- 994b736: Studio design: say what is true when the metadata designer registries are + unpopulated (objectui#6795 part C). + + The three registries (`preview-registry`, `inspector-registry`, + `default-inspector-registry`) are plain `Map`s filled by a module-scope side + effect, and every studio-design consumer reads them **during render with no + subscription**. A consumer that reads an empty registry therefore gets + `undefined` and never recovers — measured: registering afterwards leaves the + consumer in its fallback forever. Four consumer states lied about that, and + one was silent: + + - **Data pillar field rail** — the guard was + `fieldSel && (fieldSel.kind === 'group' || inspector)`, so selecting a + **field** with no inspector registered dropped the whole rail: clicking a + field did literally nothing while the designer above it went on saying + "click a field to edit its properties". A selection now always opens its + rail, and the rail names the missing inspector. + - **Interfaces canvas** — "{type} shows a read-only preview for now; design + support is in progress" was false twice: the branch renders no preview at + all, and page design support exists. It is split by + `listMetadataPreviewTypes()` into the two causes that are actually + distinguishable — this type has no designer, or none are registered at all. + - **Interfaces rail** — no longer tells the author to click a canvas that is + not rendered. + - **Automations pillar** — the canvas chip and the rail both said "click a + node" while the canvas was a raw JSON dump. + - **`ObjectActionsPanel`** — rendered only the action's own label, which read + as "this action has no properties"; the label now carries the reason there + is no editor under it. + + ⛔ None of these messages promises recovery ("loading…", "try again", a + spinner): that would replace one false statement with another. Making the + registries observable so recovery is real is part A of #6795, which the ruling + deferred. `ObjectSettingsPanel` and `ObjectHooksPanel` are deliberately + untouched — the measurement found both already correct. +- 0049c52: `ActivityFeed` no longer drops a row whose activity kind it does not recognise + (objectui#6816). + + The notification filter was `activities.filter(a => notificationPreferences[a.type])` + — a truthiness test over a `Record<ActivityItemType, boolean>`, which answers the + same falsy value to two unrelated questions: "the user toggled this kind off" + (hide, which is the feature) and "this kind is not in the record at all". The + second made the row **vanish** from the panel. + + In-repo `tsc` keeps that case out of reach — three exhaustive + `Record<ActivityItemType, …>` tables force every member to be handled — but + `ActivityFeed` is published API, and a host that mounts it passes rows whose + `type` came from its own data. `sys_activity.type` is author-extensible and is + not validated on write, so those kinds are real, and a missing row is the least + detectable failure a feed can have. + + An unrecognised kind now fails **open**: the row renders, through the generic + `system` presentation `activityItemType.ts` already declares for a value outside + its mapping table (neutral on purpose, and in particular not `update`). A kind + that is present and toggled off is still filtered out — presence, not + truthiness, is now the question the filter asks. +- ecd9cb2: Wizard view v1, the objectui half (Card R, objectui#6985) — alignment + pins for the + ruled `type: 'wizard'` tightening (objectstack#13622 D1–D8, maintainer ruling + 2026-08-31; spec half objectstack PR #13733). + + The renderer was already aligned: `WizardStepConfig` carries no predicate/collapse + keys (objectui#6237's ruled split), the wizard route drops-and-reports an authored + step `visibleWhen`, and `allowSkip` has been navigation-freedom-not-validation- + exemption since #2959. This card lands the residue: + + - **metadata-admin view create seeds one starter step for a wizard** (app-shell + `anchors.ts`): the create body used to emit `sections: []` for every form type, + which for `type: 'wizard'` is exactly the shape the tightened spec refuses at + parse (D7 — a stepless wizard silently rendered as a plain simple form). Same + seed-the-required-shape move the flow anchor makes for its `type` enum + (objectui#2326). Other form types keep the bare `[]` — only the wizard variant + refuses emptiness. + - **`@object-ui/types` TSDoc states the ruled wizard boundary** where the shared + section/form types restate the form-view family: `ObjectFormSection.visibleWhen` + / `collapsible` / `collapsed` name the wizard drop + spec-door refusal; + `ObjectFormSchema.sections` states sections-ARE-steps and array-order-is-step- + order; `allowSkip` states the D4 semantics. Type SHAPES are unchanged — the + spec's own ruled mechanism is a parse-time refinement over the single shared + section schema (D2 option A), which these types mirror at the type level. + - **Consumer-side behaviour pins** (`wizardRuledSemantics-6985.test.tsx`): the + wizard-inert step keys are dropped, never honoured (a denying `visibleWhen` + does not remove a step; `collapsible`/`collapsed: true` produce no collapse + affordance, with a positive control on the affordance probe); the empty-steps + wizard's measured degradation to a simple form is pinned as the shape the spec + door now refuses (one-step wizards stay legal — no arity floor); array order + is step order (with a reversed-array control). + - **Installed-spec door pins** (`wizardSpecDoor-6985.test.ts`), gated on a + capability probe of the installed `FormViewSchema` rather than a version + string: the post-Card-S half (refusal messages, prescriptions, the authored- + `false` collapse boundary, the wizard-scoped control) activates by itself on + the lockfile bump that brings the tightening in; until then the pre-tightening + half records the 17.2.x accept-set it measured. `steps:` is pinned refused on + every spec line. + + No teaching material — the #13337/#13086 fence lifts only after both halves land; + docs changes here are TSDoc/comments only. +- f0f774b: Field designer: clearing a picklist option's **Label** no longer produces metadata the + API refuses (objectui#7014 Q2). + + `ObjectFieldInspector`'s option writer guarded the key on truthiness — + `if (o.label) out.label = o.label;` — so an empty Label box was serialised as an option + with **no `label` key at all**. Measured on `@objectstack/spec` 17.2.0, + `SelectOptionSchema` accepts `{ value: 'alpha', label: '' }` and rejects + `{ value: 'alpha' }` with `invalid_type` at `[label]`: the guard was taking a document + the platform accepts and rewriting it into one it refuses, and the save came back 422 + with nothing on screen to explain it. + + The writer now emits `label: o.label ?? ''` — the value the author actually holds, + empty string included. Nothing is invented: an emptied label stays empty rather than + falling back to the option's `value`, and the author-facing surface is now exactly as + wide as the contract instead of narrower. The `??` arm also covers an option that + arrived without a usable label (a missing or non-string stored `label`, which + `readOptions` maps to `undefined`) — there is no legal document that omits the key, and + `''` is what the Label input has been displaying for that option all along. + + Pinned in `ObjectFieldInspector.optionLabel.test.tsx`, which ends each case at + `SelectOptionSchema` / `FieldSchema` rather than merely asserting the key is present — + the point of the fix is that the contract accepts what the designer emits. +- 0d4c789: Studio Interfaces: no Design mode and no "click a block" rail on leaves that have + no block canvas (objectui#7121). + + `registerStudioCanvasPreview(type, …)` opts a type into a surface-specific canvas + that renders the running app rather than an editable draft — a contract, not a + habit: `StudioCanvasPreviewProps` carries no `selection`, `onSelectionChange`, + `onPatch` or `editing`. Two affordances beside such a leaf ignored that. + + - The Design/Run switch (objectui#5800) was still offered, though `editing` is + handed to exactly one canvas branch (`Preview`). On a studio-canvas leaf the + switch moved `canvasMode` and reached no renderer — a live-looking control + wired to nothing. It is now gated. + - The right rail fell through to "Click a block on the canvas, and edit its + properties right here." beside a canvas that has no blocks, so the instruction + could not be followed. It now states what the canvas is, and — because this + canvas has no blocks by contract — promises no recovery. + - The rail's new branch is ordered ahead of the selection branch, so a block + selected on a *different* leaf no longer opens a scoped inspector for a block + this canvas does not contain; the header's "clear selection" button is gated + with it. + + The discriminator is `StudioCanvas`, not `isEditable`. `isEditable` is + `!!Preview && !StudioCanvas` — a conjunction of two independent causes — so + gating on it would also strip these affordances from leaves whose only fault is + that their own type has no designer, the state objectui#6795 part C pinned as + still deserving the ordinary rail. Behaviour on every leaf with a block canvas + is unchanged. +- 639114c: Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122). + + `SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external + datasource validation panel labels those kinds through a map that is TOTAL over + the union on purpose — so an upstream addition fails the build rather than + rendering a blank cell. That mechanism fired: the package did not compile against + 17.3.0 until the kind was labelled. It now reads "Not checked — remote + unreachable", following the spec's own ruling that this kind asserts nothing + about the remote schema and must never be surfaced as "schema changed": it means + introspection could not complete, which is often transient, and labelling it like + a mismatch would tell an operator to repair a schema nobody has read. + + Two internal flow-inspector types were renamed (`FlowNodeLike` → + `InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own + `FlowNodeLike`; neither name is in this package's published entry, so no consumer + import changes. + + `@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with + the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The + `previewMode` prop itself is host-supplied, unchanged, and unaffected. +- 0101fab: Studio Interfaces pillar: a canvas block selection no longer outlives the leaf + it was made on (objectui#7137). + + `InterfacesPillar`'s only clear of `selection` sat inside the draft-load effect, + *after* its `if (!current || !isEditable) … return` guard, so it never ran on the + early-return path. Since `isEditable = !!Preview && !StudioCanvas` is a + conjunction, that is two populations of leaf: a studio-canvas leaf (`object`), + and a leaf whose own type has no registered designer. Walking to either from a + leaf with a block selected carried the selection across, still describing a block + on the previous leaf's canvas. + + Two symptoms, both measured before and after: + + - the scoped inspector opened for a foreign block — recorded three renders as + `page:home_page:block:blk_1`, with `blk_1` a dashboard block — and the header + offered to clear a selection belonging to another leaf; + - in the folded (chat-dock) layout, `hasInspectorTarget` stayed true across the + leaf change, so `nextCenterTab` saw no edge and stranded the author on the + Properties tab of a leaf with no properties to show. + + The selection is now stamped with its leaf and read back through that key — the + same "expires by construction" shape `blockingReport` already uses against + `inspectorKey` in this component. It goes null in the *same* render as the leaf + change rather than one committed render later, and there is no imperative clear + left for a future guard to strand. Within a leaf nothing changes: the Design/Run + round trip still keeps its selection, and a same-leaf reload (`publishNonce`) + still clears it. + + objectui#7121's gating of the rail and the Design/Run switch is untouched, and + its discriminator remains `StudioCanvas` — not `isEditable`. +- ebc05b4: Studio's package switcher reads the server's `writable` verdict instead of guessing + from `manifest.scope` (objectui#7177, ADR-0130 Consequences row 6, server half in + objectstack#14375). + + `GET /api/v1/packages` now stamps every row with `writable: boolean`, computed by + `isWritablePackage` (ADR-0070 D2) — the same predicate the server's authoring and + lifecycle gates enforce. `parsePackages` uses it when the row carries one, so the + lock badge and the gate cannot disagree. + + The old `scope !== 'project'` expression stays as the fallback for servers that + predate the field, and its output is pinned byte-identical. It is wrong for exactly + one row, which is why the verdict had to move server-side: a `type: module` + sub-package of a multi-package artifact (ADR-0130 D4) is served with no `scope` key + at all — the schema default is applied at parse time, while the artifact load path + hands the raw manifest body to `registerApp`. The heuristic reads that as a writable + database base, while the server refuses every write to it. Nothing in the raw row + separates it from a scope-less Studio-created base, which really is writable — only + the server's `engine.manifests` does, so a client-side "missing scope means + read-only" rule would have flipped every Studio base read-only instead. + + Kernel packages (`scope: system` / `cloud`) stay hidden whatever verdict they carry: + that filter is about visibility, not writability. +- f626808: fix(app-shell,plugin-list): a list view's own `description` now reaches the screen + + A `description` authored on a per-list-view entry (`listViews.<viewName>.description`) + was validated, built and served correctly, then silently never rendered. Two + independent cuts, both fixed here: + + - **app-shell** — `ObjectView`'s `renderListView` relay copied ~46 keys off the + active view onto the schema it hands `ListView` (`label`, `sort`, `filter`, + `hiddenFields`, `inlineEdit`, `color`, `allowExport`, …) but had no rung for + `description`, so the renderer could only ever see the object-level list's + description and a per-view one was unreachable. It is relayed now, with the + same two-rung shape as `label`. This is *not* the object's own + `objectDef.description`, which stays the page header's subtitle. + - **plugin-list** — `ListView` rendered `typeof description === 'string' ? … : ''`, + a type test rather than a resolution. `ListViewSchema.description` is + `I18nLabel`, so an inline locale map (`{ en, 'zh-CN' }`) — metadata the spec + entitles an author to write — rendered a blank strip in every locale. It now + resolves through the same shared helper the sibling `label` uses, and the + visibility guard reads the resolved text, so a map with no usable entry drops + the strip instead of reserving empty space for it. + + `appearance.showDescription: false` still suppresses the description in both arms. +- 34ea56d: Relay a per-view `rowColor` through the two object-view hosts (objectui#7218). + + `rowColor` is a declared member of `ListViewSchema` — imported by reference from + `@objectstack/spec`, shape `{ field, colors? }` — and `ListView` reads it to + seed its `rowColorConfig` state, which colours whole rows from the named field's + value. Neither object-view host relayed it: `app-shell`'s + `ObjectView.renderListView` builds its list schema by spreading the host's and + then relaying 47 named keys off the active view, and `plugin-view`'s + `ObjectView` assembles 46 inside its `object-view HOST-COMPOSITION SURFACE` + fence. `rowColor` had a rung in neither. + + So an authored per-view row colour was unreachable on the object route: + authored, validated, built and served correctly, then dropped at the relay. + Nothing errored and every authoring gate passed — the only symptom was that the + rows were not coloured, which an author cannot notice short of diffing the DOM. + Same "declared and inert" shape objectui#7199 fixed for `description`. + + **This is a relay, not a new surface.** The interface route + (`InterfaceListPage.tsx`) has shipped `rowColor: view.rowColor` next to + `grouping` and `pagination` since ADR-0047, into a schema typed + `ListViewSchema`, with no fence of any kind — so the key was already + author-reachable and already had a delivery path; two of three hosts simply did + not use it. The legacy shorthand for the same feature (bare `color`) already had + a rung in both literals; only the spec-canonical spelling was missing. + + **No published surface moves.** Both rungs are view-sourced only, and neither + adds a cast read off the object-view node — that would have added a 28th name to + the objectui#5097 HOST-COMPOSITION exemption whose count the 2026-08-18 ruling + fixed at 27, which is a ruling and not a refactor. `grouping` is the in-fence + precedent for a view-only rung. + + ⚠️ Not `userActions.rowColor`, a boolean permission toggle sharing this name at + a different nesting level ("may the user open the colour panel" versus "what the + colours are"). That key is untouched, and the new pins hold the two apart. +- 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). + + objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope + (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This + closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, + `DetailView`, and `ObjectDataTable` (which builds its own whitelist in + `computeLookupExpand` rather than calling `buildExpandFields`). + + **Three of them pass no column list at all**, which makes them the sharp ones: + `buildExpandFields` reads an absent column list as "no column restriction" and falls + back to **every declared relation on the object**, denied ones included. So a standalone + calendar, a gantt, and every record page in the console asked the server to resolve the + object's full relation set by default rather than by configuration. + + **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column + list is already FLS-filtered field by field, which is exactly the route PR #7229 measured + as unsound: an emptied column list reads as "no column restriction", so a detail view + whose authored fields are all denied had its `$expand` **widened** from the relations it + asked for to every relation the object declares. The principal who may read least was + asking for the most. + + **Reproduced before it was fixed**, as a failing test per site. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip, and the + client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, + which contains only the object's declared reference-bearing fields, so the "`checkField` + answers false for an undeclared key" trap cannot be reached and derived / host-joined + columns are untouched. An unanswered permission policy filters nothing. Neither + `buildExpandFields` nor `computeLookupExpand` is changed. +- 19f1639: A synthesized default list view now always leads with the object's name field + (objectui#7245). + + **The defect.** An object that declares no list view gets its default grid columns + synthesized from `highlightFields`, taken verbatim. But `highlightFields` is ADR-0085's + *"most important fields"* role, not a column list — and its first consumer, the + detail-page highlight strip, **deliberately removes the title field**, because the page + H1 directly above it already shows one. So metadata that is entirely correct routinely + omits the record's name from `highlightFields`. The showcase `showcase_account` declares + `nameField: "name"` and `highlightFields: ["status", "industry", "annual_revenue"]`, and + its default `所有记录` grid rendered 14 rows whose columns were `#` / Lifecycle / Industry + / Annual Revenue / actions — no name column, and no way to tell one account from another. + + A list has no H1 to lean on, so the same declaration needs the opposite treatment here. + This is not a new convention: `deriveLookupColumns` in `@object-ui/fields` already leads + its record-picker columns with the display field and filters it out of the declared list. + The list faces now agree with it. + + **What changed.** `@object-ui/core` gains two exports on the ADR-0079 title ladder: + + - `resolveNameField(objectDef)` — *which field* titles an object: the declared + `nameField` (then its deprecated `displayNameField` / `NAME_FIELD_KEY` aliases), else + the type-aware derivation. The name-space twin of `getRecordDisplayName`, which answers + what that field *says* on one record. Both now read one spelling of the declared + pointer, so they cannot drift into naming different fields. + - `leadWithNameField(objectDef, columns)` — moves that field to the front of a + **synthesized** column list. + + All three faces that synthesize default list columns call it: `ObjectView` + (`defaultListColumnsFromObject`), `InterfaceListPage` (`defaultColumnsFromObject`) and + `ObjectGrid`'s own derivation. The name field is **moved**, not merely appended, so an + author who lists it third still gets it first — "the column that identifies the row" + means first. On the two capped faces the lead is applied *before* the 5 / 6-column slice, + so an object declaring its name field late no longer loses it off the end. + + **Scope, deliberately narrow.** Author-declared column lists are untouched — a view or + grid that declares `columns` / `fields` said what it wants, and reordering it would be + renderer-side second-guessing of metadata. Three cases also decline to lead: a name field + the object carries no field def for (never fabricate a column), one marked + `hidden: true` (the author said don't show it), and a *derived* pick that lands on a + system-managed column — `deriveTitleField` filters by type only, and leading a default + list with a raw id is the regression objectui#2702 / #2777 fixed. A *declared* + `nameField` pointing at a system field still leads: `sys_migration` really does point at + `id`, and an explicit designation is not a heuristic misfire. +- d4c6a86: The console chrome's Home affordances follow the product's declared landing (objectui#7256). + + `/` has honored `app.isDefault` since the hardcoded `PREFERRED_APPS = ['cloud_control']` + redirect was retired; the chrome had not. The top-bar logo, the sidebar's Home row, the + mobile sheet's Home row and the app-switcher's Home entry each named `/home` literally — + the ENVIRONMENT layer's launcher (ADR-0075). So a deployment that declares a landing + offered the customer two homes in two voices, and one click on the logo left the declared + one. + + On cloud's control plane that second home is actively wrong: its "Build an app" / "Start + from a template" cards are environment-side actions that cannot work from the control + plane, and its "Your apps" tiles are the control plane's own internal management apps. + + - `@object-ui/app-shell` adds `resolveDeclaredHomePath()` — the one reader of the + declaration — plus `useHomePath()`, which the four chrome sites now consume. The signal + is the App metadata the server already sends: no hostname sniff, and no product name + baked into the shared bundle. + - `@object-ui/console`'s `/` resolver is unchanged, and a behavioural matrix now pins its + answer equal to the chrome's for every declared app list, so the post-login landing and + the logo cannot drift apart. + + Deployments that declare no landing are unaffected: every Home affordance still resolves + to `/home`. The "this app is gone" recovery redirects in `AppContent` / `ConsoleShell` are + deliberately untouched — they are error paths, not Home affordances. +- 3f93fe4: Studio top bar: a failed package-list fetch is reported and told apart from the + other two states it used to look exactly like (objectui#7368). + + `PackageSwitcher` held the list as `PkgEntry[] | null` and caught the + `fetchPackages()` rejection into an empty block, so `null` meant both "still + loading" and "the fetch failed". The trigger rendered `current?.name ?? packageId`, + which collapses a third situation into the same pixels: a package whose producer + declared no name at all (`parsePackages` falls `name` back to the id). Any failure + of `GET /api/v1/packages` therefore left the Studio top bar printing the raw + reverse-domain package id — `app.b2r4` — with no toast, no console line and no + retry, forever, and the author had no way to tell whether to go fix the manifest or + to go retry. + + The switcher now records the failure beside the list and renders the three states + apart: `loading` keeps a spinner next to the id, `failed` adds a "Failed to load" + marker carrying the error on its tooltip and replaces the popover's forever-"Loading…" + line with the reason, and `loaded` leaves the id standing alone — where a bare id now + really does mean "this package declares no name". The two sibling `fetchPackages()` + callers on the same surface (the writability courtesy gate and the object-namespace + lookup) stop swallowing their rejections too; all three report on one shared sonner + id, so a single outage produces one toast rather than three. + + The degradation itself is deliberately kept — the switcher still navigates and one + 503 does not take the top bar down — and no retry was added: retry policy (how many, + what backoff, what to show after giving up) is undecided, and retrying would delay + the moment the failure becomes visible, which is the opposite of what this fixes. +- 3399704: Fix `AiUsageIndicator` to recognize the free plan's new `resetKind: 'weekly'` and its + `resetsAt` (objectui#7371, consumer of cloud PR #1852's rolling 7-day AI quota window). + + Before this change a `weekly` meter fell through to the component's unrecognized-kind + path and rendered no reset line at all — not a crash, but silently wrong information + next to a live progress ring. The indicator now shows "Resets in N days" (or "Resets in + N hours" once inside the final day, e.g. `console.ai.usage.resetsWeeklyHours`), computed + from the endpoint's `resetsAt`, in both languages via `@object-ui/i18n` + (`console.ai.usage.resetsWeeklyDays` / `resetsWeeklyHours`, real i18next plural families + with a base key so every locale pack resolves correctly, all ten packs translated). D5 is + preserved — no token count is ever rendered, only the days/hours until reset. + + Contract-first: `resetsAt` is read verbatim from the endpoint, never re-derived or + guessed client-side. A `weekly` meter with `resetsAt: null` (nothing counted yet in the + window) and any `resetKind` this build does not recognize both render no reset line — + fail-soft, not a crash or stale copy. + + `AiUsageResetKind` (`packages/app-shell/src/hooks/useAiUsage.ts`) gains the `'weekly'` + member; `resetsAt` was already `string | null` and needed no shape change. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 97c3e19: fix(app-shell): the object-field options editor no longer drops `default` and `visibleWhen` on save + + Opening a picklist field in the metadata-admin designer, editing any option and + saving used to write the option back without its `default` or `visibleWhen` + key. It was not a validation failure — the payload stayed perfectly valid, just + smaller than what the author wrote — so the loss was silent, and it took a + picklist default with it: `default` is `enforce` on the object-field face, and + the engine seeds the insert path from the option holding it. + + The loss started in the reader, not the writer. `readOptions` projected each + authored option down to `value` / `label` / `color`, so both keys were already + gone before `patchOptions` ran. `readOptions` now carries the keys the editor + has no control for and `patchOptions` writes them back, which also protects any + option key the spec accepts later. No authoring UI changed. +- 0c8dbc4: Strip the framework's read-time decorations off a served draft before the metadata + editor's client Zod gate judges it (objectui#7603). + + **The symptom.** Opening an item that has a **pending draft** in the metadata admin + raised a false "this item is invalid" banner plus inline field errors — on a body the + server accepts, and on exactly the workflow where an author is mid-edit. Every wired + metadata type whose schema is `.strict()` was affected (14 of the 15); `sharing_rule` + was shielded only by accident, because `AUTHOR_SHAPE_ONLY_TYPES` switches its edit gate + off for an unrelated reason. + + **The cause.** `client.getDraft()` serves a **decorated** body: the strict draft branch + returns `item: decorateMetadataItem(type, …)`, which attaches `_diagnostics` whenever + the type has a registered Zod schema, and `_draft` on the preview-draft branch. + `ResourceEditPage` merged that body over the layered baseline and handed the result + straight to `validateMetadataDraft`. The layered half is clean — `getMetaItemLayered` + serves RAW layers — so the misfire required a pending draft to exist, which is why it + stayed invisible: an item with no draft passes today. + + `@objectstack/spec` names both keys a read-time decoration and states that a served body + "is therefore NOT a valid input to the schema that produced it until these are removed". + The server was right and the client was wrong. + + **The fix.** `extractDraftBody` — the one function that turns a served draft envelope + into a body, and the chokepoint all three merge sites go through (the load effect, the + post-save refresh, the post-publish refresh) — now passes the body through the spec's + own exported `stripReadDecorations`, the same helper `MetadataService.saveFields` + already uses on the write side. The strip runs after the presence verdict, so removing + our own decorations can never turn a served draft into "no draft". + + No schema was loosened and no key list is restated in this repo. The list is the spec's, + reached through its helper, because a local copy goes stale the next time the framework + adds a decoration — and a decoration this code does not know to remove is precisely the + defect. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) is deliberately + **not** on that list: those keys are allowlisted by the closed schemas so provenance + survives a re-parse, and this strip leaves them alone. A genuinely undeclared key on the + same draft is still refused, by name. +- c01cf9a: Studio object designer: a new field's API name now follows the Label on **every** + keystroke, not just the first one (objectui#7615). + + Typing a label one character at a time — what a person at a keyboard actually does — + used to leave the field permanently named after its first letter: `Health Score` + produced the API name `h`, and no later keystroke moved it. Pasting or autofilling the + same label produced `health_score`, so the two ways of entering the same label + disagreed. The one-letter name then leaked into the REST API, formulas and exports, + with nothing on screen saying the API name had stopped following the Label. + + Cause: the derivation asked "is this name still auto-generated?" by pattern-matching + the name itself (`field_<N>` / `<type>` / `<type>_<N>`), and its own first rename + destroyed that shape. The inspector now records who owns the API name instead of + re-reading it off a string the feature rewrites. + + Three boundaries, unchanged in intent from objectui#2260: + + - a new field whose API name the author has not touched re-derives on every label + change; + - a field that arrived already named (i.e. saved) is never renamed by a label edit; + - once the author types in the API name box the Label stops moving it — including + when what they typed looks like an auto-generated placeholder, which no + pattern-match on the string alone can tell apart from a real one. +- 655afab: A failed package-list refresh no longer reads as "the package was deleted" and no + longer evicts the author from the Studio (objectui#7821). + + `onManageChanged` — the callback the Studio's `PackageDetailSheet` fires after every + package lifecycle action (disable / duplicate / delete / publish / manifest edit) — + refreshed the list into a local `list` initialised to `[]` and swallowed the rejection + under a comment reading "keep the stale list". That is true of the `pkgs` state, which + is simply not written, and false of the local, which stayed `[]`. So after a failed + `GET /api/v1/packages` the `!list.some(...)` check three lines down was + unconditionally true, the code took the branch labelled `// Deleted`, and — when the + managed package was the one under the editor — navigated away with `list[0]` + undefined, i.e. to `/home`. One transient 503, network blip or auth expiry threw the + author out of the editor with no toast and no confirmation, while the package was + still there. + + The local now starts as `null` — "the refresh told us nothing" — and only a list that + actually came back, without the managed package in it, is read as a deletion. A + failure draws no inference at all: no navigation. It is reported instead, through the + posture this surface already has (objectui#7368): `formatMetadataError` on the shared + `studio-package-list` sonner id, and recorded so the switcher reads `failed` rather + than presenting a now-stale list as current. A real deletion navigates exactly as + before — to the first surviving package, or `/home` when none is left. + + Still a `.catch` and still no retry: one 503 must not take the Studio down, and no + retry policy has been ruled on. +- cff3d97: Forward the authored `chart:` block WHOLE from the object-view relay to `ListView` + (objectui#7823). + + `ObjectView`'s `renderListView` builds the list schema by spreading the object's list + schema and then relaying the active view's visualization blocks under `options`. Every + sibling block is forwarded whole; `chart` was a hand-listed projection of exactly six + keys — `chartType`, `xAxisField`, `yAxisFields`, `aggregation`, `series`, `config` — the + pre-ADR-0021 key set, frozen. The whole ADR-0021 (objectui#1890) authoring shape + (`dataset` / `dimensions` / `values`) and the legacy `categoryField` / `valueField` + spelling had no rung, so a view that declared them reached `ListView` with its binding + stripped, and `ListView` could not tell that from a view that had declared nothing. + + **What changes for authors.** On the object-view route, a list view that declares an + ADR-0021 chart block and whitelists `chart` in `appearance.allowedVisualizations` is now + offered the Chart toggle and renders from the dataset it named. Before this fix the + capability gate added by objectui#7544 was handed six `undefined` keys, correctly + answered "nothing declared" about a view whose author had declared everything, and + ADR-0047 filtered the author's own whitelist down to `['grid']` — no toggle, no + diagnostic. The legacy `xAxisField` / `yAxisFields` spelling did survive the projection + and did resolve, so the two authoring shapes behaved differently on this route for + reasons that lived entirely in that one object literal. + + **Not a wider whitelist.** The projection is replaced by a pointer, not extended from + six keys to nine: a hand-listed key set is a copy, and copies rot silently — three more + keys would buy ADR-0021's correctness while re-arming the identical trap for the next + block key, and nothing would fire then either, because the relay's view definition is + `Record<string, any>` and a missing rung is invisible to `tsc` (objectui#7559 owns that + mechanism). Forwarding whole is safe because `ListView` reads this block by name at both + of its readers — the capability gate's `resolveListChartBinding` and the `chart` render + branch — and never spreads it. + + A view that declares no chart block, an empty block, or a block with no binding at all + is still offered no Chart toggle: the relay now forwards `undefined` instead of the old + permanently-truthy husk of six `undefined` keys. +- f5d2acc: Studio: a failed package lookup no longer opens the management sheet on nothing + + `fetchFullPackage` — the helper behind the switcher's "Package info & settings" — + fetched `/api/v1/packages` and went straight to `res.json()`, never reading + `res.ok`. The platform answers a failed read in the ADR-0112 envelope + (`{ success: false, error: { code, message } }`), which parses cleanly through + that reader: the error object is neither an array nor carries `packages`, so the + list fell to `[]` and the lookup returned `null` without throwing. `openManage`'s + `catch` therefore never ran and the two lines after it still fired, opening the + management sheet over a null package — which renders nothing. During an outage + the author clicked the menu item and got silence: no sheet, no toast, no + explanation. + + The read now refuses a non-2xx, carrying the server's own `error.message` and + `error.code` (in the 5xx band the platform withholds the producer's prose, so the + code is the discriminating word) and naming the status when the body is not JSON + at all. The failure is reported through this surface's existing posture — + `formatMetadataError` on the shared `studio-package-list` sonner id, so one outage + across this surface's four callers of that endpoint is one toast, not four. + + And the sheet no longer opens on a `null` package at all: a successful list that + simply does not contain the package — deleted or uninstalled elsewhere — now says + so instead of opening over nothing. +- 3b30920: Studio: a failed managed-snapshot refresh no longer leaves the package sheet showing + the pre-action record as current (objectui#7907). + + `onManageChanged` runs after every package lifecycle action fired from the detail sheet + (disable / enable / duplicate / publish / publish-drafts / manifest edit). Its tail + re-read the managed record so the change would show immediately, and swallowed any + failure under a bare `catch {}` commented "keep the current snapshot" — a snapshot the + action itself had just made stale. The author disabled a package, was told nothing, and + went on reading `Status: Enabled`. + + The sheet derives its lifecycle verb from that record (`enabled` picks both the button's + label and the endpoint it POSTs), so a stale snapshot did not merely display a stale + badge — it re-armed the author with the verb they had just fired. The failure is now + reported through this surface's existing posture (`formatMetadataError` on the shared + `studio-package-list` sonner id, so one outage across the surface is still one toast) + and the sheet closes rather than stay open on a record known to be pre-action. The + Studio, the top bar and the package list are untouched: still a degradation, never a + throw, and no navigation is inferred from a refresh that could not happen. + + The same tail dropped `fresh === null` — a successful read whose list no longer contains + the package — just as quietly; it now reports that with the same sentence `openManage` + already uses for it. + + Pre-existing, and objectui#7881 made it much easier to hit rather than causing it: + before that fix `fetchFullPackage` never read `res.ok`, so this `catch` could only ever + see a non-JSON body; now that the helper refuses a non-2xx, the same `catch` was also + swallowing every 401 / 403 / 503 / 500. +- bfa4fe7: `PackageFormDialog` reads the producer-marked `error.userMessage` (objectui#7979). + + The create / edit / view package dialog POSTs and PATCHes `/api/v1/packages` through its + own `apiJson`, which held a fourth copy of the ADR-0112 failure-envelope ladder — + character for character the one `PackagesPage` had before objectui#7959. It read the + diagnostic `error.message` and stopped, so two things a refusal carries never reached the + author: the producer's marked `error.userMessage` (present since objectstack#9934, emitted + by both doors that serve these routes) and `error.code`. + + The read now comes from the one shared rule, `readEnvelopeFailureText` + (`utils/apiErrorEnvelope.ts`), which prefers the mark over the diagnostic and appends the + declared code behind whichever prose won. Create and edit are exactly where an author meets + a refusal that names what to fix — a namespace rule, a version already published — and in + the 5xx band the door substitutes the generic `Internal server error` into `message` while + the mark rides through untouched, so on a marked 500/503 the dialog used to show the generic + sentence and discard the specific one. + + The two rungs below the shared read stay: a bare-string `error` and a top-level `message` + are older runtimes' shapes, not this envelope, and they are live for this call site alone. + The dialog's two status-driven arms (409 → "already exists", 403 → the localized capability + copy, objectstack#8270) are unchanged. +- f57ca75: fix(studio): one draft-envelope reader, and it strips the framework's read decorations + + `client.getDraft()` serves a DECORATED body — the draft branch stamps + `_draft: true` and then `decorateMetadataItem` attaches `_diagnostics` for any + type with a registered Zod schema. The spec names both READ-TIME decorations + precisely because a served body "is NOT a valid input to the schema that + produced it until these are removed" (`METADATA_READ_DECORATIONS`). + + objectui#7603 taught `ResourceEditPage` to strip them. It could only teach one + site, because `extractDraftBody` existed **four times** — three verbatim copies + plus a hand-rolled one in `ObjectHooksPanel` — and six more consumers unwrapped + the envelope inline. Ten readers, one of which knew the rule. + + **The user-visible half.** The pending-changes sheet's per-entry diff compares + the published body against the draft body key by key. Those two reads are + decorated ASYMMETRICALLY — only the draft branch stamps `_draft` — so the sheet + listed `_draft` under "Also changed:" on every entry that has a published + counterpart, and `_diagnostics` alongside it whenever the two read-time verdicts + differed. Framework-internal keys were being presented to the author as their + own edits, on the screen where they decide whether to publish. + + **The rest.** Six sites merged a decorated body into a document they then wrote + back through `save(..., { mode: 'draft' })` — the Studio app / page / object / + flow surfaces, the package OWD panel, the object hooks panel, and the + adapter's `updateView`. Today's server absorbs that (it strips read decorations + on ingress, before its own schema gate), so nothing 400s; this is still a client + emitting a body its own spec calls invalid, and the fix belongs at the producer. + + The cure is one function rather than ten strips: `extractDraftBody` is now + exported from `@object-ui/data-objectstack`, beside the `getDraft` whose + envelope it decodes. The key list is the spec's exported + `stripReadDecorations` — never a second hand-maintained copy in this repo. The + presence verdict still runs BEFORE the strip, so removing our own annotations + can never turn a served draft into "nothing pending", and the ADR-0010 + protection envelope (`_lock`, `_provenance`, `_packageId`, `_packageVersion`) + is deliberately untouched: those keys are declared by the closed schemas. + + No schema was loosened, and no gate was taught to tolerate `_diagnostics`. +- 7258eaf: De-developerize the metadata-admin property form now that Studio's 「界面」 panel + grafts it in front of an AI-build maker (objectui#8218). + + `SchemaForm` + `widgets.tsx` were written for an administrator editing metadata. + Studio's interface panel renders the very same form for someone who has never + seen a JSON Schema, so four developer habits landed inside an otherwise fully + Chinese surface. Same cause, one pass: + + - **Machine-name tooltip.** The `title="Machine name"` on the identifier chip + went through the engine string table. Note the chip's own predicate degrades + under localization: it shows when the visible label does not spell the machine + name, which a translated label never does — so in a Chinese panel it renders + for *every* field. Reported, not changed here; whether a maker should see the + identifier at all is a product call, not a rendering one. + + - **Master-detail column headers.** A column whose item schema carries no + `title` printed the raw JSON Schema key (`actionUrl`, `actionType`) as if it + were a column name — in *every* locale, English included. It now humanises the + key, the same convention this tree already applies wherever a title is absent + (`json-schema-to-fields.ts`'s `prop.title || humanizeKey`, and `SchemaForm`'s + own grid repeater: `s.label || prettify(s.field)`). `title` is an OPTIONAL + annotation, so its absence is not off-spec metadata and the renderer has to + render something; this is not a lenient contract fallback. The LOCALIZED + column name needs an upstream channel that does not exist yet — filed there. + + - **Numeric fields.** Both numeric renderers (`SchemaForm`'s field control and + the master-detail cell) now gray the schema's `default` in as a placeholder, + so an empty box reads "using the default" instead of "unknown", and forward + `minimum` / `maximum` / `multipleOf` onto the control. Previously only + `fieldSpec.min` / `.max` were read — which a spec-derived authoring form never + declares — so a panel accepted a negative column count its own contract had + already ruled out. The default stays a placeholder, never a written value, so + "left on the default" remains distinguishable from "pinned to today's default" + in the saved metadata. + + - **Untranslated strings.** A full sweep of both files: 45 user-visible literals + (aria-labels, placeholders, empty-state prose, segmented-control labels, + secret-field copy, the code editor's chrome) now resolve through the engine + table in `en` and `zh`. +- 450f110: fix(app-shell): the Action designer's preview draws what the runtime dialog will draw + + `ActionDefaultInspector` offers eight param `type` spellings; `ActionPreview`'s + dialog mock switched on five of them over a private table, so three of the eight + previewed as a control `ActionParamDialog` does not render — `datetime` and a + targeted `lookup` as plain text boxes, and a `select` whose options were not + authored yet as a text box as well. A `text` param that happened to carry + `options` previewed as a select the runtime never draws, for the same reason in + the other direction. + + The mock now resolves each param through `paramToField`'s + `resolveParamWidgetType` / `paramDegradesWithoutTarget` — the same adapter the + dialog itself renders through — so the two panes cannot disagree about a + spelling again. `datetime` draws a date/time control, a `lookup` with a declared + `reference` draws a record picker, a targetless one draws the record-id text box + the dialog degrades to and says why, and a `select` always draws a picker. + + The per-param editor also gains an `options` control for `select` params. It had + none, and `params` is hidden from the collapsed "More fields" form, so the panel + that offered the type had nowhere to author the choices the type needs. +- 1edbc2f: **Behaviour change:** a `master_detail` action param with no reference target now + receives the same "paste a record id" placeholder and help text a targetless + `lookup` param has had since objectui#3405. Previously it degraded to a plain + text input exactly like `lookup` did, but silently — the user was shown an + unexplained empty box that wanted a bare record id. + + The cause was two hand-maintained answers to one question. `paramToField()` + performs the degradation over RESOLVED widget keys (`lookup`, `master_detail`), + while `ActionParamDialog` decided who gets the hints with its own literal over + RAW param spellings (`'lookup' || 'reference'`). Neither set contained the + other: `master_detail` degraded with no hints, and `reference` was a copy of an + alias-table row the adapter folds to `lookup` before it tests membership. + + `paramToField` now exports `paramDegradesWithoutTarget(param)` and both the + adapter's own fallback branch and the dialog's two hint readers ask it, so "who + degrades" and "who gets hints after degrading" are one member set answered once + (objectui#5654; same convergence shape as objectui#4770 / #4790 / #4815). + + Params that already had the hints keep them, including the `reference` spelling; + a param that renders as text without having degraded (an unknown type) still + gets no picker hints. +- 649f4a5: **Behaviour change:** the console action runtime (`useConsoleActionRuntime`) now + builds its authenticated fetch with `sameOriginOnly: true`, matching the + `provider: 'api'` data-source lane (`ConsoleShell`). A metadata `type: 'api'` + action whose resolved target is a different origin than the page is fetched + through the bare global fetch: the platform Bearer token, `X-Tenant-ID`, and + `Accept-Language` are no longer attached (objectui#5702, maintainer ruling + 2026-08-22). Previously the Bearer rode to any off-origin target whose URL + contained `/api/`, and `X-Tenant-ID` rode to every off-origin target + unconditionally. + + Same-origin actions — including every relative target in a same-origin + deployment — are unchanged, and off-origin requests still execute (pass-through, + not a refusal). An off-origin integration that legitimately needs the platform + bearer declares itself explicitly or proxies same-origin. + + Note for split-host setups: `apiHandler` prefixes relative action targets with + `VITE_SERVER_URL`. When that is set to an origin different from the page's, + those action requests are off-origin and no longer carry credentials. The + committed dev and starter configurations are unaffected: since objectui#5745 + they ship `VITE_SERVER_URL` empty and reach a split-host backend through the + Vite `/api` dev proxy, so their action requests are same-origin. +- 169f6d2: `AiChatPage`'s public share-link base now resolves through the one console-mount + resolver instead of a private copy of it (objectui#4482). + + The page built `publicShareBase` itself — read the injected `<base href>`, take its + pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third + independent implementation of the mount resolution `resolveConsoleUrl` centralizes. + objectui#4472 had just deleted the other two on that rule; this was the surviving + sibling. Its output was correct, so nothing a user hits was broken and nothing a user + hits changes: measured over the base-href matrix, the deleted builder and + `resolveConsoleUrl('s')` return identical URLs for every shape the console is served in + — `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable + builds, nested mounts, and no `<base>` at all. + + The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`, + which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM + it returns `undefined` rather than a URL built from an origin that does not exist, so + `ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument — + the mount is only ever carried by the injected `<base href>`, and a resolver with no + other input cannot be pinned by a test that steers something production never reads. + + `resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected + `<base>` element for each deployment shape, plus a structural case asserting no other + app-shell file reads the `<base>` tag — so a fourth copy fails a test rather than + waiting for mount semantics to change under it. +- aa3b810: `features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead + of being spelled inline at two call sites (objectui#5577). + + `features.marketplace` already had a documented accessor whose docblock is where the + fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating + `/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*, + plus the "never infer this from the shape of a failure" warning. `features.aiStudio` + had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false` + and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine + had two spellings and neither reader could cite it. + + The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and + against a runtime-config snapshot whose `features` is absent that read is a TypeError + rather than a fail-open — the exact shape that crashed 29 tests across four suites in + PR #5575 before it was corrected. Measured here: no live path can currently deliver + such a snapshot to `ChatDock` (the module's singleton constructs `features` on every + write and exports no setter, and no suite mounts the dock's default body under a + partial stand-in), so this closes a reachable-by-construction crash rather than a live + one — and it closes it at the source by leaving no inline read to get wrong. + + `isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`: + neither is re-exported from `src/index.ts`, so the package's published `exports` surface + is unchanged. +- 0235ce7: The predicate identity bound for a signed-out visitor now carries `id: null`, so a + `ctx.user.id` visibility gate BITES instead of failing open (objectui#6534). + + `buildExpressionUser` has two branches, and only the signed-in one carried `id`. + So `'id' in buildExpressionUser(null)` was `false`, and an absent key is not + `false`: a CEL predicate naming `ctx.user.id` / `current_user.id` / `os.user.id` + hit an unbound key for a signed-out visitor and FAULTED. A faulting visibility + predicate fails OPEN (`evaluateVisibility`), so the gated field or action rendered + for exactly the principal it was written to exclude, with nothing on screen to say + the gate had not bitten — silently, for every signed-out visitor. + + Because the defect was in the shared normaliser rather than at a mount site, it + reached EVERY mount site, including `AppContent` and the console's + `InternalFormRoute`, both of which have always called the normaliser correctly. + This is the same fault-open mechanism objectui#6515 fixed one level up, where + `RecordFormPage` hand-rolled a descriptor missing `id` and `isPlatformAdmin`. + + `null` rather than `undefined`, and rather than leaving the key absent, is settled + by precedent on this exact object rather than chosen here. objectui#5424 removed + `roles` from it because a present-and-always-`undefined` key "is the shape that + teaches the wrong thing" — the context answers rather than being plainly absent, + and the answer is silently wrong; `undefined` here would reproduce that defect one + key over. Leaving it absent IS the defect. `null` is a VALUE a CEL author can + compare against, so `ctx.user.id == '…'` resolves to a clean FALSE. Measured, not + assumed: at a real mount site with `authState.user = null` and a field gated on + `ctx.user.id == 'u_admin'`, the field is now filtered OUT of the schema handed to + `ObjectForm`, where before it was present. + + This also closes the last asymmetry between the two branches. Both now advertise + the same six keys, which is the symmetry objectui#5424 was closing when it removed + `roles` — and the shape pin now asserts the key sets are equal, so a future edit + that adds a key to one branch and forgets the other fails whichever branch it + forgets. + + NOT CHANGED, deliberately: fail-open on a predicate that DOES fault. That is + shipped permission-boundary policy (objectui#6443 / #6487 / #6445) and remains + exactly as it was — an unevaluable `visible` still renders. This change removes a + REASON to fault; it does not touch what happens once a predicate has. No accept set + is widened, no gate is relaxed and no fallback is added: the only behavioural + movement is that an id-gated surface which used to render for anonymous visitors + now hides from them. +- 9022bd5: Pin `module` / `moduleResolution` to `nodenext` in `@object-ui/app-shell`'s build config, matching the pins `@object-ui/react`, `@object-ui/fields` and five other packages already carry. This package builds with a bare `tsc`, which never rewrites import specifiers, so what the source writes is exactly what `dist` ships; under `nodenext` a missing relative extension is a compile error, so the extensionless-specifier defect that made published entries unloadable under plain Node cannot come back silently here. + + The lazy `@monaco-editor/react` imports in the metadata designer's source editors now read the package's named `Editor` export instead of its default. `@monaco-editor/react@4.7.0` is CommonJS and ships no `exports` map, so under `nodenext` the default resolves to the module namespace rather than to the component. The two names are one declaration in that package's own typings and the same object at runtime in both its CommonJS and ESM builds, so the editor that renders is unchanged. +- 029e2fb: The console's `<PageHeader>` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761). + + This repository has two components named `PageHeader`. `@object-ui/layout`'s is + the renderer for the authored `page:header` / `page-header` node and converged + on `subtitle` in objectui#3789, because `subtitle` is the key + `@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s — + the console's own title row, drawn by `ObjectView` and `ObjectDataPage` — + spelled the very same concept `description` and had no `subtitle` at all. Both + rendered correctly; the defect was one concept carrying two key names one + package apart, the objectstack#4115 shape moved up a layer. An author reading + one component to learn the other was being taught a key the contract does not + have. + + **Not a breaking change, measured rather than assumed.** The convergence is a + plain rename with no alias, because this component is not on the published + surface: + + | gauge | result | + |---|---| + | exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) | + | `exports` map | declares exactly `.` and `./styles.css` | + | Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves | + | in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) | + | emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) | + + No supported specifier reaches the prop, so there was nothing to keep + compatible, and a renderer-side `description` alias would have been exactly the + second dialect AGENTS.md #0.1 forbids — the layout side had just finished + retiring one. Out-of-repo consumers cannot be enumerated from this repository; + what can be, and is, is the set of import paths through which one could have + reached this component, which is empty. + + Rendered output is unchanged: same element, same classes, same position. The + patch tier is a declaration that the tarball moved, not a claim that a consumer + must act. + + `packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the + pin the card asked for. It asserts the subtitle on the DOM a reader gets (a + `<p>`, in the title block, after the `<h1>`), that `description` now draws + nothing and is rejected by the compiler, and — the assertion that actually goes + red if either side drifts again — that both packages' `PageHeaderComponentProps` + declare `subtitle`. +- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop + clipping their tail steps. + + Both occurrences were a single non-wrapping flex row whose steps were each + `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable + items, so the bar's intrinsic width grew without bound with step count and + label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK + step names measured **1070px inside a 527px container** (objectui#5554). + + The two hosts failed differently, and neither failure was recoverable by the + reader: + + - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not + scrollable, so the nearest scroller was the drawer *panel*. Reaching steps + 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the + record card, the activity timeline and the action buttons off-screen and left + a near-blank panel. + - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one + carried `overflow-x-auto`, so it scrolled itself rather than its container. + Better, but the tail steps still sat behind a scroll gesture with no visible + affordance. + + In both, readers took the clipped bar for the end of the data; the reporting + customer acceptance tester said so verbatim. Widening the window does not help: + the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. + + Both now render as a column: one row per step, a badge-and-rail gutter, and a + label that may wrap. Width is capped by the container at every step count and + every label length, which also suits both hosts' tall-and-narrow aspect. The + rail segment below each step keeps the tint rule the horizontal connector used + — it is coloured by the step it leads *into*. + + **Always vertical, with no step-count or measured-width threshold**, because + the overflow is driven by intrinsic content width (labels x count), not by + count alone: three 16-character CJK labels already crowd a 527px drawer, so any + count threshold picks a cutoff that is wrong for some real flow, and a measured + one reintroduces a viewport-dependent branch. The card's requirement is a fix + that cannot break at an untested viewport or flow length, and a layout with no + breakpoint and no measurement is the form that satisfies it. Horizontal-with- + scroll was ruled out for both occurrences: it leaves steps behind a gesture. + + Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and + `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is + green against the broken code too — every step was always in the DOM, and the + clipping was layout — so the suites assert the property the defect names + instead: no row is `shrink-0`, every label is `min-w-0` and none is + `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no + axis, overflow or width-pinning class carries a breakpoint prefix (so there is + no viewport with untested behaviour). The reported failing regime is exercised + directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins + that the layout classes are byte-identical across all four, so no count + threshold can put some other flow length back on the old path. + + The two steppers are kept identical by hand rather than extracted to a shared + component: they live in different packages, and deduplicating them is a + refactor with its own surface. Filed separately. +- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) + + Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no + deprecation window, and the protocol-17 session face emits no `roles` key at + all. Three client sites still read it: + + - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's + Approvals tab and Home's To-do card. It read nothing else, so it sent **no + `role:` identity at all**: an approval addressed to a position rather than to + a person matched nothing and vanished from all three surfaces, silently. + - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the + Approve/Reject enablement. It also splits the scalar `user.role`, so it + degraded rather than dying: it still yielded `role:user` while dropping every + business position name (`manager`, `finance_approver`, …). + - **`AppContent`'s expression user** — forwarded a `roles` key that was always + `undefined` into every CEL predicate context. Removed; `positions` and + `isPlatformAdmin` were already forwarded correctly beside it. + + The retired spelling is **not** kept as a fallback — pairing the two is what + ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the + declaration. + + `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately + untouched: it is a semantics decision, not a rename, and is deferred to a + maintainer ruling. +- 77c4ed3: `AppSidebar` is now `@deprecated` — use `UnifiedSidebar` instead. + + A census (objectui#5720) found `AppSidebar` has no in-repo mount point + (`ConsoleLayout` renders `UnifiedSidebar`, not this component) and no + downstream consumer visible anywhere across this org's GitHub-visible + repositories. It stays exported — from the package barrel and the published + `dist/index.d.ts` — because `@object-ui/app-shell` is a public npm package + (`publishConfig.access: "public"`) and an external consumer outside this org + is structurally invisible to that census; that is why it is deprecated + rather than deleted outright. No behavior change in this release — the + component still renders exactly as before. Its admin nav cluster is a + near-duplicate of `UnifiedSidebar`'s and has already drifted from it (it + gates only `sys-marketplace` on the workspace-admin flag, where + `UnifiedSidebar` gates the whole cluster); that divergence is not being + reconciled, since the component is scheduled for removal rather than kept + in parity — see objectui#5817 for the removal plan. + + Migration: replace any `AppSidebar` usage with `UnifiedSidebar` from the same + package. +- cc3366b: The built-moment transition (#5799) now fires on auto-publish environments too: `detectBuiltAppPackage` reads the raw build envelope (`status:'drafted'` OR `'published'`, packageId + an `app` item), because an auto-publish posture rewrites apply_blueprint's envelope to `published` and the drafted-only `draftReview` lift never fired there — measured live on staging, where reopening a built conversation stayed on the full page. +- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock + noticed it (objectui#5054). + + One abandoned device approval could be noticed by either of two clocks, and the + Cloud Connection panel had a different answer for each. When the panel's own + `expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — + translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered + HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` + threw a bare `Error` carrying only that sentence, and the catch rendered it + verbatim. Same user, same failure, two languages, decided by which clock got + there first — visible on a zh console as the same abandoned approval reading + Chinese or English depending on whether the tab sat open past `expires_in`. + + `getJson` now carries the envelope's `declaredCode` and `code` across its throw, + and a single closed map turns the two RFC 8628 outcomes a user can actually cause + into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, + `access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten + locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream + spelling there — `code` is `DEVICE_CODE_FAILED` for both. + + Every other code is unchanged: `invalid_grant`, and anything upstream invents + next, still render the wire `message`, which stays the single source of truth for + failures this console has no copy for. No API, export or resolver was widened. +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. +- fdefd4b: Console action runtime: closing an action confirm dialog now keeps the dialog's + text instead of blanking it mid-fade. + + `useConsoleActionRuntime` reset its confirm state by replacing the whole object + (`{ open: false, message: '' }`), which cleared `message` and dropped `options`. + Radix keeps `AlertDialogContent` mounted through its exit animation, so the + dialog's description went blank and its title and button labels reverted to + their defaults while it was still fading out. It now flips only `open` and keeps + every field, matching `RecordDetailView`'s second confirm runtime, which already + closed this way. Both runtimes feed one `ActionConfirmDialog`; the parity pin now + covers the close path as well as the open path. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- 71ee495: The two form CONTAINER contracts now have ONE declaration each, derived from + `@objectstack/spec`, and the console reads them instead of its own copies. + + objectui#5542 converged the LEAF of this contract — the field spec — and left the + two containers above it untouched, because converging them was a bigger call than a + mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared + twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once + in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean + subset — these two had **already drifted, in both directions**, so neither copy was a + subset of the other and there were two live answers to "what may an author write": + + - `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; + the console declared none of them. The console's `columns` admitted the string arm + (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. + - `FormViewSpec` — the console declared `label` / `groups` / `sharing` / + `submitBehavior`; app-shell stopped at `type` plus `sections`. + + The drift is decided by asking the **contract**, not by picking a side. `columns` + does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` + with the four numeric literals, folded to a number by its own transform), so + app-shell's numbers-only declaration was rejecting metadata the platform accepts — + objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is + the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured + against the installed `@objectstack/spec` 17.0.0), because a form config is titled, + not labelled. The value that read actually finds is the VIEW's identity label, which + arrives on the `ExpandedViewItem` envelope or beside the config on a flattened + runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to + the body it unwraps, rather than smuggled onto the form contract. + + Both types are therefore **derived from the spec's own `FormSection` / `FormView` + with named narrowings** — the repo's sanctioned form for a spec-shaped local type + (`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the + two layers agree on comes from the spec and cannot fall behind it; the four positions + where this layer is deliberately narrower are each named in an `Omit` list and + restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving + it would silently re-open #5542), and `label` / `description` / `visibleWhen` / + `visibleOn` keep the shapes this repo's renderers and evaluators actually consume + rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s + `submitBehavior` union — previously hand-written under the comment "Mirrors the spec + FormView.submitBehavior union" — is now read back off the shared type, making the + mirror structural. `@object-ui/app-shell` re-exports both names from its package root + (type-only, erased at build — nothing is added to the bundle), because a type that + cannot be imported is a type that gets retyped. + + The pins are what make future drift loud, and each half is pinned on both sides. + `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the + non-narrowed half of each type against the spec's own symbol, so re-hand-writing + either declaration fails `type-check` the day the spec moves rather than years later + when someone reads two files side by side — and the console's pins read both types + back out of the **exported** `buildSections` signature rather than naming them, so a + re-inlined local copy fails even if it agrees on every key on the day it is written. + Their liveness controls are what stop them being phantom checks: the removed copies + are pinned NOT equal to the shared types (proving the `Equal` helper still + discriminates), the renderer's honoured `RenderableSection` is pinned not equal + either (so the authored-document and honoured-row types cannot be collapsed again), + and an undeclared key is still rejected (so the derivation smuggled in no index + signature or `any`). Every narrowing carries a matching negative pin, so "derived" + cannot quietly become "widened to whatever the spec says". + + Behaviour is unchanged — the runtime always accepted these keys. The vitest halves + prove it: a section spelling its column count as the string `'3'` lays out identically + to the numeric `3` on both sides, and a section carrying the keys only one side used + to declare builds the same rows. +- cebdfe7: The form-field authoring contract now has ONE declaration, and the console reads it + instead of its own copy. + + objectui#5040 was not a missing key. It was that **two hand-written descriptions of + one contract drifted**, and nothing could notice, because each was only ever checked + against itself. PR #5537 converged the two app-shell descriptions into + `views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: + `FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same + name, in a different package — so the same failure mode stayed fully available. + + Measured key by key before choosing a route, because the two honest outcomes are + "same contract, import it" and "genuinely narrower layer, rename it and pin the + subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, + every one identical in type, none console-only — and it sat in a position that + describes an **authored document**: `FormSectionSpec.fields`, read straight off the + `/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both + files even spell the same six-member `type` union and call the element type + `FormFieldSpec`). The narrow, renderer-honoured shape is a different type that + already exists in that file, `RenderableField`. So this was one contract described + twice, and the console's description was wrong about the document: legal metadata — + `visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, + and ten more keys — was undeclared there. That is #5040's own symptom, "the type + rejects the configuration the runtime accepts", which no runtime test can see. + + `@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root + (type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` + imports it and deletes the local declaration. Reachability is the load-bearing half: + a type that cannot be imported is a type that gets retyped, and retyped copies drift. + `form-spec.ts` itself is untouched. + + `FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the + field-spec type back out of the **exported** `buildSections` signature rather than + naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even + if the copy agrees on every key on the day it is written — which is exactly what did + not happen to the copy this change removes. Its liveness controls are what stop it + being a phantom check: the removed nine-key shape is pinned NOT equal to the shared + type (so the `Equal` helper is proven to still discriminate), `RenderableField` is + pinned not equal to it either (so the honoured-row and authored-document types cannot + be collapsed again), and an undeclared key is still rejected (so the import did not + smuggle in an index signature). Behaviour is unchanged: the runtime always accepted + these keys, and the vitest half proves the same rows are built. +- 4db5989: A widget title stored as an inline per-locale map is editable again in both dashboard + authoring surfaces, and a save writes back only the active locale's entry + (objectui#5428). + + `@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at + 17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both + authoring panels edit a title in ONE single-line input. Writing the input's value back + as the whole value would collapse every other locale on the first keystroke, so both + surfaces took the same conservative branch: show a map-valued title resolved, and make + it READ-ONLY. + + That branch could not lose data, but it rested on a premise the spec had already + invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was + plain `string` through rc.5" — stated sixty lines below a comment in the same file + documenting the rc.6 widening that makes a stored map reachable. Both could not hold. + The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward + was not protect an unreachable path: it denied an author the ability to edit a widget + title in their own locale. + + objectui#5301's maintainer ruling settled the write rule for the sibling surface — a + save replaces only the active locale's entry and preserves the others — and + `@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because + the read and the write have to agree. Both panels now adopt it: + + - `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; + - `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. + + A plain-string title keeps saving as a plain string, so the common path is unchanged. + An edit made in a locale the stored map does not carry ADDS an entry under that locale + rather than overwriting the entry the display fell back to. + + The pins are preservation pins, not "the input is editable" pins: at both surfaces a + keystroke on a map-valued title must leave every other locale's entry byte-identical. + Reverse-verified by mutating each write back to the flattening form and confirming those + assertions go red at both surfaces. + + Not a multi-locale editor: an author still reaches only the entry for the locale they + are in. Authoring every locale from one panel remains an open product question. The + stale deferrals both comments carried pointed at objectui#4163, which closed as + completed on 2026-08-15 while the placeholders were still in the tree; they are replaced + with the rule that is actually in force rather than re-pointed at another tracker. +- f7e34ca: Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectui's own component extension. + + `DashboardWidgetSchema.type` was `string` on the TypeScript interface and `z.string()` in the Zod twin — an unbounded hatch. A typo'd family, a chart type the spec retired, and a component type nothing registers all type-checked and validated, surfacing only as the renderer's red `OBJUI-001` panel at runtime. + + It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum). + + Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one. +- dc14bcb: Delete the dead `src/utils/metadataConverters.ts` module. It had zero importers, was not + re-exported from the package barrel, and no `exports` subpath reached it — `toObjectDefinition`, + `toFieldDefinition`, `MetadataObject` and `MetadataField` were never part of the published + surface, so nothing external can break. The module was removed rather than left alone because it + carried two patterns the live code no longer uses: a name-heuristic `isSystem` (the server's real + `system` flag is the source of truth) and a three-way `referenceTo` tolerance for a target the + spec spells `reference`. A dead copy that disagrees with the live one is what a future author + copies from. +- ca2b409: Studio's form designer canvas now emits the namespaced `FormField.type` spelling + instead of passing `objectDef.fields[x].type` through raw (objectui#4838). + + Per the maintainer ruling on that card, a bare spec-type name (`markdown`, + `html`, `richtext`, …) is **not** a legal `FormField.type` — one widget, one + legal spelling, the namespaced widget id. `ObjectFormDesigner` was the measured + producer of the bare spelling: it handed a raw object-metadata type straight to + `isWideFieldType`, a helper whose vocabulary is `FormField.type`. It now + normalizes through `mapFieldTypeToFormType`, the one place that widget decision + is made. + + User-visible effect: a `repeater` field is finally laid out full-row on the + canvas, matching the runtime form. `repeater` is a spec `FieldType` that + resolves to the wide `field:grid` widget, but bare `repeater` is not one of + `WIDE_FIELD_TYPES`' bare members, so the raw pass matched nothing — the canvas + showed it at normal width while the real form spanned it. Fields whose spec name + doubles as a widget id (`textarea`, `markdown`, `html`, `richtext`, `grid`) are + unaffected; they matched under both spellings. + + The two tolerant consumers this makes look redundant are deliberately left + alone, each scheduled under its own follow-up with deprecation care: the + `field:`-prefix fallback in `renderFieldComponent`, and `WIDE_FIELD_TYPES`' dual + spellings. Removing a tolerance is the consumer-tightening half, and other + producers have not been normalized yet. +- eba3a6e: metadata-admin: `FormFieldSpec` declares `dependsOn`, and the widget half reads + the same declaration instead of its own copy of it (objectui#5040) + + `FormFieldSpec` — the authoring type for a metadata-admin form layout, the + element type of `FormSectionSpec.fields[]` — did not declare `dependsOn`. + `widgets.tsx` held a second, inline description of the same object as + `WidgetProps.fieldSpec`, and that one did, because two registered widgets read + it as their primary configuration: `field-selector` resolves + `dependsOn || reference || 'objectName'` to decide whose field catalog to + offer, and `dynamic-config` uses it to pick a sub-schema out of + `WidgetContext.dynamicSchemas`. One value travelling down one channel, + described twice, disagreeing on the one key that decides what those widgets + show — so + + ```ts + { field: 'fields', widget: 'field-selector', dependsOn: 'objectName' } + ``` + + the only configuration that makes `field-selector` work, was a `TS2353` for + anyone who typed their spec. It survived because in-repo specs reach the form + through `as any` / loose types, so the authoring type was never asked. + + No runtime behaviour changes: `MetadataField` already handed `dependsOn` + through and both widgets already read it. What changes is the type face — it + now admits what the runtime has always accepted. The two descriptions are one + declaration, extracted to a leaf module + (`views/metadata-admin/form-spec.ts`) that both halves import, because + `SchemaForm.tsx` imports `./widgets.js` and a back-edge would close a cycle. + `SchemaForm.tsx` re-exports `FormFieldSpec` and `VisibilityPredicate`, so every + existing importer is unaffected. + + `dependsOn` is `string | string[]` here, deliberately **not** + `@object-ui/types`' wider canonical `DependsOnInput`, which also admits + `{ field, param }` objects: both readers index `[0]` and use the result as a + field name, so the wider shape would be a type that lies. Converging the two is + its own decision, pinned as a refusal rather than taken silently. +- 4334341: `MarketplacePackagePage`'s third fetch effect — `listLocalInstalls` — is now gated on + `marketplaceEnabled` and `isAdmin`, the same two predicates the page's other two fetch + effects already check, in addition to (not instead of) its existing + `features.installLocal` check (objectui#5620). + + `listLocalInstalls`'s only consumer is `localInstalls.find(...)` in the content branch, + which is unreachable whenever the page has already returned `MarketplaceDisabled` (no + marketplace on this runtime) or `MarketplaceAccessDenied` (a refused viewer) — both + decided ahead of the content branch since objectui#5533 and objectui#5583. Before this + fix, a runtime with `features.installLocal: true` still fired the request — and + discarded its answer — on a marketplace-off runtime and for a non-admin, the same + wasted-round-trip class objectui#5533 established the fix for on this page, on the flag + that card was not about. + + `features.installLocal` remains its own axis: a runtime can mount a local kernel install + path with no marketplace proxy at all, so the fix adds the two predicates as a + conjunction rather than replacing the existing check. The request still fires exactly + when it did before AND the viewer would actually see its answer. +- 8140418: Studio 的记录网格只请求服务端真有的列——「添加字段」不再把整个网格打成「该视图的查询被拒绝」 + + 在 Data 支柱点一下「添加字段」,整片网格立刻变成错误态,并建议「清除筛选条件」——而现场根本没有筛选条件。 + + 根因是投影的来源错了。`gridColumns` 取的是**草稿**对象的字段名,而这个数组是下游取数的输入;`addField` 只往本地草稿推一个 `field_<N>`。于是列一变就重新取数,`select` 里带着服务端不存在的列,data API 按设计拒绝——它的错误信息还专门解释了为什么不能静默丢弃未知列:那会把窄投影悄悄答成宽投影。 + + 修法的边界是**实测**出来的,不是猜的:把字段存成草稿返回 200 且 `state=draft`,紧接着的 `select` 指名它**仍然** 400。物化发生在**发布**时,所以「有没有保存」是错的问题,「服务端有没有」才是——答案在 `layered().effective` 这条基线里。 + + 因此新增 `publishedFieldNames`(加载时取自基线),并让 `gridColumns` 只保留其中存在的列。新字段照常在右侧检查器里被选中和配置(那本来就是配置它的地方),发布之后它可查询了,才作为列出现在网格里。 + + 过滤放在列数组这一处、而不是取数那一侧,是为了让「网格要什么」只有一个真相源。 + + 回归测试 `DataPillar.gridProjection.test.tsx` 断言在**交给对象视图的列数组**上——那个数组就是投影本身;如果只监视取数调用,日后把数组在下游改一手也能过,缺陷就悄悄搬回来了。撤掉这一行过滤,第二条精确转红、第一条仍绿。 +- 3d8cab5: Interface-page maps: drop the derived marker-title binding, restoring the + object's own declaration as the authority + + `defaultMapFromObject` bound a `titleField` alongside `locationField`, derived + from the object's display field. That binding was added to route around a forge + in `ObjectMap` — `getMapConfig` filled an absent `titleField` with the literal + `'name'`, and the marker title was a plain `record[titleField]` read, so an + object whose display field was not `name` titled every popup `undefined`. The + forge is gone: `ObjectMap` now resolves marker titles through + `@object-ui/core#getRecordDisplayName`, the same ADR-0079 resolver the kanban, + calendar and gantt renderers already used — which is why none of them binds a + derived title either. + + What the binding did once the forge was gone was invert precedence. It reaches + the resolver as `options.titleField`, i.e. step 0 — ahead of `titleField` on the + object, ahead of the declared `nameField` pointer, and ahead of the legacy + `titleFormat` template. A field name derived by the page could therefore only + ever change the answer by out-ranking something the object itself declared; in + every other case it reproduced, at step 0, the string the resolver already + computes further down its ladder. The deriver now binds `locationField` and + nothing else, exactly like its kanban / calendar / gallery / gantt siblings. + + No authoring surface changes. An author's own `map.titleField` is untouched — it + travels as the view-level `map` block, `ListView` merges it per key, and the + resolver honours it at step 0 by design. Objects that declare nothing resolve to + the same field as before, now via the resolver's own type-aware derivation + rather than a binding forced ahead of it; objects that declare a `titleFormat` + template (or a `titleField`) now have that declaration honoured on the map, as + it already was on every other visualization. +- f09c7bc: Interface-page maps: derive a marker-title binding from the object's display field + + An ADR-0047 interface page that whitelists `map` derives its map binding with + `defaultMapFromObject`, which bound only `locationField`. With no `titleField` + reaching `ObjectMap`, `getMapConfig` filled the gap with the literal `'name'` + and the marker title is a plain `record[titleField]` read — so on any object + whose display field is not `name` (for example one keyed by `title`), every + marker popup titled itself `undefined`. + + The derivation now also binds the object's display field, resolved with the + field-name half of ADR-0079's precedence: the declared `nameField` (and its + `displayNameField` / `NAME_FIELD_KEY` aliases), otherwise the shared + `deriveTitleField` scan from `@object-ui/core` — the same ranking the kanban, + calendar and gantt renderers resolve titles through, so a map and a board over + one object agree on what a record is called. When nothing resolves the key is + omitted rather than defaulted. A hand-declared `map` block still wins per key. +- d524bde: The marketplace **catalog** page now tells a non-admin that the runtime has no + marketplace, instead of telling them they lack permission (objectui#5557). + + `MarketplacePage` ordered its two early returns admin-first, so on a runtime that + mounts no marketplace at all (`features.marketplace: false` — an `OS_CLOUD_URL=off` + deployment, the EE deploy template's factory default) an unprivileged member got + "access denied" for a surface that exists for nobody. That answer sends them to + ask an administrator for a grant that would not help them, and it left the + informational disabled state built in objectui#5504 unreachable for every + non-admin. The runtime check now answers first, because "this deployment has no + marketplace" is true regardless of who is asking. + + This restores the sibling-page invariant for the one class of viewer it still + failed for: `MarketplacePackagePage` was reordered the same way in objectui#5533, + so on a marketplace-off runtime the catalog page and the package detail page now + give a non-admin the same kind of answer. + + Scope, deliberately narrow: + + - **Admin-first ordering stays correct where a marketplace exists.** On a runtime + with `features.marketplace: true`, a non-admin still gets `MarketplaceAccessDenied` + — the catalog is an install surface, and a member who cannot install has nothing + to do with it. That boundary is pinned by an explicit test, not left to prose: + without it, a change that simply dropped the admin check would look correct. + - Nothing an admin sees changes, on either kind of runtime. + - No new i18n keys, and no change to `MarketplaceAccessDenied` or + `MarketplaceDisabled` themselves — only which of the two the page reaches for, + and in which order it decides. + - The disabled state is still the server's own answer (`features.marketplace`), + never inferred from a failed request, and it still fails open. +- 13413f3: The marketplace package detail page decides "you are not an admin" before it fetches, + instead of after the load has already failed. + + `MarketplacePackagePage` ordered its early returns with the `!isAdmin` guard *after* + both the loading branch and the `error || !data` branch, and gated its two fetch + effects on `features.marketplace` alone. On a runtime that mounts a marketplace, a + non-admin who opened a package URL was therefore walked through the fetch and the + skeleton, and — when the load failed — was handed the destructive "Failed to load + package" card carrying the server's own error message. Whether that viewer was + refused or handed a diagnosis about a surface they are not allowed to use came down + to whether an unrelated request happened to succeed. + + The guard now sits ahead of both branches, and `getMarketplacePackage` and + `getCloudInstallationInfo` are gated on `isAdmin` as well, so the page stops issuing + requests on behalf of a viewer it has already decided to turn away. That is the + discipline objectui#5533 established on this same page for `features.marketplace`, + applied to the other predicate that decides the same thing. It is also the ordering + `MarketplacePage` carries after objectui#5557, so the two sibling pages now answer one + runtime the same way for every viewer. The server remains the authority on what a + non-admin may fetch; this only stops the client doing work it would discard. + + Unchanged for an admin, deliberately and under test: a failing load still produces the + destructive card with the server's message intact, and a successful one still renders + the package. A "fix" that hoisted the refusal unconditionally, or that deleted the + failure branch, would satisfy every non-admin assertion and fail those two. + + `loading` stays seeded from `marketplaceEnabled` alone rather than from + `marketplaceEnabled && isAdmin`. `isAdmin` reads `activeMember`, which `AuthProvider` + resolves asynchronously *after* the session settles, so an admin whose role comes from + the org member row renders once as a non-admin before the flag flips. Seeding `false` + there would leave that first admin render with `loading: false` and no data — the + destructive card, painted for a frame before the effect could raise the flag again. + `MarketplacePackagePage.guardOrder.test.tsx` pins the flip case for that reason, along + with the ordering, the skipped requests, and the marketplace-off boundary the guard + must not jump above. +- 2573ff4: The marketplace **package detail** page now states that the marketplace is turned + off, instead of red-erroring, on a runtime that has none (objectui#5533). + + `MarketplacePackagePage` takes the same `isMarketplaceEnabled()` early return its + sibling catalog page took in objectui#5504, rendering the informational + `MarketplaceDisabled` state when the server reports `features.marketplace: false` + (an `OS_CLOUD_URL=off` deployment — the EE template's factory default). Until now + the same runtime answered a bookmarked or pasted package URL with a destructive + "Failed to load package / Not found." card, so the two sibling pages reached + opposite conclusions about one runtime: the catalog called it configuration, the + detail page called it a failure. + + Both requests the page fires for its own view are skipped in that state — the + package fetch and the cloud-installation probe — rather than fired and discarded: + a discarded request still reaches the server and can race the destructive card + onto the screen before the disabled state settles. + + Scope, deliberately narrow: + + - **Not** a "swallow all errors" change. With `features.marketplace: true` the page + behaves exactly as before, and a package that genuinely is not there still + renders the destructive card carrying the server's own message. The flag is the + runtime's own answer, never inferred from the shape of a failure, and it fails + open — a runtime that answers nothing keeps its detail page. + - The `installLocal` surfaces are untouched. That is a different capability flag, + and an air-gapped `OS_CLOUD_URL=off` runtime still has a working install-local + path. + - No new i18n keys: `marketplace.disabled.*` and `marketplace.action.backHome` + already ship in all ten locale packs. +- 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no + title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three + times (objectui#5024). + + `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four + states out by hand, 42 lines apart in one file, compared by no gate. They are now + one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s + `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies + were restating a schema rather than filling a gap. + + The user-visible half is the banner. Its title was three independent `&&` branches + with no fallback, while the switch that opens the banner is true for any non-`none` + value — so a lock state outside the four opened the box and left the headline + empty. That is reachable without a fifth state ever being added here: + `MetadataClient.layered()` casts the wire value through unchecked, so a newer + server reaches this banner as-is. Measured, not assumed — feeding `no-publish` + through the page rendered the padlock, the border and an empty title. The title is + now a keyed lookup with a loud fallback that names the unrecognised token, so a + fifth state fails `type-check` here and, if one arrives from a server anyway, the + operator reads a sentence instead of a blank box. +- b2ea297: `ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject` + instead of being a hand-written copy (objectui#5362; maintainer ruling + 2026-08-20: the object document type belongs to the spec). + + What changes on the published type surface: + + - **Gained:** the full spec object-document surface, including the three keys + the runtime already reads but the old interface rejected as excess + properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`, + `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of + the spec document). + - **Removed:** nine members the old interface declared that no objectui + runtime code reads and the spec document does not know: `extends`, + `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling + is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`. + `ObjectTrigger` and `ObjectRelationship` remain exported unchanged. + - **Kept:** `editMode` — the one measured client-side member the runtime reads + (`recordFormNavigation` / `AppContent`) — now declared on the new + `ObjectSchemaClientExtensions` interface, which the derivation intersects. + Note the spec's strict parse rejects `editMode` on published documents + (`unrecognized_keys`); it is a client-type member only. + + Spelling settlement: `listViews` (camelCase) is canonical — `list_views` + appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in + `@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling + READ fallback for stored pre-settlement documents (that stock has never been + censused — objectstack#7917); the CRUD guide and its pinned transcription now + author the canonical spelling. +- 26ff2d6: `sanitizeViewOverride` now strips a half-filled `between` from a stored view + overlay, instead of handing it back to the merge (objectui#5025, + objectstack#8815). + + The overlay recovery pass asked "is this filter row filled in?" with the + shape-blind predicate objectstack#8815 retired — `value == null || value === '' + || (Array.isArray(value) && value.length === 0)`. That is correct for `scalar` + and `list` and blind to `pair`: a `between` carrying one bound is + `['2024-01-01', '']`, an array of length 2, so the pass read it as a real + condition and kept it. The two write paths (`plugin-list`'s `ListView` and + app-shell's `viewFilterFold`) were converted to the builder's arity-aware + `isFilterValueComplete`; this read path was the third verbatim copy, and the one + whose job was to clean up exactly the rows the other two used to write. Until + now a stored half-range survived the pass, reached the query, and the server + refused the whole view (`400 INVALID_FILTER`) for every user on every later + read. + + The pass now delegates to the same `isFilterValueComplete` the write paths use, + on both of the at-rest shapes it handles (the spec `ViewFilterRule` object and + the legacy runtime triple). A complete range — bounds of `0` and `false` + included — is untouched, and the `scalar` and `list` families read exactly as + before, since the retired predicate was already right for them. One shape is + newly stripped beyond the half-filled array: a `between` whose value is a bare + scalar, which `ViewFilterRuleSchema` itself refuses. + + Which operators want no value at all is unchanged and still answered by + app-shell's `VALUELESS_FILTER_OPERATORS` — that set is already derived from the + builder's `VALUELESS_FILTER_BUILDER_OPERATORS` rather than being a private copy, + and its parity is pinned. +- 36fc746: Every envelope reader on the package surfaces now renders a producer-marked + `error.userMessage`, and the Studio package list opens the failure body at all. + + `fetchPackages` — the `/api/v1/packages` read behind the Studio switcher, the writability + courtesy gate, the namespace lookup and the builder landing page — answered a refusal with + `HTTP <status>` and never opened the body, so `message`, `code` and `userMessage` were + discarded together: a 403 whose envelope said `Reading packages requires the studio.access + or setup.access capability.` reached the author as four characters. `apiJson` on the + package admin page read `error.message` and never the mark, and rendered no code. + `duplicatePackage` read `error.message` alone. + + All three now ask one shared rule (`readEnvelopeFailureText`): a producer-marked + `userMessage` outranks the diagnostic `message` at any status — presence of the field is + the producer's marking, and a consumer that sees it renders it verbatim — with `error.code` + appended to whichever prose won. An unmarked refusal is unchanged and still renders its + diagnostic; a body carrying no prose still falls back to each reader's own status text. +- 38158c6: ObjectView no longer forwards `chart.config` onto the `object-chart` node it builds. + + `@objectstack/spec`'s `ListChartConfigSchema` is a `strictObject` declaring exactly + `chartType` / `dataset` / `dimensions` / `values`, and objectui binds it by reference, so + `config` is refused by name (`unrecognized_keys ["config"]`) — by the client schema and by + the same schema the platform's metadata write door parses every save through. The rung was + therefore a channel no conforming author could feed, on both the dataset and the legacy + branch, and it landed on a node that does not declare `config` either. + + For every spec-conforming view this is a no-op. A non-conforming row that somehow carried + the key degrades rather than breaks: `ChartRenderer` generates a container config from + `series` plus a positional palette when none is present, so the chart still renders, with + series-derived labels and default colours. +- 3211397: `ObjectSettingsPanel` now calls `isExternalWider` from `owd-sharing.ts` instead of + re-declaring `OWD_WIDTH` and the ADR-0090 D11 width comparison inline (objectui#5477). + + `owd-sharing.ts` states its own purpose — it is "the single home" for the pieces the + per-object Settings tab and the package-level OWD overview must agree on, the D11 + "external ≤ internal" comparison among them — and the Settings tab was the one surface + not calling it. The two implementations were verified equivalent before the swap, over + the full domain of both dials plus `undefined`, the rejected legacy aliases and + prototype-chain keys: 144 input pairs, zero disagreements. The module's extra `!!` + truthiness guards are redundant at this call site, which already normalizes both dials + to `''`, and `'' in OWD_WIDTH` is false regardless. So no author-visible verdict + changes — the same three pairs warn, and the same twenty-two stay calm. + + The substantive half is the pin that keeps it that way. `ObjectSettingsPanel.owdAgreement.test.tsx` + drives BOTH surfaces over the full 5×5 cross-product of the values their dials offer and + requires all three legs — the Settings tab's rendered warning, the overview's per-row + error, and `isExternalWider` itself — to agree on every pair, with the violating pairs + pinned by name so the sweep cannot pass vacuously. Re-inlining a drifted copy into either + surface, swapping the `(internal, external)` argument order, or refining D11 in only one + place now fails a test instead of silently leaving the authoring surface enforcing the + old rule. +- 76ceb1e: The console's param-collection dialog now titles itself from `action.label` alone — + the unreachable `|| action?.title` fallback beside it is removed (objectui#4282). + + `title` is declared on no action surface in the ecosystem: it is absent from + `@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from + `@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` + inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` + `ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, + `action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand + side of that `||` could not be reached by authored metadata: a fallback that cannot + fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. + Nothing a user hits changes; the line now reads exactly one key, matching the + `description` line directly below it. + + `useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader so the alias cannot + be reinstated silently: an action carrying `title` and no `label` must open an untitled + dialog rather than a dialog named by a key no producer sets. +- 7e81168: `ActionParamDialog`'s "this param carries a reference target" rule now derives from the shared reference-field family instead of the last private copy of it. + + `packages/app-shell/src/utils/paramToField.ts` restated the rule inline as + `LOOKUP_WIDGET_TYPES.has(type) || type === 'user'` — the fourth and last + hand-maintained answer to one question ("does this widget resolve a foreign key, + so hand it `reference_to` / `display_field` / the rest of the picker config"). + The other three converged on `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` in + objectui#4770 / #4790 / #4815; this face is now the fourth. + + No reachable behaviour change. The shared set is one member wider (`tree`), and + that member can never be a widget key on this surface: it is absent from + `fields`' widget map and `mapFieldTypeToFormType` sends it to `field:lookup`, so + every key the rule tests arrives as `lookup`. Both halves are pinned, so + registering a real `tree` widget surfaces the change instead of shipping it + silently. + + The module's second rule — which widget keys degrade to a text input for want of + a declared `referenceTo` — is a different set over overlapping types (`user` + defaults its target to `sys_user` and must never degrade) and was deliberately + left un-merged, matching the same split the plugin-grid twin keeps. + + Also retires a comment that claimed the disjunction "moves in lockstep with + plugin-grid's `bulkParamToField` twin — the two param faces are never split". + Measured on the tip before this change, that was false in both senses: the twin + had read core's Set since objectui#4815 while this line read a private literal, + so the two shared nothing and no gate could report a split; and the two member + sets already differed, by `tree`. Lockstep now holds mechanically — the pin is + on object identity (a spy on core's `has`), so a member-identical private copy + fails where a value check would pass. +- c5200f0: Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. +- 76ceb1e: `RecordDetailView`'s param-collection dialog now titles itself from `action.label` + alone — the unreachable `|| action?.title` fallback beside it is removed + (objectui#5610). + + This was the second copy of the limb objectui#4282 removed from + `useConsoleActionRuntime`. `RecordDetailView` builds its own action runtime rather + than routing through that hook, so the two near-identical `paramCollectionHandler`s + have drifted as a pair and the first fix could not reach this one. + + `title` is declared on no action surface in the ecosystem: it is absent from + `@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from + `@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` + inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` + `ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, + `action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand + side of that `||` could not be reached by authored metadata: a fallback that cannot + fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. + Nothing a user hits changes; the line now reads exactly one key, matching the + `description` line directly below it. + + `RecordDetailView.paramDialogTitle.test.tsx` pins the reader so the alias cannot be + reinstated silently: an action carrying `title` and no `label` must open an untitled + dialog rather than a dialog named by a key no producer sets. A pin per reader is the + only shape that covers both handlers, since the hook's own pin cannot see this site. +- a392e1c: 「打开这条记录 →」卡片——ask 的记录交接终于有了客户端的另一半 + + 服务端半边(cloud#1659 的 `open_record`)先落了地,实测发现它是**半活的**:agent 发出 + `status:'record_handoff'`、回答说「点击上方链接打开」,而上方根本没有链接——控制台 + 有 `build_handoff` 的探测器,这个状态一处都不认识,信号被原样丢弃。 + + 按五步补齐:`detectRecordHandoff`(含持久化 `{type:'text',value}` 包裹形状——replay + 信封那课的规矩)→ live 映射提升 → 水合提升 → 卡片渲染 → 宿主回调。 + + 两个设计点: + + - **app 段点击时现场解析**。记录路由要 `/apps/:app/:object/record/:id`,交接载荷只有 + 对象和记录 id;宿主回调用一次同源元数据读取 `_packageId` 再导航,不给 agent 增加 + 它未必知道的参数。 + - **刻意不做「被取代」置灰**。builder 卡的旧 prompt 会过时,旧的记录链接不会——记录 + 不因为有新交接而失效。 + + 真机闭环验证:问「把《沉默的大多数》标记成已读」→ 卡片渲染 + (`沉默的大多数 — 把阅读状态改为已读`)→ 点击 → 落在 + `/apps/app.hdke/hdke_book/record/<id>` 详情页,「编辑」在手边。 + + 缺任一 id 的交接在探测器就被丢弃,与服务端的拒绝对称——指向空处的卡片比散文更糟。 +- 4ef29f0: Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). + + The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. + + Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. + + `@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. +- fd8dace: Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save + + objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. + + The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door. + + `MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones. + + One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb. + + **BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group. + + Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) still discards per-draft advisories server-side — objectstack#9343, open and unruled — and nothing here compensates for that from the client side. A test pins the absence, so a later traversal of a batch-shaped `published[]` cannot be added without turning it red. +- ad404e0: Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. +- 1eb15c7: Page-block canvas: draw the real icon and colour tone for block types that render fine but are not offered in the palette. + + The canvas read its per-node icon and tone from `BLOCK_TYPE_META`, which is the palette's *offer* list ("what may an author drag in") — while the canvas is answering a different question ("what may an author already have in this page"). The two diverge for an alias pair: `record:discussion` and `record:chatter` are one renderer under two names, so exactly one spelling is always unoffered, and a page carrying it showed a plain unknown-block box and neutral grey instead of the message icon and the blue record tone its twin gets. + + `block-types.ts` now declares `BLOCK_RENDERER_ALIAS_GROUPS` — spellings that resolve to the same registered renderer — and the canvas resolves display chrome through `resolveBlockDisplayMeta()`, which falls back to an alias sibling's entry. Keyed on renderer identity, not on "is excluded": palette exclusions that genuinely are not page blocks (`element:text_input`, `element:record_picker`, `element:form`, `ai:chat_window`) keep the generic box, and nothing became draggable — the palette offer list is unchanged. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- add10d8: Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button. + + Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on + the first surfaces a new author sees. + + **Validation no longer runs on mount.** `新建软件包` opened with both required + fields already red: the create draft is `{ version, type }`, so + `ManifestSchema.safeParse` reported `name` and `id` missing on the very first + render, and the dialog then jumped a line height per field as each error + cleared. `SchemaForm` now defers the error *line* until the row has been + touched — first focusout anywhere in it, or the field's own first edit — for + create forms only. The rule itself did not move: `issues` still reaches the host + unchanged, so the submit button is gated on exactly the same validation, and + edit/view forms still report from mount, where the issues describe stored values + rather than something half-typed. + + **The package form's help text is translated.** The labels came from the + metadata-admin i18n bundle and the help line under each one came straight from + `ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over + "Human-readable package name". `getPackageForm` now reads each field's help + through the same bundle as its label, via a new `tOptional` that returns + `undefined` rather than echoing the key back. Only zh entries exist: an en-US + console finds nothing and keeps falling through to the spec's own sentence, so + the English keeps exactly one producer — `@objectstack/spec` in the framework + repo — and this repo never holds a copy of it to drift. + + **The publish panel's primary button opens clear of the toast stack.** The + console mounts its toaster bottom-right and `DraftChangesPanel` is a + `side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way + there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed + out. Opening the panel now clears the stack the surface the author just left had + raised. Repositioning the toaster was measured and rejected: the draft preview + bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar` + anchors bottom-centre and the nav rail owns the left edge, so a move only + relocates the same collision onto a different primary control. + + Not fixed here, and not fixable here: the other strings the card names are + produced outside this repo. `Owning Business Unit` (`packages/spec`), + `Search Index` (`packages/objectql`) and the `Revise Window` flow node's name + and description (`packages/plugins/plugin-approvals`) all come from the + framework, which owns their translation catalogue; patching them in the console + would create a second source of truth that diverges at the next framework + release. +- 6cc62e2: Studio Interfaces: the nav-leaf binding reads the canonical target key only + + `resolveSurface` fell back to the bare spellings `page` / `object` / + `dashboard` / `report`, and carried a `case 'view'`. Every + `NavigationItemSchema` member is a `strictObject`, none of those bare + spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is + not one of the union's nine members — so all of them are keys `AppSchema` + answers with `unrecognized_keys`, and every one of those branches could only + fire on an app that cannot be saved. The Studio nav item inspector's object + picker likewise read `node.object ?? node.objectName`, preferring the rejected + spelling over the canonical one; it now reads the canonical key first. + + No shape that parses today stops parsing: this narrows the designer back to + what the contract already declares. +- d15a92d: Studio's pre-publish security block can now take you to the object it names. + + The pending-changes sheet reports what the publish door would refuse — as + `object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under + Settings → Record sharing". Naming it was the half that shipped; reaching it was not. + The `?surface=<type>:<name>` deep-link that would have carried the author there + captures the URL exactly ONCE, at mount, and the sheet is opened over an + already-mounted pillar — so writing the param changed the URL and moved nothing. + + That mount-time capture is deliberate and stays exactly as it was: the mirror half + rewrites the param on every in-pillar selection, so a capture that followed the URL + would re-trigger its restore on each one. What was missing is a third half — a live + target delivered BESIDE the URL, which is what a producer already inside the pillar + needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity + (`{type, name}`), the host routes cross-pillar requests back through the URL (that + pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the + author declines over unsaved edits, and the mounted pillar applies the rest. + + Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next + rail reload would drag the author back off whatever they had since selected — the + regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` + pins a hand-picked object surviving a package switch, and + `surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it + still ignores every URL change after mount, and a live request never moves it. + + The sheet's other home is the Home / draft-preview bar, where the Studio object editor + is not a reachable destination at all. Reachability is answered structurally — the + producer hook returns `null` when no host published the channel — so off-Studio the + item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both + directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. + + Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its + signature, its return and its behaviour, and only the Data pillar subscribes to the + new channel. The channel is its own React-only module on purpose — importing the hook + into the sheet would have pulled `nav-selection` and the App-nav inspector into the + console's eager graph. +- 3ece13e: Removed the dead `theme:` entry from `clientValidation.ts`'s `LOADERS` table, which + read `ThemeSchema` off `@objectstack/spec/ui` — a symbol the spec retired upstream + (objectstack#10485 / PR objectstack#10695, which deleted the whole `ui/theme.zod.ts` + module). `theme` was never a registered metadata type, so metadata-admin never asked + for it (objectui#5715). + + This is dead-code hygiene, not a bug fix, and it changes nothing an author can + observe. Two measurements say why, and both cut against the more dramatic reading: + + - The entry is not broken *today*. objectui's own `@objectstack/spec` pin (17.1.0) + still publishes `ThemeSchema` as a working strict Zod schema — measured by + importing the `ui` subpath and by ablation, where restoring the entry made + `validateMetadataDraft('theme', ...)` actively reject a draft. The skew is with + objectstack's `main`, not with what objectui installs, which is exactly why no + gate here ever went red. + - It could never have crashed. `getSchemaForType` duck-checks the loader's result + (`typeof schema.safeParse === 'function'`) and wraps the call in `try/catch`, so an + `undefined` export degrades to a silent no-op validator, never a throw. After a + future spec bump past the retirement that is the branch it would have taken. + + The one thing that was genuinely off: `hasClientValidator` answers from key presence + alone, so it returned `true` for `theme` while the validator behind it would have + judged nothing. Per its own docblock that combination makes `ResourceEditPage` treat + live client issues as the error source and suppress the server's `_diagnostics` — a + stored item rendering as clean. Unreachable in practice, but it is the reason the + entry is worth removing rather than leaving inert. + + The line is replaced by an absence note in the shape the file already uses for + `workflow` and `approval`, because re-adding this entry from the spec would + type-check and go green against the current pin. A metadata-admin theme editing + surface remains a separate capability decision. +- 4c2c389: Studio package lookup now renders a producer-marked `error.userMessage` from the failure + envelope, preferring it over the diagnostic `error.message`. + + `fetchFullPackage` — the `/api/v1/packages` read behind "Package info & settings" and the + managed-snapshot refresh — read only `error.message` and `error.code`. In the 5xx band the + platform withholds the producer's prose and substitutes the generic `Internal server error` + into `message`, while the marked `userMessage` rides through untouched, so an author met a + 500/503 that carried a sentence written for them and was shown the generic one instead. + Per the envelope contract, presence of `userMessage` IS the producer's marking and a + consumer that sees it renders it verbatim; `error.code` still travels alongside, and an + unmarked refusal is unchanged. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- 286dd8d: A view personalization overlay now stores **only the patch** — a column drag no + longer freezes the source view's filter into it (objectui#5233). + + `ObjectView`'s `persistViewPatch` wrote `{ ...baseViewDef, ...patch }`, so an + overlay written by a mere sort/density/column change copied the view's *current + effective* `filter` — along with its `columns`, `label`, `type`, `isDefault` — + into the stored row. The display merge is `{ ...source, ...override }`, so that + copy then outranked the source view forever: an admin edited the view's filter + and every user who had once resized a column silently kept the old filter, with + nothing anywhere reporting it. + + The write now goes through the exported seam `buildPersistedViewBody`. For a + **system view's overlay** the body is the patch plus `viewKind` when the active + tab carries one — identity, not content, and the same line the read side already + draws (`VIEW_OVERLAY_IDENTITY_KEYS`); `object`, `name` and the overlay marker are + stamped by the adapter. A **saved view's own row** is deliberately still written + whole: for it the body *is* the view, there is no source underneath it for a + copied key to shadow, and the write is a whole-document PUT — narrowing it would + delete the user's view rather than narrow it. + + This is the write half of the maintainer's 2026-08-12 ruling (objectstack#7494). + It was blocked until `columnState` was admitted to the view-metadata surface as + an explicitly runtime-only overlay key (objectstack#9933, released in + `@objectstack/spec` 17.1.0) — before that a `columnState`-only patch was refused + `422 INVALID_METADATA`, and the fat copy was the only thing supplying a + recognized key. The read half (`narrowPersonalizationOverlay`) shipped earlier + and stays: rows written before this land are still tolerated on read, and because + the write replaces the whole document, the next toolbar toggle also strips such a + row at rest. No migration. +- Updated dependencies [9c74902] +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [f66072d] +- Updated dependencies [2d7304d] +- Updated dependencies [062943f] +- Updated dependencies [993f312] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [53dc89d] +- Updated dependencies [67a87d9] +- Updated dependencies [baac3f4] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [94e2fa7] +- Updated dependencies [490d9a9] +- Updated dependencies [a1c41c5] +- Updated dependencies [1117414] +- Updated dependencies [6d63cd0] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [7c96c94] +- Updated dependencies [a100f77] +- Updated dependencies [4da5109] +- Updated dependencies [3e853c9] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [17ccec9] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [63d54dd] +- Updated dependencies [7e19d03] +- Updated dependencies [beccf1c] +- Updated dependencies [c5fbe0b] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [2cf69e4] +- Updated dependencies [864154e] +- Updated dependencies [2cf69e4] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [7a72422] +- Updated dependencies [a76b18c] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [b4393e5] +- Updated dependencies [5173a5e] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [b3d562c] +- Updated dependencies [129bcc5] +- Updated dependencies [d3005f7] +- Updated dependencies [c9a7252] +- Updated dependencies [1e7fe0a] +- Updated dependencies [5f19b92] +- Updated dependencies [a26b9e4] +- Updated dependencies [d18a0d3] +- Updated dependencies [9caa7d4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [e0b289d] +- Updated dependencies [46f0bb4] +- Updated dependencies [2da6441] +- Updated dependencies [3b9c774] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [7d2a689] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [c38162d] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [5127378] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [1c19722] +- Updated dependencies [5961030] +- Updated dependencies [faa863d] +- Updated dependencies [129c7a9] +- Updated dependencies [fd814d6] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [854222c] +- Updated dependencies [e929c56] +- Updated dependencies [3777538] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [7975f2d] +- Updated dependencies [bd2f56a] +- Updated dependencies [c4987fb] +- Updated dependencies [f55d666] +- Updated dependencies [f241a4d] +- Updated dependencies [6a7893d] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [73e8c33] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [b49f9a1] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [f9653ae] +- Updated dependencies [7a5da14] +- Updated dependencies [8a6ecac] +- Updated dependencies [babe956] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [891a188] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [4ac3769] +- Updated dependencies [3beef6d] +- Updated dependencies [7d8e546] +- Updated dependencies [d636743] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [19f3637] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [9bd08fe] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [fab4802] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [f07b976] +- Updated dependencies [9409eb9] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [f9984c0] +- Updated dependencies [9700dd9] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [1349400] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [2a7ac32] +- Updated dependencies [220c18d] +- Updated dependencies [eeb6c2f] +- Updated dependencies [5f4514f] +- Updated dependencies [04a67b9] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [0349555] +- Updated dependencies [854cba3] +- Updated dependencies [78ca238] +- Updated dependencies [40c4711] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [84ffdbc] +- Updated dependencies [a276480] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [f626808] +- Updated dependencies [2e32ed4] +- Updated dependencies [5015fcf] +- Updated dependencies [7c3df8f] +- Updated dependencies [67dadd6] +- Updated dependencies [ac257b3] +- Updated dependencies [e21308e] +- Updated dependencies [34ea56d] +- Updated dependencies [a4514e8] +- Updated dependencies [7c3df8f] +- Updated dependencies [6411def] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [bb459ea] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [b61d7d8] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [6ca6e12] +- Updated dependencies [669d71b] +- Updated dependencies [6a99bb2] +- Updated dependencies [2d3fe73] +- Updated dependencies [ed27d7c] +- Updated dependencies [7dedec6] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [ed4a2f1] +- Updated dependencies [0dc2c93] +- Updated dependencies [f0bb9fa] +- Updated dependencies [d327b9c] +- Updated dependencies [81a2eb1] +- Updated dependencies [b8fc1e2] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [1cca678] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [245c7b7] +- Updated dependencies [8fe8e5c] +- Updated dependencies [d80447d] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [fe8f451] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [2af1fa7] +- Updated dependencies [01c27c4] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [7ef3867] +- Updated dependencies [0ead1f6] +- Updated dependencies [bdb4dbd] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [53ded82] +- Updated dependencies [64a252d] +- Updated dependencies [6712930] +- Updated dependencies [d1bebb0] +- Updated dependencies [5a211b2] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [35e6366] +- Updated dependencies [f57ca75] +- Updated dependencies [31a0d05] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [2d80456] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [ebfc3c2] +- Updated dependencies [9a5d669] +- Updated dependencies [d06fba8] +- Updated dependencies [b2e85a9] +- Updated dependencies [79a4b8f] +- Updated dependencies [d5c1f52] +- Updated dependencies [c7cd2b6] +- Updated dependencies [b2065e7] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [686d5d9] +- Updated dependencies [cc3366b] +- Updated dependencies [76ae729] +- Updated dependencies [bc5870c] +- Updated dependencies [f47d94c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [894d103] +- Updated dependencies [a31adc6] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [2e471a6] +- Updated dependencies [3d79455] +- Updated dependencies [cc5de5c] +- Updated dependencies [84d2e98] +- Updated dependencies [f24195a] +- Updated dependencies [56f4e34] +- Updated dependencies [1d9100e] +- Updated dependencies [62c0fa1] +- Updated dependencies [4db5989] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [5eddeeb] +- Updated dependencies [b470e91] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cad512f] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [0b12a33] +- Updated dependencies [105f3c5] +- Updated dependencies [32ef595] +- Updated dependencies [3ccd9e8] +- Updated dependencies [c8da8b3] +- Updated dependencies [689b979] +- Updated dependencies [d6fe1e1] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [15236e0] +- Updated dependencies [ec9fdaa] +- Updated dependencies [d6613a2] +- Updated dependencies [e2e8e68] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [6c1b105] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [425762e] +- Updated dependencies [0fce2ef] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [9850c6e] +- Updated dependencies [9118a31] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [6c5ee71] +- Updated dependencies [84d70bb] +- Updated dependencies [cfc2c7a] +- Updated dependencies [9e725e0] +- Updated dependencies [6f017e9] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [f1c27f0] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [63b4e0e] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [343c598] +- Updated dependencies [af3861f] +- Updated dependencies [0ccbdc1] +- Updated dependencies [515f171] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [17fbbaf] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0d1e702] +- Updated dependencies [b03ba3a] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [a392e1c] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [4ef29f0] +- Updated dependencies [fd8dace] +- Updated dependencies [ad404e0] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [591bf27] +- Updated dependencies [38a9568] +- Updated dependencies [93bbc20] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [2dd9443] +- Updated dependencies [43ca9d5] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [ba306e3] +- Updated dependencies [eddc1dd] +- Updated dependencies [ff2d547] +- Updated dependencies [36918bf] +- Updated dependencies [490f482] +- Updated dependencies [934a532] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [5d79faf] +- Updated dependencies [30266cf] +- Updated dependencies [9a9977c] +- Updated dependencies [57f9b07] +- Updated dependencies [cfcff30] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [6b348d2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [dd35800] +- Updated dependencies [f75810e] +- Updated dependencies [4d73b07] + - @object-ui/plugin-form@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-chatbot@17.7.0 + - @object-ui/plugin-dashboard@17.7.0 + - @object-ui/plugin-report@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/plugin-list@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/plugin-kanban@17.7.0 + - @object-ui/data-objectstack@17.7.0 + - @object-ui/plugin-view@17.7.0 + - @object-ui/plugin-calendar@17.7.0 + - @object-ui/plugin-charts@17.7.0 + - @object-ui/plugin-designer@17.7.0 + - @object-ui/plugin-editor@17.7.0 + - @object-ui/providers@17.7.0 + - @object-ui/layout@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/auth@17.7.0 + - @object-ui/collaboration@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index 040a7c5c90..f51a8c86be 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/app-shell", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 06a23c79be..2b69b32916 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,371 @@ # @object-ui/auth +## 17.7.0 + +### Minor Changes + +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- 686d5d9: `AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424. +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. + +### Patch Changes + +- 639114c: Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122). + + `SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external + datasource validation panel labels those kinds through a map that is TOTAL over + the union on purpose — so an upstream addition fails the build rather than + rendering a blank cell. That mechanism fired: the package did not compile against + 17.3.0 until the kind was labelled. It now reads "Not checked — remote + unreachable", following the spec's own ruling that this kind asserts nothing + about the remote schema and must never be surfaced as "schema changed": it means + introspection could not complete, which is often transient, and labelling it like + a mismatch would tell an operator to repair a schema nobody has read. + + Two internal flow-inspector types were renamed (`FlowNodeLike` → + `InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own + `FlowNodeLike`; neither name is in this package's published entry, so no consumer + import changes. + + `@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with + the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The + `previewMode` prop itself is host-supplied, unchanged, and unaffected. +- ebfc3c2: `ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually + gone and, when it is not, both reports the failure and stops `get()` answering from + the surviving value — instead of swallowing the failed removal (objectui#5731). + + `clear()` nulled `_memoryValue` and then removed the persisted key inside a + `try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a + NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two + halves of `clear()` were not equally strong: nulling memory always sticks, while a + removal that did not stick left the key readable and the read order preferred it. + Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does + not stick", and it was silent — the cleared organization went back on the wire as + `X-Tenant-ID` on every subsequent request. + + The removal is now judged by a READ-BACK rather than by catching the throw, which is + both narrower and wider in the right directions. Wider: a wrapped or proxied + `localStorage` whose `removeItem` is a silent no-op never throws and leaves identical + residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where + every operation throws have nothing readable to resurrect, were already safe, and are + not reported as failures. + + A key whose removal could not be verified is quarantined in memory for the rest of the + page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`, + which `clear()` has just nulled and which a later `set()` refills with the value that + write was meant to persist. The quarantine is released as soon as a removal on that key + sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract + (objectui#5279); a re-stamped signed-out organization is not. + + The failure is not thrown and not returned. All five call sites — sign-out's + `purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`, + `leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive + after the transition they follow up on has already happened, and none can act on a + storage failure; a `boolean` every caller ignores would read as handled when it is not. + So the invariant is restored inside `clear()` and the failure is reported to the + console. + + A working `localStorage` behaves exactly as before: the removal sticks, nothing is + quarantined, nothing is reported, and a non-null persisted read is still authoritative. + `set()`'s swallowed write failure is deliberately untouched — that swallow is + objectui#5703's memory fallback, and it is the correct kind, because the memory copy + upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s. +- 9a5d669: `ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls + back to the in-memory value otherwise, instead of returning the `localStorage` read + unconditionally — so the fallback is reachable in the browser state it was written for + (objectui#5703). + + `set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but + `get()` only consulted `_memoryValue` when the READ itself threw. There is a real + browser state where the read does not throw and the fallback is nonetheless the only + copy: `localStorage` present and readable but rejecting writes — Safari private + browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`. + In that state the active org was stored and could not be read back, measured as + `_memoryValue = org-42` alongside `get() = null`. + + The cost was silent and lasted the whole session rather than the documented first-boot + window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on + any request. Per the edge contract documented on objectui#5279 that header is a routing + hint a reader falls through on — the framework scopes from the session — so no row + visibility rode on this; what was missing is the tenant-routing input, on every request. + `switchOrganization` also appeared to succeed while the client-side stamp never + followed. + + Sign-out is unaffected, and that is the half worth stating: the new fallback fires + exactly when the `localStorage` read is null, which is the state `clear()` leaves + behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That + property is now pinned by test rather than relied upon, so a future `clear()` that only + removed the persisted key fails a test instead of quietly re-stamping a cleared org. + + A non-null persisted read still wins over the memory value, so a working `localStorage` + behaves exactly as before. +- 32ef595: Restore platform-admin detection for permission-set-derived administrators. + + `useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace + gating and the "Build an app" CTAs. Its third source read `user.roles`, a key + the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it + to `positions`). An administrator whose adminship comes from the + `admin_full_access` permission set — the single-tenant deployment shape, where + there is no organization member row and the server deliberately no longer + overwrites `user.role` — matched none of the three sources and read as **not an + administrator**: Setup and Studio simply disappeared for them. + + The hook now reads `user.positions[]`, the one spelling the session publishes. + Detection is restored for that path and unchanged everywhere else: an active + member row with an admin role, a stored `user.role` admin scalar, and + preview/no-auth mode all behave exactly as before, and nobody who was not an + administrator becomes one — pinned by four negative cases alongside the + positive one. + + Also corrects the now-stale documentation that described the removed spelling: + the hook's own docblock, the `roles?: string[]` declaration on the client + `AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and + two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the + comment corrections. +- 343c598: The active organization id is now stored per user, and a change of session user drops the + previous user's client state wholesale (objectui#5664). + + `auth-active-organization-id` was a single un-namespaced `localStorage` key while its + siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, + `flow-palette-recents:u:`). On a browser handed from one account to another — a shared + machine, a kiosk, a handover, a support session — the arriving user's console read the + PREVIOUS user's organization id. The header workspace chip rendered the previous user's + workspace for a user whose `organization/list` was empty, and the consequence past the + cosmetics is the one worth stating: the polluted org context suppressed + `RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so + a brand-new user on that browser silently never got the new-user flow at all. + + Nothing about row visibility rode on this. With the stale id the server answers + `403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero + environments; the damage was entirely in what the client believed about itself. + + Three changes, and the third is the one that closes the class rather than the instance: + + - The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention + its siblings already use. + - It can no longer be written un-namespaced at all. Where no session user is known yet the + value lives in memory for that page-load only — a namespacing that kept a bare-key + fallback would re-open the defect the first time a write happened before the user id + resolved. + - **A change of session user drops the previous user's client state wholesale.** This is an + allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so + the NEXT storage key someone adds without a `:u:` scope is covered before it is written. + Only device-scoped entries survive: the arriving session's own bearer token, the pointer + recording whose state the browser holds, and the UI theme. + + Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a + non-null `localStorage` read and falls back to the in-memory value, and the memory value is + nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge + too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- + sign-in path that never reloads the page. + + Existing browsers are not migrated. A value sitting under the retired bare key is + unattributable — nothing recorded whose org id it is — so migrating it is precisely the + defect it would be migrating away from, and it is deleted instead. A signed-in user loses + nothing durable: the active organization is a server-owned fact that + `AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no + active org is held, including the ADR-0081 single-membership repair. One boot re-supplies + it; users with no organization land on the guided first-run flow, which is the outcome this + card is about. + + `apps/console`'s pre-render auth preflight purges every spelling of the active-org key — + the retired bare one and each `:u:` scope — when it finds a dead bearer token, and + deliberately leaves the session-user pointer in place so the next sign-in can still tell + that the browser changed hands. +- 2dd9443: `purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's + `objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a + single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777). + + The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n` + aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed — + `AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are + the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198 + classifies a surviving entry as a cross-principal disclosure on a shared browser, not + mere staleness — so a partial sweep here is the sharper half of the same defect class + objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`). + + `Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on + its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is + not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()` + (objectui#5731) quarantines a key: this function does not own reads for the metadata + seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to + guard and nothing to quarantine — adding read-back verification would be a general + storage-error-handling refactor of the module, out of this card's scope. What is + mirrored is the reporting channel: a key whose `removeItem` throws is named in a + `console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is + discoverable instead of silent. + + Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata + key, asserting every other metadata key on both sides of it is still swept and unrelated + non-matching keys are untouched, plus a control that the warning fires only on an actual + failure. +- ff2d547: `sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage` + state on a change of session user (objectui#5664 part 3) — now costs one key when a + single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763). + + The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key + `n` aborted the walk, so keys `n+1..end` were never swept, and the failure was + swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt` + believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the + next un-namespaced key — one nobody has written yet — cannot re-open the cross-user + pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys + survived depended on `Object.keys` iteration order rather than on anything bounded. The + previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their + permission-filtered app list, a cross-principal disclosure per objectui#5198) could all + land on the wrong side of the abort. + + `Object.keys(store)` — the reason a guard exists here at all — stays guarded on its + own; only the per-key guard is new, so one uncooperative key now costs exactly that key. + + Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is + NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its + one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a + quarantine are what keep a failed `clear()` from handing the value straight back. + `sweepStore` walks keys it does not own reads for — another package's recents cache, a + metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding + read-back verification for keys this function does not otherwise touch would be a + general storage-error-handling refactor of the module, which this card is scoped away + from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is + named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is + discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in + path, inside an `AuthProvider` effect) still cannot act on the failure and must not + throw either. + + A working `localStorage`/`sessionStorage` behaves exactly as before: every + non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist + (`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected. +- 36918bf: `switchOrganization` now re-resolves identity for the organization it just switched + to by its own explicit decision, instead of depending on an accidental + `TokenStorage` side effect to notice the switch (objectui#5750). + + `AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity + re-resolved across a switch only because `POST /organization/set-active` happens to + return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from + the UNSIGNED `session.token` spelling `getSession()` normally stores — so + `TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription + calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That + signed spelling is deterministic on the raw session token, not on the organization: + two switches with no `get-session` landing in between produce the identical signed + value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity + is left answering for whichever organization the FIRST switch targeted even though + `activeOrganization` already reads as the new one. + + Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage" + link on an org card, plus its own "Back to organizations" button) — ordinary + client-side navigation with no full-page reload and nothing debouncing repeat + switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not + reachable paths for this: both force `window.location.href` immediately after a + successful switch, and the resulting fresh `AuthProvider` mount always performs an + authoritative `loadSession()` regardless of how the race above resolved. + + `switchOrganization` now tracks the organization it last resolved to itself and + re-resolves explicitly whenever a switch's target differs from that, while + suppressing the (now redundant) rotation notification for its own `set-active` + call — so the common single-switch path still spends exactly one `get-session`, not + two. A generation guard discards a still-in-flight, now-superseded switch's answer + rather than let it clobber a later switch's fresher one. +- 934a532: Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the + unstamped-first-request window in which it is not sent (objectui#5279). Documentation + only — no behaviour changes. + + The header had no written contract anywhere, and the shape of the missing information was + actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a + search confined to this repo and the framework (`objectstack`) returns zero readers and + reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was + held until a cloud-side reading came back non-empty. Without the contract written down, + the next person to grep reaches the same false conclusion and deletes a live routing + input. + + `packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means + (a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim, + not an authorization input, not what scopes rows), who stamps it and under exactly which + condition, who reads it, and what a reader may and may not assume. The framework half is + stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the + API-key principal or `session.activeOrganizationId` and from no header — alongside + `plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability. + The configuration half is quoted from the contract this package can actually resolve, + `TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the + default of a configurable `tenantHeaderName` and `header` ranks second of six + identification sources behind `subdomain`. + + The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is + filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` -> + `getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at + all. What a reader observes is documented as **absent, never present-and-empty**, with the + five situations that open the window and the instruction to fall through to the next + identification source rather than fail closed. The gap is recorded, deliberately not + closed: the cloud readers observe today's behaviour, so changing when the header first + appears is its own decision. + + Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes + about the wire — no active organization means no header at all, the stamp is not gated on + `/api/` the way `Authorization` is, and the active organization overwrites a caller-set + `X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed. + ## 17.6.0 ### Minor Changes diff --git a/packages/auth/package.json b/packages/auth/package.json index 51b6385905..161552094b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/auth", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Authentication system for Object UI with AuthProvider, useAuth hook, AuthGuard, and form components.", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index f54db40eeb..6e3b94ff5a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,584 @@ # @object-ui/cli +## 17.7.0 + +### Minor Changes + +- 7dc08a3: `objectui check` recognises a schema by validating it, and reports broken ObjectUI files instead of filing them as foreign ones. + + A file with a root `type` was judged only when its root carried an ObjectUI + structural key (`children`, `body`, `className`, …). Leaf schemas carry only + their own vocabulary, so nothing checked them: measured on this repository, 475 + files were eligible, 166 were judged and 309 were skipped. + + The command now has a second recogniser arm — the document validates as an + ObjectUI component schema under `@object-ui/types`' own Zod union — which the + maintainer's 2026-08-25 ruling selected over shipping a JSON Schema artifact to + point a `$schema` URL at. It admits 209 of those 309 files. The structural arm + still runs first, so recognition costs nothing for files that already had a + marker, and `package.json` is still never judged: `"type": "module"` names no + component the protocol models. + + Validity alone would have answered two different questions with one word. + A broken ObjectUI schema fails validation exactly as a foreign file does, so a + two-bucket report would have filed it as "not ObjectUI" — and the symptom of + that is an absence: the file simply stops being mentioned. Measured, that bucket + is not empty: 54 files land in it and 53 of them are real corpus content. + + So files the recogniser refuses are split. When the root `type` names a + component this build registers, the file is **listed by name** as ObjectUI + content that did not validate, pointing at `objectui validate <file>` for the + reason — either the document is off-spec or its component type is not modelled + by `@object-ui/types`. Everything else is counted as skipped, as before. The + printed explanation now describes both arms, and only unreadable JSON still + makes the command exit non-zero. +- a5d5547: `objectui validate` now prints the failing union arm the document selected, instead of a + bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B). + + `safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a + document matches no arm, Zod reports ONE top-level issue — `invalid_union` · `Invalid + input` · path `(root)` — and hangs every arm's real diagnosis off that issue's `errors` + array, which nothing read. So a menu whose item used the divider spelling retired in + objectui#6523 printed a bare verdict on the whole document, while the remediation text + objectui#6931 wrote into that arm sat one level down, unreachable. + + **What is printed now.** When the top-level issue is a failing union: + + - the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath + the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes, + and **nothing** from the other arms; + - no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few + of the accepted values, ranked by edit distance and **capped** at five + (`MAX_UNION_ARMS_REPORTED`); + - the document declares no `type` at all ⇒ the note says so and offers no candidates — + "nearest" needs something to be near, and an alphabetical slice of 108 arm names + presented as guidance would be a bogus suggestion; + - a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms + both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled + and capped by the same constant. This is the path that finally delivers the + objectui#6523 text to the author. + + Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf + arms, so one mistyped `type` would have produced hundreds of lines. + + `objectui check` is unchanged and deliberately so: it has no zod-issue printer, using + `safeValidateSchema(...).success` as a boolean recogniser. Printing issues behind a + *negative* recognition would flood its report with diagnoses of non-ObjectUI files, the + failure objectui#5127 and objectui#6075 exist to prevent. + + Nothing about which documents are ACCEPTED changes — this is diagnostic output only. +- 94021dd: `objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge. + + A root `type` was treated as a component key wherever it appeared. `type` heads at + least seven unrelated JSON vocabularies, and the most common of them is + `package.json`'s `"type": "module"` — so the first line a user saw running + `objectui check` in their own project was a warning about their own package + manifest. Measured at this repository's root: 46 warnings, 45 of them + `package.json` (objectui#5127). + + A file now enters type judgement only when its root carries a structural key + declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`, + `style`, the `visible`/`hidden`/`disabled` predicate family, `testId`, + `ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an + `.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply + never judged. The key set is read out of the node contract rather than invented, + and it is closed: it grows only when `BaseSchema` grows. + + A list of filenames to exclude was the alternative and was rejected: it is a + second hand-maintained list of the shape objectui#5115 had just finished + deleting, and it can only ever enumerate the foreign vocabularies someone already + thought of. This is a positive marker instead. + + Because the marker narrows what is checked, the command now also reports the + count of files that had a root `type` and no marker, together with the marker + keys that opt one back in. That number is the coverage this gate gives up until + schema files are recognisable, and printing it is what keeps the loss visible + rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was + never type-judged, before this change or after it. Exit codes are untouched: a + JSON parse failure remains the only thing that fails the run. + + No public `$schema` URL is introduced. An earlier revision also admitted a file + whose root `$schema` had an `objectui.org` host; the maintainer ruled against + minting that identifier (2026-08-20, objectui#5127), so the structural key is the + only marker. Because the matching was host-based rather than literal, that arm + can be added later without invalidating a single file. + +### Patch Changes + +- 05474af: Fix `objectui check` scanning build output because its ignore list only excluded a + root-level `dist/` / `node_modules/` (objectui#6320). + + `packages/cli/src/commands/check.ts` passed `ignore: ['node_modules/**', 'dist/**', + '.git/**']` to `globSync`. `glob` matches `ignore` patterns against the path relative to + `cwd`, so an unanchored `dist/**` / `node_modules/**` excludes only a directory of that + name at the scan root — every nested `packages/<name>/dist/`, `examples/<name>/dist/`, + `apps/<name>/dist/` (and their `node_modules/`) was still scanned. In a built workspace + this means `objectui check` re-reads the author's own schemas a second time from build + output, roughly doubling every count it reports (measured on this repository: 617 → 1047 + files globbed after a full build) with nothing in the output explaining why. + + The ignore patterns are now anchored at every depth (`'**/dist/**'`, `'**/node_modules/**'`), + matching the fix's stated intent: exclude build output and installed dependencies wherever + they live, not only at the project root. A root-level `dist/` / `node_modules/` remains + excluded, unchanged. + + Confirmed before widening: no example, template, or docs fixture in this repository + authors a schema under a directory literally named `dist` — the widened pattern excludes + only generated content. +- 854222c: `@object-ui/plugin-report` now registers its three components under namespace + **`plugin-report`**, the spelling its consumers already declare (objectui#6416). + + It used to register `report`, `spec-report` and `report-viewer` under namespace + `report`, while `apps/console` declared the lazy stubs for the same three short + names under `plugin-report` and the CLI's known-type whitelist shipped the + `plugin-report:*` spellings as renderable. Two things followed from the + disagreement: + + - **`plugin-report:report`, `plugin-report:report-viewer` and + `plugin-report:spec-report` could never be satisfied.** `Registry.register` + clears the lazy stub for the type IT registers, and that type was + `report:report`, so those three stubs were never cleared and no component was + ever stored under them: `get('report', 'plugin-report')` returned `undefined` + and `hasLazy('report', 'plugin-report')` stayed `true` forever. A schema + authored with any of the three whitelisted keys resolved to nothing — the + gate handed authors a green light for a key the runtime could not satisfy. + - **The bare `report` key was claimed twice under two different namespaces.** + `Registry.register` and `Registry.registerLazy` share the + `meta?.namespace && !meta?.skipFallback` branch, so what bare `report` + *declared* depended on whether the plugin chunk had loaded yet — the + objectui#6353 shape. + + **No authored metadata changes.** The direction was chosen by measurement: + nothing in this repository, and nothing in the sibling `objectstack` checkout, + authors a `report:*` spelling (0 hits), while the bare spellings are authored in + 48 places. `type: 'report'`, `type: 'spec-report'` and `type: 'report-viewer'` + resolve exactly as before; the three unreachable `report:*` keys are retired and + the three `plugin-report:*` keys now name real components for the first time. + + `packages/cli/src/utils/known-schema-types.ts` is regenerated from the + registrations, dropping `report:report`, `report:report-viewer` and + `report:spec-report`. + + Two pins are the half that outlives the fix: + `packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts` replays + this package's real declared metadata and a console-shaped lazy stub into a + fresh `Registry` in **both** registration orders, checking the bare key's + declared namespace after every step, so order- and phase-independence are + properties under test rather than properties of the file the test imports. + `scripts/__tests__/report-namespace-agreement-6416.test.ts` re-derives both + sites from source and fails if the plugin, the console stubs and the generated + whitelist ever disagree again. +- 85b4957: `objectui validate` now says when a validation issue sits at the document root + (objectui#7004, mechanical half). + + The printer guarded its Path line with `issue.path.length > 0`, so an issue at + `path: []` printed no Path line at all — silent in exactly the case a reader + most needs oriented. That case is the common one, not an edge: the CLI validates + against `AnyComponentSchema`, a union over every component arm, so any document + matching no arm reports a single top-level issue (`invalid_union` · `Invalid + input` · root path). Authors saw a bare verdict on a whole document with nothing + saying which node had been judged: + + ``` + 1. Invalid input + Code: invalid_union + ``` + + Every reported issue now carries a Path line; a root-level one reads + `Path: (root)`, parenthesised so it cannot be mistaken for a real key named + `root`. Non-root issues print their authored path exactly as before. + + Scope: the printer still reads only top-level issues. Whether a failing union + should also surface its per-arm diagnoses — and if so which arm's — is an + author-facing diagnostic contract left open on objectui#7004 for a maintainer + ruling, and is deliberately not decided here. +- 639114c: Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122). + + ⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.** + The exported `ObjectSchemaClientExtensions` narrows from + + ```ts + export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' } + ``` + + to + + ```ts + export type ObjectSchemaClientExtensions = Record<never, never>; + ``` + + Two breaking consequences for a consumer that names the type directly. **(1)** It + no longer declares `editMode`; the key is now carried by the spec's + `ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against + the extension type ALONE loses it. **(2)** `interface` → type alias also ends + **declaration merging**: a consumer that reopened + `declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }` + to add its own client-side member no longer compiles, because an alias cannot be + reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule — + objectui's own breaking changes are graded `minor` with the semantics stated in + the body, since any `major` in the fixed group would push all 39 packages off + `@objectstack`'s major. + + **`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured: + the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty, + declared as the same `'page' | 'modal'` union objectui carried). Its local copy + is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin + prescribed for this event, leaving the client delta empty. Nothing is removed + from the product: `editMode` stays authorable and stays typed on + `ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a + local member — and a published, spec-validated object document may now carry it, + which at 17.2.0 was refused by name. + + **`user:profile` is retired across all three sites.** 17.3.0 dropped it from + `PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly + `['user:profile', 'element:form']`, gained set empty). objectui went on knowing + it in three places, so all three moved together: the Studio palette exclusion + ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the + regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing + user-reachable went with it — neither type had a renderer, `user:profile` had + only the dashed "Component Placeholder" scaffold, and the app shell's own + profile affordance is a React slot, never this block type. A page schema still + naming it now draws the loud "Unknown component type" panel rather than a silent + grey box, which is this repo's standing treatment for a type outside the + supported surface. + + **`record:details` sections document the eight keys 17.3.0 added.** + `group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`, + `description` and `headerColor` are now declared on a section entry (4 → 12 + members). Six of the eight are already honoured by `DetailSection`, so the + `sections` input description now teaches all of them, and says plainly which two + are not read here. Designer controls for them are a separate feature and are + deliberately not added. + + **`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` → + `^17.3.0`, and this is the second half of its `minor`.** The package's emitted + `dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which + `17.0.0` does not export, so the old range was a claim the artifact did not + support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]` + and names `^17.3.0` as the lowest version carrying every symbol the package + references. Breaking for a consumer pinned below 17.3.0: it can no longer + resolve this package. That is the range stating the truth rather than a new + restriction — the artifact already required those symbols — and it is the + remedy the gate itself prescribes ("Raise that package's range to the lowest + version that exports the symbol… Do not add a tolerant re-declaration on this + side: the range is the claim, and the claim is what is wrong", objectui#5793). + `@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0` + and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is + this repo's normal state, not an exception. + + ⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing + and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main + workflow that cannot red a pull request, and `main` is green on it — the last + eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1 + with CI's own `--cross-check` invocation, and exits 0 with this raise, judging + 278 (subpath, symbol) pairs across 19 published packages either way. Its blocking + copy runs on the publish path, so leaving it would have surfaced as a cancelled + release rather than as a red check. The correction is recorded on objectui#7688. +- 100547e: `objectui validate` now refuses a form field whose widget id names a namespace + other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` + has given since objectui#5375 (objectui#5449). + + The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema + declared `type` and `widget` as bare optional strings — so a field typed + `ui:password` validated clean while the runtime validator rejected the same + document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an + author actually runs before shipping, so it was the one handing out the false + green: an author did exactly the diligence objectui#5375 asks for and still + shipped metadata that renders a secret into a plain text box. + + A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's + precedence (`widget` before `type`), the key it blames, its error code and its + message verbatim, so the two entry points cannot describe one defect two ways. + + **This rejects documents that previously validated.** Only colon-qualified + field widget ids outside the `field:` namespace are affected — `field:`-prefixed + ids and bare names such as `password` still pass, registered or not. A field + carrying, say, `type: 'ui:password'` must be rewritten as `password` or + `field:password`; it never rendered as a password box in any case. + + Which of the repo's authoring-time validators is canonical remains open + (objectui#4631) — this states the rule on the zod side rather than unifying + them. +- d91aed9: Name the case-only spelling when a component type misses the registry. + + Registry lookup is exactly case-sensitive, so a node typed `Page` misses a registered `page` and falls through to the OBJUI-001 "Unknown component type" panel. Because the mistake is usually uniform across a document, the symptom is not one broken widget — it is the whole page rendering as error panels, with nothing in the message pointing at the cause. + + Both surfaces that report the miss now name the spelling that would have resolved. `SchemaRenderer`'s panel reads `Unknown component type: Page — did you mean 'page'?`, and `objectui check` reports `Unknown schema type "Page" in <file> — did you mean "page"?`. When no known type differs by case alone, neither says anything extra — `zzz` gains no bogus suggestion, and this is case matching, not an edit distance, so `pge` suggests nothing either. + + **Lookup itself does not change.** `Page` still misses, still fails, and still renders the panel; only the message teaches. Normalising the lookup was considered and rejected (objectui#5247, maintainer ruling 2026-08-19): it would make two spellings valid everywhere, permanently, and legalise the typo class (`PAGE`, `pAge`) along with the PascalCase convention. + + Each surface reads its candidates from the set it can actually trust — the renderer from the live `ComponentRegistry` (including pending lazy stubs), the CLI from the registration-derived `KNOWN_SCHEMA_TYPES` snapshot — so neither can suggest a type nothing registers. +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [b9f5ff1] +- Updated dependencies [4704aa4] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [6bca0e4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [c86185e] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 181a4f5de3..e7d235eee9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/cli", - "version": "17.6.0", + "version": "17.7.0", "description": "Standalone CLI for Object UI — scaffold, develop, build and validate JSON/YAML schema-driven applications.", "type": "module", "homepage": "https://www.objectui.org/docs/utilities/cli", diff --git a/packages/collaboration/CHANGELOG.md b/packages/collaboration/CHANGELOG.md index 85e1e442d7..a8e2a6a5df 100644 --- a/packages/collaboration/CHANGELOG.md +++ b/packages/collaboration/CHANGELOG.md @@ -1,5 +1,224 @@ # @object-ui/collaboration +## 17.7.0 + +### Patch Changes + +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [41b7ce3] +- Updated dependencies [d2fb6ef] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [d6ceb8d] +- Updated dependencies [969ba84] +- Updated dependencies [dbd5194] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [20cb8db] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [6bca0e4] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [fd8dace] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [38a9568] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index cbc2a66063..7dd34aea1e 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/collaboration", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Real-time collaboration for Object UI with presence tracking, live cursors, conflict resolution, and comment threads.", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d8a3eadc68..1c0077c3c7 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,3760 @@ # @object-ui/components +## 17.7.0 + +### Minor Changes + +- a04d7c6: Mint the `box` component type — the class-transparent neutral block container + (objectui#3965, maintainer ruling 2026-08-29 方案 A). + + The JSON authoring vocabulary had no neutral block box, which is why the + deprecated `div` could never actually retire: every replacement the deprecation + notice names injects layout of its own (`container` adds width/centering and a + responsive padding ramp, `flex`/`stack` add a display mode and gaps, `grid` adds + `grid-cols-*`, `card` adds border/shadow and wraps children in a `CardContent` + element — all measured through the real `SchemaRenderer`). `box` closes that + gap with a three-clause contract, pinned in + `packages/components/src/renderers/__tests__/box-neutral-container.test.tsx`: + + 1. renders `children`; + 2. authored `className` passes through **verbatim**; + 3. **zero** injected classes. + + Deliberately unlike `div`, `box` reads `children` only — never `schema.body`. + The `div` renderer's `children || body` fallback is what made a mechanical + `div`→X swap silently drop content on `body`-authoring nodes while the element + count stayed unchanged; content moves into `children` at migration time (the + objectui#6771 B-ruling direction). + + Landed on both contract faces per the zod-mirror-parity pairing (objectui#6424 + family form): `BoxSchema` interface in `@object-ui/types`, its zod mirror in + `@object-ui/types/zod`, the `SchemaRegistry['box']` entry, and the registry + registration (`namespace: 'ui'`, `isContainer: true`). With `box` landed, the + catalog's 25 remaining `div`-authoring fixtures (80 nodes) migrate to it + mechanically with zero render difference, and the catalog ratchet closes to + zero tolerance for JSON-authored `div`. +- d796c8d: Auto-derived related lists consume the field-level `relatedListFilter` + declaration — the list query AND-composed, the tab badge counting the same set + (objectui#4664). + + A `lookup` / `master_detail` field may now declare `relatedListFilter`, a + canonical Query-DSL `FilterCondition` such as `{ status: { $ne: 'deleted' } }` + (`@objectstack/spec` 17.1.0 — objectstack#8704 / PR #8955). Until now this repo + accepted that key at every gate and consumed it nowhere: a record page's derived + related lists answered wider than the metadata asked, and the driving scenario — + soft-deleted child rows on auto-derived record pages — had no way to be + expressed at all. + + What ships: + + - `deriveRelatedLists` reads the key off the FK and carries it on the derived + descriptor; `RecordDetailView` forwards it into the page synthesizer, which + emits it onto the `record:related_list` node's **existing** `filter` key. That + key already had a read site (objectstack#7118): `RelatedList` ANDs it with + `{ [referenceField]: parentId }`. The declared filter is therefore an authored + constraint that may only NARROW this parent's children — never a replacement, + which would leak other parents' rows — and no second filter dialect appears + for derived pages. + - The **tab badge honours the same composed filter**. `page:tabs` reads the + `filter` off the `record:related_list` node it is badging and the count store + composes it with the parent scope through the same `mergeFilterNodes` sink the + row query uses, so the badge and the rows send one `$filter`. Badge-count + parity is normative in the spec key's own contract text; without this half the + feature would ship the defect it exists to prevent — a badge saying 7 above a + list showing 3. + - Counts cache per (object, relationship, parent, **scope**), so a filtered and + an unfiltered probe over the same relationship are separate entries rather + than one wrong number. + + With no `relatedListFilter` declared, the synthesized node, the row query and + the badge probe are byte-identical to before. Consumption only — this change + adds no authoring UI for the filter. +- 4172589: **Breaking for authored metadata:** a `data-table` column spelled `name` no + longer resolves its cells. Use the declared `accessorKey`. + + `data-table`'s column normalization used to read + `accessorKey: col.accessorKey || col.name` — but `TableColumn` + (`@object-ui/types`) declares only `accessorKey`, never `name`. The declared + surface admitted one spelling while the runtime admitted two, which is the + second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of + 2026-08-20 settled the direction for the whole family: retire the consumer-side + alias, translate at the producers. `label` → `header` retired first + (objectui#5351); this retires `name` → `accessorKey` and closes the family. + + **Who is affected — a column authored DIRECTLY onto a `data-table` node:** + + ```json + { "type": "data-table", + "columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated + ``` + + becomes + + ```json + { "type": "data-table", + "columns": [{ "header": "Email", "accessorKey": "email" }] } + ``` + + **Who is NOT affected.** Columns reaching the table through `object-data-table`, + a detail view's `related[]` list, or `object-grid` are unchanged — the adapter + never sees a legacy spelling from any of them. The reason differs by producer, + and the difference matters if you are debugging one: + + - `object-data-table` and a detail view's `related[]` list **resolve** the + legacy spelling before delivery, stamping `accessorKey` from `name` (via + `columnIdentity`). A `name`-spelled column keeps working there. + - `object-grid` **refuses** it instead: since objectui#5068 an authored column + must spell the declared `field`, and one that does not is dropped at intake + and never reaches the table. Its delivered columns carry `accessorKey` + stamped from `field`. So a `name`-spelled `object-grid` column does not + render today either — that is objectui#5352's open question, unchanged by + this release. + + Only the directly-authored `data-table` node narrows here. + + **How the break presents, so you can recognise it:** the column is not dropped + and nothing is thrown — its header still renders over blank cells, and + neighbouring columns are unaffected. If a table's header row looks right but one + column's cells are empty, check that column's key spelling first. + + The two published skill guides that taught the `name` spelling + (`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate + in this same release, so the platform never refuses a spelling it still ships. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is a + breaking change for any author who used the tolerated spelling. It is not + `major` per this repo's fixed-group convention (objectui's own breaking changes + ship as `minor`; the group's major tracks `@objectstack`). +- d2fb6ef: **BREAKING (authoring): `ui:icon` names its glyph with `icon`, not `name`** + + `{ "type": "icon", "name": "check" }` no longer renders an icon. Write + `{ "type": "icon", "icon": "check" }`. Stored metadata authored before this + release needs converting — see the migration below. + + Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` + outside an `@objectstack` major sync); the break is real and is stated here. + + **Why** + + `name` is the SDUI identity key every authored node carries, alongside `id` — + it is not `ui:icon`'s private prop. So an ordinary node like + `{ type: 'icon', id: 'save_icon', name: 'save_icon' }` asked lucide for a glyph + called `SaveIcon`, missed, and rendered **nothing at all**: silent to a human, + and clean to a DOM gate, because a renderer that renders nothing spreads no + attributes to find. `action:*` already reads `icon`, so this is the vocabulary's + existing answer, and it leaves no node type on which the identity key is + unusable. + + **What changed** + + - `IconSchema` (types + its zod mirror) declares `icon: string` **required**, + exactly as `name` was required before it — a key rename at constant + strictness. `name` reverts to the optional identity inherited from + `BaseSchema`. The mirror previously *required* `name`, which is why the + renderer could not be migrated on its own: the published contract refused the + correct shape. + - `ui:icon` resolves its glyph from `schema.icon`. There is deliberately **no** + `icon ?? name` fallback: a key meaning "identity" or "glyph" depending on + whether a lucide lookup happened to hit is the ambiguity being removed. + - The registry's `inputs` entry and `content/docs/components/basic/icon.mdx` + moved in the same change as the resolver. + - All 98 authored icon nodes in this repo are converted. + + **The break is loud in three places, never silent** + + 1. `IconSchema` **refuses** a legacy node, with a message that names the rename + and points at the converter — not zod's default `expected string, received + undefined`. + 2. A legacy node that reaches the renderer unvalidated draws the visible + placeholder shipped in the previous release, and its `console.warn` now + carries the exact rename (`icon: "save_icon"`) plus the converter's name. + Its accessible name says so too, and it gains a + `data-objectui-icon-legacy-name-key` marker so a gate can tell + "unmigrated node" from "glyph that does not resolve". + 3. **Migration for stored metadata** — `migrateIconNodeKeys` from + `@object-ui/types`: + + ```ts + import { migrateIconNodeKeys } from '@object-ui/types'; + + const { document, converted, warnings } = migrateIconNodeKeys(storedPage); + if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n')); + if (document !== storedPage) await save(document); + ``` + + It walks the whole document and lifts `name` to `icon` on every icon node. + It is a one-shot conversion a deployer runs over stored documents — **not** a + read-path fallback; nothing calls it during rendering or parsing. It + **reports rather than guesses** for the two cases it will not touch: a node + already declaring both keys (`icon` wins, `name` stays the identity it is), + and a node naming no glyph at all. +- 7cd3987: `ui:icon`: an unresolvable glyph now renders a visible placeholder instead of nothing + + An icon whose name does not resolve to a lucide glyph used to `return null`. + That failed silently in two independent ways at once: invisible to a human (no + gap, no error boundary — just an absent glyph), and clean-looking to a gate (a + renderer that returns `null` spreads no attributes, so a DOM scan of it reports + no findings). + + It now renders a dashed-square placeholder on the same SVG host, keeping the + authored `className`, `size` and colour so the gap sits exactly where the icon + would have been, with `role="img"`, an accessible name identifying the icon + that failed, and a `data-objectui-icon-unresolved` marker. The `console.warn` + stays and now names the cause. + + Also fixed: a node with no `name` at all reached `toPascalCase(undefined)` and + threw, which the error boundary then swallowed — a third silent failure. It + renders the placeholder too. + + Not included: `ui:icon` still reads the SDUI identity key `name` as its glyph + name. Moving it to `schema.icon` is ruled but blocked on an authored-metadata + migration — see objectui#5631. +- f8cdbf2: Route a `data-table`'s `emptyAction` node through `SchemaRenderer`, so a `visibleWhen` + authored on it is actually evaluated (objectui#5926 gap 1). + + `visibleWhen` is not a per-block concern in this platform. It is enforced **once, + generically**, in `packages/react/src/SchemaRenderer.tsx`: `shouldHide` tests `visibleWhen` + ahead of the hoisted `visible` (objectui#5454), sets `_hidden`, and the `_hidden` early + return fires **before** the registry dispatches. A block renderer cannot ignore the gate, + because it never sees the node. + + `emptyAction` was the one authored-node exception in the tree. The empty-state CTA slot + resolved the registry **directly** — `ComponentRegistry.get(node.type)` — and mounted the + result itself, so the node never passed through `SchemaRenderer` and its `visibleWhen` was + **never evaluated**. `@objectstack/spec` accepts the key (`SchemaNodeSchema` carries + `visibleWhen`, and `data-display.zod.ts` types `emptyAction` as a `SchemaNode`), so an + author wrote a gate, the platform took it, and nothing enforced it — declared-not-enforced, + the same class objectui#5401 / #5505 closed for `record:alert`, one level down. + + Measured on the branch point, mounting a `data-table` with no rows so the empty state is + actually reached: an `emptyAction` carrying + `visibleWhen: { dialect: 'cel', source: 'features.can_create == true' }` under an ambient + scope of `features.can_create = false` **rendered**, and so did the bare-string spelling of + the same predicate. Both now render nothing. The three must-show cases were pinned in the + same file and were green before and after — an `emptyAction` whose predicate resolves + **true**, one declaring **no** `visibleWhen`, and one whose predicate names an unbound root + (the central gate fails soft to visible, and this slot now gives the same answer as every + other node rather than a private one). + + The fix is a **route**, not a new check: no `visibleWhen` test was added to `data-table.tsx`. + A local check on this slot would have been a fourth evaluator, which is the drift + `page:tabs`' item-level predicate already records on this card. The slot now mounts its + authored node exactly the way the `empty` renderer's `action` slot always has. + + **Behaviour change worth declaring, beyond the gate itself.** No declaration moves and no + new key is accepted, but two host-observable answers change on this slot: + + - An `emptyAction` whose `visibleWhen` resolves false stops rendering. That is the fix. + - An `emptyAction` whose `type` is missing or names an unregistered component now gets the + platform's uniform "unknown component type" report instead of rendering as silent + nothing. Malformed metadata gets one answer across the tree rather than a private one + here — but a page that shipped a typo'd `emptyAction` type went from invisible to visibly + reported. +- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). + + Seven modules resolved authored icon names into lucide's runtime `icons` record, each + with its own copy of the logic: **three different tokenisers** (`split('-')` on five of + them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` + rename on only **four** of the seven. The same authored name therefore rendered on one + surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened + with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, + and the other six call it. + + **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it + was measured rather than chosen.** Its regression set is empty three independent ways: + against the authored population, against a maximally-pessimistic every-authored-name x + every-surface cross-product, and against a bound-free differential over 8,298 spellings + derived from all 1,767 live record keys — each with a discrimination control that fired + in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface + pairs in that last reading, stripping two surfaces of every snake_case and + space-separated spelling they resolve today. + + **What changes for you — all of it widening, none of it removal.** No name that resolved + before stops resolving: no key of lucide's record contains `_`, whitespace or `-` + (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the + wider one produces the same key. Sixteen name-surface pairs start resolving where they + rendered a fallback or nothing before: + + - `layout_dashboard` and `building_2` (and every other snake_case or space-separated + spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, + `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and + the related list. + - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried + no rename map. `Home` is not a live record key, so this could only ever be a widening. + + **What does NOT change: what each surface draws when a name does not resolve.** The seam + answers `name -> component`, returning `null`, and decides nothing else (maintainer + ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at + the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning + (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, + `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` + and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and + then dropped once the tree was measured to have four such behaviours rather than two: a + lookup function is the wrong place to publish a presentation decision. + + `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface + this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: + its census goes from seven sites to one, and its normalisation stops being a + widest-common approximation of three disagreeing resolvers — so the under-reporting that + gate disclosed at objectui#5932 is closed rather than merely bounded. +- 6fd2cf7: `radio-group` now renders the `orientation` its own type has always declared (objectui#6158). + + `RadioGroupSchema.orientation` was declared in two layers and read by none. The shipped TS + type carries `orientation?: 'horizontal' | 'vertical'` with `@default 'vertical'` + (`packages/types/src/form.ts:383`) and the zod mirror carries the matching + `z.enum(['horizontal', 'vertical'])` (`packages/types/src/zod/form.zod.ts:282`), while + `packages/components/src/renderers/form/radio-group.tsx` contained neither the string + `orientation` nor `direction` and forwarded only `defaultValue`, `className`, the + form-control DOM whitelist and the designer props. + + The consequence was measurable rather than cosmetic: every radiogroup root the library + rendered came back byte-identical on that axis — no `data-orientation`, no + `aria-orientation` — so the docs page's `## Layout Options` section demonstrated a + distinction the product could not make, and its horizontal demo rendered vertically. An + author reading the shipped type had every reason to write `orientation: 'horizontal'` and + no way to discover it was inert. + + The key is now forwarded to the underlying Radix `RadioGroup`, which accepts it natively + with the same two-value vocabulary and puts it on the root as `aria-orientation` and + `data-orientation`; the layout utilities follow it so the visible difference the docs + promise is real. This restores declared = enforced **without widening the acceptance + set** — no new key is accepted, and no spelling outside the declared enum becomes legal. + + Two behaviour notes for anyone already shipping radio groups: + + - The declared `@default 'vertical'` is now actually applied instead of being left to + Radix's own `undefined`. A group that never authored the key keeps the vertical stack it + already rendered, and additionally announces `aria-orientation="vertical"` — the + announced orientation now agrees with the rendered one rather than being absent. Arrow + key roving focus narrows to Up/Down for those groups, which is the correct pairing for a + vertical stack. + - Author `className` still wins: the orientation layout utilities compose first and the + authored class last, so tailwind-merge resolves every conflict in the author's favour. + + Registry meta `inputs` for `radio-group` gains `orientation` in the same change — it was + the third surface that omitted the key, and leaving it out would have kept the designer + palette disagreeing with the type. +- 5ef9c4f: The section grouping contract (objectui#6236, maintainer ruling 2026-08-27): a + `section-divider` row may now CLAIM its member fields — `FormField.fields: string[]`, the + same membership shape `FormFieldTab.fields` / `FormFieldPane.fields` already model — and + the form renderer then gates the WHOLE group on the divider's own visibility verdict + (`visibleWhen` / `visibleOn` / legacy `condition`). + + Before this, one authored `FormSection.visibleWhen` meant two different things: the + console renderer drops the whole `<section>` (heading and fields), while the plugin-form + chain's renderer treated `section-divider` as a purely presentational row and hid only + the HEADING, leaving the section's fields rendering (measured in objectui#6111, which + pinned that honestly rather than implying a guarantee it did not deliver). + + Ruled semantics, now pinned in `section-grouping-6236.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** (console precedent, 2026-08-22 + ruling after #5594) — a hidden section's values still submit. + - **A hidden section's fields skip client-side validation** — a user is never blocked by + an error pointing at a control they cannot see (the objectui#6110 defect shape); the + server-side contract remains the loud floor for genuinely-required data. A section + hiding mid-session also clears its members' stale errors, the way a field's own false + predicate already did. + - **A divider without a claim keeps the old contract** (its predicate gates only the + heading), so existing schemas are untouched. + + Both halves ride the mechanism the field-level predicate already uses (return `null`; + react-hook-form keeps the value and skips the unmounted control), so field-level and + section-level visibility cannot drift apart. The zod mirror (`FormFieldSchema`) declares + the key with the same scope note. + + `@object-ui/plugin-form` wires the producer half: all six `section-divider` synthesis + sites (ObjectForm's stacked simple path, ModalForm's sectioned and derived-fieldGroup + paths, DrawerForm's sectioned and derived-fieldGroup paths, SplitForm's panes) now stamp + the membership claim onto the divider they emit, from the RESOLVED member list — so an + authored `FormSection.visibleWhen` finally hides the whole section on the object-view + chain, matching the console renderer. The #6111 honest pin (`measured scope`) flipped + accordingly: it now pins heading-and-fields hiding together, and every per-layout DENIED + row asserts the claimed member as well as the heading. The derived-fieldGroup sites carry + the claim for uniformity but stay fail-open — the spec `fieldGroups` vocabulary has no + section-predicate slot to author. The tabbed arm's predicate slot (objectui#6237) is + designed to reuse this same grouping contract. +- 46f0bb4: The tabbed arm of the grouping contract (objectui#6237, same maintainer ruling as + objectui#6236): `FormFieldTab` gains the predicate slot the ruling named — + `visibleWhen?: string | { dialect?: string; source: string }` — so a section rendered as + a TAB PANEL (`ModalForm` `contentLayout: 'tabbed'`) can finally carry an authored + `FormSection.visibleWhen`. The tabbed layout synthesises no `section-divider` at all, so + the #6236 membership-claim mechanism had nothing to stamp the predicate onto and no slot + to copy it into; the predicate was silently dropped one hop before evaluation (measured + in objectui#6237's card). + + The form renderer evaluates the tab's predicate with the same record assembly the + field-level rules use (`ruleRecord` / `previousRecord` / host predicate scope, #6010), + fail-open, and when FALSE draws neither the tab's trigger nor its panel. Not drawing the + panel unmounts the claimed fields through the exact mechanism a field's own false + predicate uses, so the ruled hidden-group semantics are inherited rather than + re-implemented, and are pinned in `fieldtab-visiblewhen-6237.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** — a hidden tab's values still + submit. + - **A hidden tab's fields skip client-side validation** — a user is never blocked by an + error pointing at a control they cannot see; the server-side contract remains the loud + floor for genuinely-required data (#2959's trap, answered the same way for tabs as for + sections). A tab hiding mid-session clears its members' stale errors. + - **Deterministic re-selection**: a predicate hiding the ACTIVE tab activates the user's + pick if still visible, else the declared default, else the first visible tab — never an + empty panel — and the user's pick is restored the moment its tab is re-admitted. + - **No mid-interaction collapse**: whether the tabbed arm engages stays judged on the + DECLARED tabs, so a predicate hiding one of two tabs filters the strip (and hides the + tab's fields) instead of collapsing the modal into the stacked layout under the user's + cursor. With every tab hidden the strip is omitted; unclaimed fields still render. + - **A tab without the key keeps the pre-#6237 contract** (always drawn), so existing + schemas are untouched. + + `@object-ui/plugin-form` wires the producer half: `ModalForm`'s tabbed synthesis site now + copies the section's `visibleWhen` onto the tab it emits, and the #6111 layout matrix + gains the tabbed-modal rows (direct and via `ObjectForm` delegation). `TabbedForm` / + `WizardForm` still declare no section predicate in their own section configs — those arms + remain open on objectui#6237. +- 8ec11e1: `page:header` resolves its `actions` as declared ACTION IDS (objectui#6252, + implementing the objectstack#11592 ruling — maintainer, 2026-08-25, on + recommendation B). + + `@objectstack/spec`'s `PageHeaderProps.actions` is + `z.array(z.string()).describe('Action IDs to show in header')` and has been for + as long as the key has existed. The canonical renderer read that array as + `ActionDef` objects and resolved nothing, so a header authored the way the + published contract declares rendered **zero buttons** — satisfying the schema + deleted the header. Two sibling surfaces already read it as ids + (`record:quick_actions`, and `layout:page-header` by delegating to it), so one + authoring key meant two different things depending on which renderer drew the + header. + + Each id is now resolved against the object's own `actions` metadata through the + same `useMetadataItem` entry `record:quick_actions` uses. Resolution happens at + the top of the actions pipeline, so the existing chain — `record_header` / + `record_more` placement, the `requiredPermissions` capability gate, `visible` / + `hidden`, `order`, and the inline/overflow split — runs unchanged over + uniformly-shaped defs: an id-authored header and an object-authored one converge + before a single filter runs. + + - Inline `ActionDef` objects keep rendering, per element, so a half-migrated + `['convert', { … }]` array resolves the id and passes the object through. This + is renderer tolerance for the migration and stays undeclared — the contract is + ids. + - An id that resolves to no action renders nothing and warns **once**, naming + the object's declared action names. A mistyped id is no longer indistinguishable + from a correctly hidden one. + - Nothing is written back onto the authored node, so an id-authored page carries + no `ActionDef` — and no `body.source` handler body — into what it serializes. +- 6e88630: One authority per exported type name, batch 3 of objectui#6349: `ComboboxOption`, + `NamedActionDef`, `OrgTranslate`. + + **`@object-ui/components` — `ComboboxOption` now IS `@object-ui/types`' declaration.** + The component declared its own `{ value, label }`, a strict subset of the + `ComboboxOption` that `@object-ui/types` declares for `ComboboxSchema.options` and + mirrors in `form.zod.ts` (`{ value, label, disabled? }`). The component now re-exports + the types declaration (through the `@object-ui/types/form` subpath — the root barrel + does not publish the name), so the name `ComboboxOption` exported from + `@object-ui/components` gains the optional `disabled?: boolean` member. Every value + that type-checked before still does — nothing narrows and no key changes type; the + one thing that moves is `keyof ComboboxOption`, so a consumer that EXHAUSTS the type + (a `Record` over its keys) will need the new key. Note that the `Combobox` component + itself does not read `option.disabled` — that member was already declared on the + `@object-ui/types` face and is now visible on this one too; it is recorded as a + separate finding, not changed here. + + **`@object-ui/plugin-grid` / `@object-ui/app-shell` — internal, surface unchanged.** + `NamedActionDef` was declared identically in `resolveBulkActions.ts` and + `resolveLegacyRowActions.ts`; the latter is now the one authority and the former + re-exports it. `OrgTranslate` was declared identically in `orgErrorMessage.ts` and + `orgRoleLabel.ts`; the former is now the one authority and the latter re-exports it. + Neither name is on its package's public entry, and every deep-`dist` module still + exports the same name with the same shape. + + `FilterBuilderCondition` / `FilterGroup` (the other two names this batch was sized + with) are deliberately NOT converged: their shapes disagree on `id`, `value` and on + `operator`, and the only dependency-legal re-point would retype `operator` — the + vocabulary objectui#7561 is asking a maintainer to rule on. +- 9d86e1d: Retire the `timeScale` alias on the timeline node — `scale` is the only axis spelling + (objectui#6355, maintainer ruling 2026-08-27). + + **BREAKING for authored metadata.** `timeScale` was this renderer's pre-spec spelling of the + Gantt axis bucket. `scale` is canonical — it is `@objectstack/spec` `ui/TimelineConfig.json`'s + axis key and the key the renderer preferred (objectui#6170 ruling, 2026-08-25: `timeScale` + goes the alias-retirement route, not a silent second spelling). objectui#6355's ruling + retires it immediately, with no phased window, while the project is at startup stage. + + **What breaks, and how you will find out.** A timeline document that spells `timeScale` is + now **refused**, loudly, at the authoring boundary: + + - `TimelineSchema.timeScale` is declared `?: never` — writing it is a type error; + - the Zod twin declares `z.never().optional()` — parsing a document that carries the key + fails with `invalid_type` / `expected: never` on the `timeScale` path. + + The fix is a rename: `timeScale` → `scale`. The accepted values are unchanged (`hour`, + `day`, `week`, `month`, `quarter`, `year`), so no value needs rewriting. + + **Why a tombstone rather than deleting the key.** `BaseSchema` is `.passthrough()` on the + Zod side and carries `[key: string]: any` on the TS side, so an *undeclared* key is accepted + unvalidated by both halves. Deleting `timeScale` outright would have let the retired spelling + parse green and type-check green while the renderer no longer read it — the Gantt axis would + silently fall back to the `month` default, the chart would change bucket, and nothing would + error. That is the silent axis breakage objectui#2942 closed, running in the other direction, + and it is the specific outcome this retirement is shaped to prevent. Keeping the key declared + as `never` on both halves is what makes the removal audible. Absent stays valid on both, so a + document that never wrote the alias is untouched. + + Also in this change: + + - `resolveTimelineScale` drops the `?? schema.timeScale` fallback read; its parameter narrows + to `{ scale?: unknown }`. + - The designer drops its deprecated `timeScale` input. The `scale` input already offers all + six buckets. + - `ObjectTimeline` now emits the resolved axis under `scale` when it composes the schema it + hands to the renderer. It previously wrote the alias, which would have made **every** + object-bound Gantt fall through to the `month` default the moment the fallback read went — + silently, since that is a composed schema no author ever sees. Writing `scale` after the + spread also restores the precedence the surrounding code intends: a `timelineConfig.scale` + now actually beats a flat `schema.scale`, where under the alias the resolver's + `scale ?? timeScale` ordering let the flat key win. + - The two in-repo authors are migrated in the same change: the schema-catalog + `gantt-style-timeline.json` fixture and the registration's own `examples.gantt` block. + - Docs drop the `timeScale` row and gain a retirement callout; + `packages/components/.../TIMELINE.md`'s Gantt table now documents `scale` with the full + six-value vocabulary it has accepted since objectui#2942 (its row still claimed three). + + Version note: `minor`, not `major`, per AGENTS.md §版本号策略 — objectui's major tracks the + `@objectstack` major and all publishable packages share one `fixed` group, so a breaking + narrowing is declared `minor` with the break spelled out here. +- c71e14d: **The unresolvable-visibility-predicate report now names the roots of the tier the + predicate was actually evaluated against** (objectui#6487). An app-shell author + whose nav, area or field `visible` faulted was told to check `record` and + `page.<var>` — two roots that tier does not bind at all. + + `formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate` + (both exported from `@object-ui/react`) take a new **optional sixth argument**, a + `PredicateScopeTier` — also exported — selecting the closing advice paragraph. + Everything above that paragraph is unchanged on every surface, and so is every + verdict: this is diagnostics copy only. + + **The published signature grew; nothing existing breaks.** The argument defaults + to `'page-component'`, so a five-argument call keeps printing the bytes it + printed before. All three in-repo call sites pass their tier explicitly rather + than lean on that default. + + Each tier's root set was derived from the code that builds the bag, not from the + prose that described it: + + - **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item + predicates. Both bind `record`, `current_user` and `page.<var>` (the roots + `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is + byte-for-byte what it was. + - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` + runs, wired onto this reporter by objectui#6443. Its evaluator is built from + `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so + the line now names `current_user` with its three ADR-0068 alias spellings, + `app`, and `features` — the deployment-flag root that provider documents for + exactly this kind of predicate — and states outright that `record` and + `page.<var>` do not exist there. + + **Why not generalise the copy instead.** Dropping the concrete root names would + have made one paragraph true everywhere at the cost of making it useful nowhere: + an author who mistyped a root needs to know which roots exist *at their tier*, + which is the whole reason the paragraph is read. + + `data` is bound at the app-shell tier but is deliberately not advertised there — + every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it, + so naming it would point an author at a root that answers nothing. +- 29cb85b: `MenuItem` is now a discriminated union, and all three menu renderers read the keys it + declares (objectui#6523, objectui#6346, maintainer ruling 2026-08-27 — "one answer for the + whole `MenuItem` family"). + + **The break, spelled out.** `MenuItem` (`@object-ui/types`, shared by `ui:dropdown-menu`, + `ui:context-menu` and `ui:menubar`) used to be a single object with `label: string` + required unconditionally. It is now `MenuCommandItem | MenuDividerItem`: a command item + (`label` required, plus `icon`/`disabled`/`onClick`/`shortcut`/`children`) or a divider + (`{ separator: true }`, nothing else). The union — not `label?: string` — is deliberate: it + is what the data actually is, and it keeps the command arm's label protection intact rather + than weakening it repo-wide to accommodate the divider. Both arms also tombstone `type` + (`type?: never` / `z.never().optional()`): the retired `{ type: 'separator' }` (and its + sibling `{ type: 'label' }`) is now a **declared refusal** at parse time, not a silent strip. + A consumer's own `MenuItem[]` authored with either retired spelling now fails + `MenuItemSchema.safeParse` and fails `tsc` under the published `.d.ts`; a consumer authoring + the declared `{ separator: true }` divider now **succeeds** for the first time — before this + change it failed a strict parse too, because `label` had no way to be omitted. + + **Renderer accept behaviour changes to match.** `dropdown-menu` and `context-menu` used to + branch on the undeclared `item.type === 'separator'`; an author who instead wrote the + DECLARED `{ separator: true }` got a value that validated, published, and rendered a blank + menu row (the divider fell through to the ordinary item branch with no `label`). Both + renderers now branch on `item.separator`, matching `menubar` — which had this right all + along and is the evidence the type, not those two renderers, was correct. Their registry + `defaultProps` and `description` strings stop teaching the retired dialect; the 4 places it + appeared in this repo (2 schema-catalog fixtures, 2 registry `defaultProps`) are migrated. + + **The item handler moves to the declared key (objectui#6346).** All three renderers now + fire `item.onClick` — the key `MenuItem` has always declared (TS source, built `.d.ts`, and + the Zod mirror all agreed) but that `dropdown-menu`/`context-menu` never read (they read an + undeclared `item.onSelect` instead) and that `menubar` wired nowhere at all. An author who + followed the published type and set `onClick` got a value that validated, published, and + never fired; that is fixed. `renderMenuItems`/`renderContextMenuItems` also tighten from + `items: any[]` to `items: MenuItem[]` — the widening that let the mismatch type-check in the + first place. Migration cost measured **zero** in this repo: no fixture, doc or test authored + `onSelect` on a menu item before this change. + + **Rider, recorded as parity not new capability.** `menubar` now also renders the declared + `shortcut` string — `dropdown-menu` and `context-menu` already drew it, so this aligns the + third container rather than expanding the surface. + + Everything that rendered correctly before this change still renders the same way; the + narrowing only refuses spellings that were already unprotected (silently stripped or never + read at all). +- 9486ac6: `ui:breadcrumb` now reads the two declared keys it never referenced — `separator` and + `maxItems` (objectui#6646). + + `BreadcrumbSchema` has declared both since it shipped (`packages/types/src/navigation.ts`, + mirrored in `zod/navigation.zod.ts`), and `separator` is additionally advertised to authors + on the component's own documentation page. The renderer contained zero occurrences of + either name: it always emitted the bare `BreadcrumbSeparator` and it never collapsed. That + made `separator` the sharper of the two — an author who read the page, wrote + `"separator": "/"` and saw a chevron got feedback **identical** to having misspelled the + key, with nothing to tell the two apart. + + **Scored `minor`, not `patch`, on the separator default.** The sibling repair (PR #6644, + the same renderer's `icon` key) was a `patch` because it only started drawing something + where nothing had been drawn. This one also changes what an **unauthored** breadcrumb + renders: `separator` carries `@default '/'` in the declaration while the renderer fell + through to shadcn's `ChevronRight`, so declared default and actual render disagreed, and + honouring only the authored value would have left the docs lying about the unauthored one. + The render is aligned to the declaration (`schema.separator ?? '/'`) rather than the + declaration being rewritten to match the render — rewriting a published `@default` is a + contract change, which ADR-0049 routes to a maintainer, and this card's dispatched arm is + "implement the declaration". Every existing `ui:breadcrumb` therefore separates with `/` + instead of a chevron unless it authors otherwise. `''` is honoured as authored (no visible + separator), not promoted to the default — hence `??` and not `||`. + + `maxItems` bounds how many crumbs are **rendered**. When the trail is longer, the first + crumb and the last `maxItems - 1` survive with shadcn's `BreadcrumbEllipsis` between them, + so the current page — the crumb a trail exists to name — is never the one dropped; at + `maxItems: 1` there is no room for both ends and the current page is what stays. A value + that cannot mean a count (absent, non-finite, below `1`) is declined rather than coerced, + because silently inventing a truncated trail is worse than ignoring the key. + + Two catalog fixtures author the keys (`custom-separator`, `collapsed-trail`) and the docs + page gained a section for each, plus the `maxItems` row its interface block never carried. + + `packages/types` is untouched: both keys were already declared, and the only thing missing + was a renderer that read them. +- 7977ff9: Component deprecation is now DECLARED, not just warned about (objectui#6674). + + A deprecated component type used to be stated in exactly two places, neither of + which a gate, a test or a type can consult: a `console.warn` string literal + inside the renderer, and the word "(Deprecated)" inside a human-readable + `label`. Both gates that touch component types ask a different question — + whether the type RESOLVES — and a deprecated type resolves, which is how one + could be authored 85 times across 27 shipped exemplars with every check green. + + - `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the + `deprecated` key on the registration metadata, plus + `ComponentRegistry.deprecationFor(type, surface)` to read it back. The + declaration carries the SURFACES it applies to rather than being a boolean: + `div` and `span` are deprecated on the JSON authoring surface and are at the + same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would + be false for one of its two readers. + - `@object-ui/components` marks `div` and `span` with the declaration their + console notices already state. Nothing new is deprecated and no build starts + failing: the catalog ratchet keeps the existing stock frozen, and draining it + stays objectui#3965's worklist. +- b97790a: Seven more `find()` readers now read exactly what `QueryResult` declares — the + `records` arm is removed from each (objectui#6726, following objectui#5945). + + `QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` — + alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`. + `records` is not a member of it. It is the spelling the server envelope and the + client SDK use, which `ObjectStackAdapter.normalizeQueryResult` maps to `data` + before returning — a *below*-the-adapter spelling that had leaked into + above-the-adapter consumers. objectui#5945 removed it from two app-shell + readers; these are the seven the same producer sweep turned up and that card did + not name: + + | module | what it does | + | --- | --- | + | `components/src/hooks/related-count-store.ts` | related-list tab badge count | + | `components/src/renderers/basic/data-list.tsx` | `element:repeater` rows | + | `components/src/renderers/basic/elements.tsx` | `element:number` client-side aggregate | + | `components/src/renderers/basic/record-picker.tsx` | `element:record_picker` options | + | `plugin-detail/src/renderers/record-activity.tsx` | `record:activity` self-fetch | + | `plugin-detail/src/renderers/record-history.tsx` | `record:history` self-fetch | + | `plugin-view/src/ObjectView.tsx` | non-grid (kanban / calendar / gallery / timeline) fetch | + + **One of them was actively wrong, six were dead.** `related-count-store.ts` + read `records` *ahead of* `data` — the precedence inversion objectui#5945 was + filed about — so a `find()` answer carrying both would have been counted from + the key the contract does not declare. The other six read `data` first, so their + `records` arm could never be reached by a conforming producer. A dead tolerant + arm is not harmless: it is where a non-conforming producer keeps working + unrejected, and hardens into a second de-facto contract nobody is checking + (AGENTS.md #0.1). + + **What stops being accepted.** A `find()` answer shaped `{ records: [...] }` + now reads as **no rows** at these seams instead of silently resolving. Every + call site degrades rather than throws: the tab badge counts 0, the repeater and + the picker render their empty state, `element:number` reports 0, the activity + and history feeds render empty, and the non-grid views paint no rows. + + **Nothing produces that shape at this seam today**, which is why this is a + removal rather than a migration. Measured repo-wide over every tracked file: + `ObjectStackAdapter.normalizeQueryResult` CONSUMES the server/SDK `records` + envelope and returns `{ data, total, page, pageSize, hasMore }`; every other + `find()` implementation in the repo (`ApiDataSource`, `ValueDataSource`, the + runner and example mocks, the `@object-ui/types` REST example) returns `data` + or a bare array. The `records` producers that DO exist are on other seams and + are untouched: `ViewDataProvider`'s own `ResolvedData` interface, which declares + `records` legitimately; the raw Cloud HTTP payloads `marketplaceApi.ts` and + `packagedActions.ts` read; and the client-SDK doubles that sit *below* + `normalizeQueryResult`. + + **The bare-array arm is kept** wherever it existed, because it is live: fakes at + these seams answer with a plain array. Each module carries its own pin — + `*.contractEnvelope-6726.*` — asserting the contract read, the live arms, and + the refusal of `records`, so the live and the dead shapes cannot drift into each + other. + + `QueryResult` is **not** widened to bless `records`; that would be a + published-type change and a maintainer decision. +- c18d099: Read `find()` answers as `QueryResult` declares them on two more seams: the + related-count badge store no longer reads `count`, and `ObjectView`'s non-grid + unwrap no longer reads `value` (objectui#6840, following objectui#6726). + + `QueryResult` (`@object-ui/types`) declares exactly one rows member, `data`, and + exactly one count member, `total`. objectui#6726 removed the `records` arm from + seven consumers after measuring that nothing produces it at the + `DataSource.find()` seam, and deliberately left two arms reading *other* + undeclared keys standing in the same expressions — because it had measured + `records` and not them. Its own pin says so in as many words. This is the + measurement it deferred. + + - `related-count-store.ts` dropped `typeof res?.count === 'number' ? res.count`, + which was tried second and *ahead of the contract's `data`* — the same + precedence inversion objectui#5945/#6726 were filed about, on the key those + cards did not measure. The store already asks the server for the count with + `$count: true` and reads it back as `total`, which is a declared member. + - `ObjectView.tsx` dropped the ladder's last branch, + `Array.isArray((results as any).value)`. Unlike the store's arm this was a + pure fallback, not an inversion — `data` was already read first. + + Both keys are the raw-payload spellings that `ObjectStackAdapter.normalizeQueryResult` + and `ApiDataSource.normalizeQueryResult` already fold into `total` / `data` + *below* this seam, so nothing above it emits them. A producer sweep over every + `find()` definition body in the repo (452 bodies / 331 files, bracket-scanned so + a body cannot leak into sibling properties) found `count` emitted **0** times, + against controls `total` (85 hits / 75 files) and `data` (135 hits / 103 files) + drawn from the same cells. Narrowed to the 25 bodies reachable by `ObjectView`, + `value` is emitted **0** times against the same controls (6 and 6). + + No producer changes behaviour, because there is no producer; what changes is + that a non-conforming one is now refused instead of silently absorbed — which + is the point (AGENTS.md #0.1). Each module gets its own refusal pin + (`*.contractEnvelope-6840.*`), and the pins keep the live arms green alongside + the deleted ones, because live and dead is the whole distinction. + + Deliberately not done: `QueryResult` is **not** widened to bless `count` or + `value`. That is a published-type change and the maintainer's call, the same + floor objectui#6726 respected. + + The `value` reading here is **seam-local** and does not transfer: at + `extractRecords` (`@object-ui/core`, objectui#6839) the same key is still LIVE — + five test doubles in plugin-calendar / plugin-kanban emit it today. +- bf97b98: feat(types): declare `renderCellEditor` and schema-level `cellClassName` on `DataTableSchema` + + `data-table` has read both keys on its production path all along — `renderCellEditor` + through a `(schema as any)` cast, `cellClassName` by destructuring it into the class of + its three utility cells (the selection checkbox, the row number, the row actions). + Neither was declared, so authoring either one was unchecked: a misspelling produced no + error and no widget, and no editor completion offered them. + `DataTableSchema` now declares both, and the cast in `data-table.tsx` is gone rather + than replaced. + + What you can write after this change that you could not write before, exactly: + **nothing new runs.** Both keys had the same effect yesterday, because + `BaseSchema`'s `[key: string]: any` already admitted them at any type at all. What + changes is that they are now *checked* and *documented*: + + ```ts + const schema: DataTableSchema = { + type: 'data-table', + columns, data, + cellClassName: 'px-2 py-1 text-sm', // utility cells only (see below) + renderCellEditor: ({ column, value, commit, cancel }) => + column.type === 'select' + ? <MyPicker value={value} onSelect={commit} onDismiss={cancel} /> + : null, // null → fall through to the built-in editor + }; + ``` + + ⚠️ **One reject direction, deliberate.** Because the keys were previously absorbed by + the index signature as `any`, authored values of the *wrong shape* also compiled and + silently did nothing. They are now compile errors: + + - `cellClassName` is declared `string`, matching `BaseSchema.className` and + `TableColumn.cellClassName`. The renderer passes it through `cn()`, which would + also swallow `['a','b']` or `{ a: true }` — those spellings now fail to compile. + One authored spelling for a class slot is the contract. + - `renderCellEditor` is declared as the function `data-table` actually calls. A + non-function value (or a function with an incompatible context/return type) now + fails to compile instead of being ignored at runtime. + + ⚠️ **What the schema-level `cellClassName` actually styles.** It is NOT the + table-level twin of the per-column key: the two reach **disjoint** cells. Measured on + the render, the schema-level key is folded into the **utility** cells only — the + selection-checkbox cell, the row-number cell and the row-actions cell — while every + **data** cell folds `TableColumn.cellClassName` and nothing else. Row density is + therefore a pair of settings (`ObjectGrid` sets both), and the schema-level key alone + leaves data cells at the primitive's default `p-4`. The docblock, the zod `describe` + and `content/docs/components/complex/data-table.mdx` all say this now. + + No runtime behaviour changed anywhere, and nothing was retired. The zod mirror + (`@object-ui/types/zod`) gains both keys in the same stroke, so the validator accepts + what the published types now invite. +- 5ad86dd: **Breaking for authored metadata:** `TextSchema.value` is RETIRED (objectui#6951, + maintainer ruling A1 of 2026-09-04; objectui#7016; ADR-0049 enforce-or-remove). + A `text` node that authors `value` no longer validates: the parse fails loudly on + the `value` path with the explanation in the message, the TS member is a + `?: never` tombstone so the same document is refused at compile time, and the + renderer no longer reads the key. Write `content`. + + **What was measured, on this branch's base.** `TextSchema` declared two spellings + for its one content slot — `content` (read first) and `value` (the fallback limb + of `{schema.content || schema.value}` at `renderers/basic/text.tsx:162` and + `:167`) — both declared by objectui#6150, whose docblock called the pair "a + dialect, not a design" and deferred the choice. The ruling's premise, that + `value` is the minority spelling, was measured before any edit over the four + roots it named: **776 `content`-only `text` nodes, 25 `value`-only, 0 authoring + both** across `examples/` (674 / 13), `apps/` (59 / 0), the `examples/` + directories under `packages/` (0 / 1) and `content/docs/**` (43 / 11) — a + thirty-to-one majority for `content`, so the retirement went ahead as ruled. + (A further 14 `{ value, label, type: "text" }` objects in the filter-builder + catalog entries are field descriptors whose `type` is a field type, not `text` + nodes, and were excluded by kind.) + + **Who is affected — a `value` authored on a `text` node:** + + ```json + { "type": "text", + "value": "Hello" } // ← was tolerated (rendered as the fallback) + ``` + + now fails validation with: + + > RETIRED (objectui#6951) — `value` is no longer part of TextSchema; write + > `content`. It was a second spelling of the one content slot, read only as the + > fallback limb of `schema.content || schema.value`, and was retired under + > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling A1, + > 2026-09-04). The renderer reads `content` alone now, so an authored `value` + > would render nothing. Rename the key; the string is unchanged. + + **Two published faces, one retirement.** The TypeScript interface `TextSchema` + (`@object-ui/types`, `layout.ts`) declares `value?: never`; the Zod mirror + `TextSchema` (`@object-ui/types/zod`, `layout.zod.ts`) declares `value` as a + `retirementTombstone()`, so the key stays DECLARED and is refused BY NAME — + a plain deletion would have let an authored `value` ride `BaseSchema`'s + `.passthrough()` into a silent blank, which is worse than the tolerated + fallback it replaces. The `value?: string` members of `TextSpanSchema` and + `TabsSchema` in the same file are other schemas' contracts and are unchanged. + + **`@object-ui/components`** — the `text` renderer renders `{schema.content}` at + both arms (the `|| schema.value` limb is gone from each), and the `context-menu` + renderer's built-in fallback trigger node now spells `content`. Nothing else in + the package moves. **`@object-ui/plugin-dashboard`** — its three placeholder + `text` nodes ("chart type is not supported yet", "Custom widget — set + `component`…", the retired-widget notice) spell `content` so they keep rendering; + their wording is unchanged and still pinned. + + **Who is NOT affected.** A document that already wrote `content` is untouched; + `content`, `variant`, `align` and `className` are unchanged; `absent` stays + valid (`{ "type": "text" }` still parses). Every in-repo document that authored + `value` on a `text` node was rewritten to `content` in the same change: nine + `examples/schema-catalog` entries, `packages/types/examples/zod-validation-example.ts`, + eleven doc fences under `content/docs/`, and the `@object-ui/components`, + `@object-ui/react` and `@object-ui/types/zod` README samples; the catalog is now + pinned tree-wide against the retired spelling. + + **Migration:** rename `value` to `content` on every `text` node; the string is + unchanged. If a document authored both, `content` was already the value that + rendered — delete `value`. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spelling. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 16a725f: **Breaking for authored metadata:** `TreeViewSchema.data` is RETIRED (objectui#6951, + maintainer ruling B1 of 2026-09-04; ADR-0049 enforce-or-remove). A `tree-view` + node that authors `data` no longer validates: the parse fails loudly on the + `data` path with the explanation in the message, the TS member is a `?: never` + tombstone so the same document is refused at compile time, and the renderer no + longer reads the key. Write `nodes` — or bind the tree with `bind`, which is + unchanged and still read first. + + **What was measured, on this branch's base.** `TreeViewSchema` declared two + spellings for its one inline-nodes slot — `nodes` (read second) and `data` (read + third: `boundData || schema.nodes || schema.data || []` at + `renderers/data-display/tree-view.tsx:105`), both declared by objectui#6150. + `data` had been REQUIRED until objectui#6939 / PR #7533 made it optional, so + this retirement starts from a declared-and-optional member on both faces. The + in-repo corpus at the retirement: seven `tree-view` nodes under + `examples/schema-catalog` and `packages/types/examples` plus one `content/docs` + fence — six on `nodes`, two on `data` (`packages/types/examples/data-display-examples.json` + and `content/docs/api/schema-reference.md`), both rewritten; no package source + authored either spelling. + + **Who is affected — a `data` authored on a `tree-view` node:** + + ```json + { "type": "tree-view", + "data": [{ "id": "root", "label": "Project" }] } // ← was tolerated (read third) + ``` + + now fails validation with: + + > RETIRED (objectui#6951) — `data` is no longer part of TreeViewSchema; write + > `nodes` (or bind the tree with `bind`). It was the second spelling of the one + > inline-nodes slot, read only as the last limb of + > `boundData || schema.nodes || schema.data || []`, and was retired under + > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling B1, + > 2026-09-04). The renderer reads `bind` then `nodes` now, so an authored `data` + > would render an empty tree. Rename the key; the array is unchanged. + + **Two published faces, one retirement — and why a tombstone, not a deletion.** + The TypeScript interface `TreeViewSchema` (`@object-ui/types`, `data-display.ts`) + declares `data?: never`; the Zod mirror `TreeViewSchema` (`@object-ui/types/zod`, + `data-display.zod.ts`) declares `data` as a `retirementTombstone()`. `BaseSchema` + already declares `data?: any` (`z.any().optional()` on the mirror), so DELETING + the member would not have refused the key — it would have ADMITTED it, + unvalidated, through the base member, and the renderer would have drawn an empty + tree. The tombstone on the extended schema shadows the base member on both + faces; the pin measures the base accepting the very document the extended + schema refuses. + + **What the ruling kept, deliberately.** `nodes` stays OPTIONAL and no "at least + one of" presence rule was added: `{ "type": "tree-view", "bind": "treeNodes" }` + is a legal, rendering document (`bind` is the first source the renderer reads), + and a bare `{ "type": "tree-view" }` stays legal as PR #7533 left it. + `TreeNode.data` — the per-node payload on each tree node — is a different + member on a different schema and is untouched. + + **`@object-ui/components`** — the `tree-view` renderer's read is + `boundData || schema.nodes || []`; nothing else in the package moves. + + **Who is NOT affected.** A document that already wrote `nodes` (the four + `components-data-display-tree-view/*` catalog entries and the nested tree in + `components-complex-resizable/editor-interface.json`) is untouched; `title`, + `bind`, the selection / expansion keys and `className` are unchanged. The + catalog is now pinned tree-wide against the retired spelling. + + **Migration:** rename `data` to `nodes` on every `tree-view` node; the array is + unchanged. If a document authored both, `nodes` was already the value that + rendered — delete `data`. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spelling. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 6a449fc: A field its own `visibleWhen` hides is now cleared, so it stops carrying a stale value + to the server (objectui#6958). + + **Breaking, deliberately.** Until now a field the form renderer hid because the field's + own `visibleWhen` (or its deprecated view-level sibling `visibleOn`) resolved FALSE kept + its value in form state and submitted it anyway. Measured against a real running app: an + object with four mutually-exclusive party columns and a type column naming which one + applies could not be saved once a party column had been filled and then hidden — the + server refused the row, correctly, by naming a column that was no longer on screen to + clear. On the edit path the stored value was re-sent on **every** attempt, so such a + record could never be retyped through the UI at all. The perverse consequence: *not* + declaring `visibleWhen` produced an uglier but strictly **more usable** form, because the + offending column stayed visible and therefore clearable. + + **What changes.** When a field's own visibility verdict goes VISIBLE → HIDDEN and the + field holds a value, the renderer clears it: `null` for a scalar, `[]` for a multi-value. + The key is deliberately PRESENT and `null` rather than withheld — objectui#6848 measured + the write contract (`driver-memory` merges `{ ...stored, ...data }`, `driver-sql` issues + `SET` for the keys present), so an absent key means "leave it unchanged" and only an + explicit `null` overwrites the stored value. Omitting the key would have left the edit + path exactly as dead as before. + + **What deliberately does NOT change.** Only the field's own conditional-visibility + predicate clears. A field claimed by a hidden section (objectui#6236), a field on a hidden + tab (objectui#6237) and a statically `hidden: true` field all keep the ruled semantics — + visibility decides what is DRAWN and nothing else, and their values still submit. A broken + predicate still fails OPEN, so a typo cannot silently null a stored column. The clear is + transition-only and never writes over an empty value, so merely opening a record cannot + strip the stored values of columns the user never saw, and a create form still omits the + key for a field it never populated (objectui#4069). + + **Migration.** If you relied on `visibleWhen` to hide a field while still submitting its + value, that value is now cleared on the transition. Carry such a value on a statically + `hidden` field, or on a field claimed by a conditionally-hidden section, both of which are + unaffected. +- d1842ab: `DataEmptyState` now declares `role="status"` by default, so an empty result is + distinguishable from a failed one on every surface that renders it + (objectui#7132). + + This is the convergence half of the two rulings that landed as objectui#7063 and + objectui#7064, both resting on objectstack#13848: uniform behaviour belongs to + the platform, and per-surface compensation is the per-app tax being ruled + against. Those two fixed their own surfaces deliberately and locally; this card + measured whether the shared primitive should carry the property. It did not. + + **Measured, not assumed.** All the surfaces were rendered and their empty boxes + read directly: + + | surface | `role` before | + |---|---| + | `DataEmptyState` bare default | *none* | + | `plugin-list` empty list | *none* | + | `plugin-list` load-error panel | *none* | + | `plugin-detail` activity timelines | *none* | + | `ui:empty` schema renderer | *none* | + | `plugin-dashboard` `WidgetEmptyState` (#7063) | `status`, typed at the call site | + | `plugin-kanban` empty board | `status`, typed at the call site | + + The sibling states in the same file had always declared themselves — + `DataLoadingState` is `role="status"`, `DataErrorState` is `role="alert"` — and + the empty state alone declared nothing. So the surfaces were not legitimately + differing: the ones that wanted the property had each hand-typed the same line, + and the ones that had not yet done so were silently missing it. That is one + platform default, copied by hand, at package level. + + **It is a default, not a fixed attribute** — `role` is spread from props, so a + call site keeps the last word. That is what makes this inert for the two ruled + surfaces: both already pass `role="status"` explicitly and receive the identical + attribute with or without it. Neither surface's behaviour changes. + + **One real defect fell out of the measurement.** `plugin-list` renders its load + FAILURE through `DataEmptyState`, borrowing it for layout — so a 403 saying "You + don't have access" and a young object saying "Nothing here yet" were the same + node shape, with no role on either. That panel now declares `role="alert"`, + which both fixes the pre-existing indistinguishability and stops the new default + from announcing an outage as a routine status. + + Metric/KPI widgets are untouched: their carve-out (`rows.length === 0 && + !isMetric`) gates whether an empty state is rendered *at all*, upstream of this + component, so a KPI still reads `0` rather than "no data". +- 78ca238: `DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s + load-failure panel is now rendered by the error state instead of the empty state + (objectui#7143; maintainer ruling 2026-09-01, director decision batch #27). + + `ListView` rendered its load FAILURE through `DataEmptyState` — the component named for + the *empty* case — passing it a destructive icon, error copy and a retry action, while + `DataErrorState`, in the same file and with the same layout, had no consumer anywhere in + the repo. objectui#7132 closed the accessibility half of that collision (the panel now + declares `role="alert"` over the empty state's `role="status"` default) and deliberately + left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a + props-surface question plus a visual change rather than a rename. + + **`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored + from `DataEmptyState` in the same file rather than spelled a second way: + + - `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle` + glyph the component has always drawn. + - `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely. + - `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than + merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??` + against its own default and this does the same, against + `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive + square `DataErrorState` already drew. + + Same names, same types, same default semantics as the empty state's; nothing existing on + `DataErrorState` changed, and a call site that passes none of the three renders exactly + what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the + ruling pins three props, and this component's retry affordance is already spelled + `onRetry` / `retryLabel` (plus `children` for a call site that needs its own control). + + One non-prop addition rides along, called out rather than folded in: the icon wrapper now + carries `data-slot="data-error-state-icon"`, mirroring the empty state's + `data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no + name — untestable and unstylable — and migrating a call site off `DataEmptyState` would + DROP that identifier rather than rename it. + + **`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call + site passes the same custom icon through the new `icon` prop, the same + `iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the + error state's spelling of `description`); its retry `<Button>` moves from `action` to + `children`, which renders at the identical position. `role="alert"`, the + `data-testid="list-error-state"` hook and `data-error-kind` are untouched. The whole + rendered delta is two attributes: + + - the panel root's `data-slot` becomes `data-error-state` (was `data-empty-state`); + - the icon wrapper's becomes `data-error-state-icon` (was `data-empty-state-icon`). + + Both are renames, not removals. Nothing in this repo styles or selects on either — no CSS + rule and no test read them — so a stylesheet in a host app targeting + `[data-slot="data-empty-state"]` to reach *this* panel is the only way to notice, and it + should be reading `data-error-state` now. Every class on every node, and the glyphs + themselves, are byte-identical: this is a visual no-op, deliberately, so the review the + ruling asks for has a small thing to look at rather than a redesign. +- b652514: Mixed id/object action arrays are refused; use all ids or all objects + (objectui#7182, maintainer ruling 2026-09-02, option C). + + An `actions` array on `page:header` or `record:quick_actions` (and the bar's + spec-declared `actionNames`) is either **all action ids** or **all inline + `ActionDef` objects**. A mixed `['convert', { … }]` array is now refused on both + surfaces: none of its authored actions is rendered, and the console names the + offending index (`… refused at index 1 — element 1 is an inline action object + but element 0 is an action id …`). Before this change the two renderers + disagreed on exactly that input — `page:header` normalised per element and drew + both halves, `record:quick_actions` switched on the whole array and rendered + nothing for the id — so one authored array meant two things depending on which + surface drew it, and the mixed form is precisely what a half-migrated page under + the objectstack#11592 ids ruling produces. + + **Breaking, deliberately, and narrowing.** `@objectstack/spec` has always + declared `PageHeaderProps.actions` as `z.array(z.string())` — the spec already + refuses an object element at validation, naming its index — and + `RecordQuickActionsProps` declares `actionNames` (ids) only. What narrows is + the renderers' undeclared tolerance: an all-object array still passes through + (transition tolerance for the migration, retired on its own card once the last + inline array is converted), a mixed one no longer does. **Migration:** convert + each array whole — every element an id naming an action declared on the + object — never one element at a time. + + New on `@object-ui/types`, beside `actionRendersAt`: the pure + `resolveDeclaredActionIds(elements, registeredActions)`, with the + `DeclaredActionsResolution` / `DeclaredActionsRefusal` result types (the shape + classifier stays module-internal: called with no registry, the function already + returns the registry-independent verdict a renderer needs before its lookup). Both + renderers call it; the whole-array switch in `record-quick-actions.tsx` and the + per-element normalisation in `containers.tsx` are gone. The rule is closed: a + string is an id, a non-null non-array object is an inline definition, and any + other element (`null`, a number, a nested array) is refused at its index too. + An all-id array resolves by `name` in authored order, first registration + winning on a duplicate name; ids that name nothing are reported back with their + index for the caller to warn about once its lookup has settled. + + **Three further behaviour changes ride on the one rule, all on published + packages:** on `page:header`, a padded id (`' convert '`) no longer resolves — it + was previously trimmed before the lookup, and ids are now compared exactly as + authored; on `page:header`, a blank `''` id is now reported by the unresolved-id + warning instead of being silently skipped; on `record:quick_actions`, an all-id + `actions` array with no object bound now renders nothing (the ordinary empty + placeholder) instead of handing the bare strings to the action engine as action + definitions. +- adbda1b: feat(data-table): pass `pendingRow` to the host cell editor — the row with its staged edits merged over it (#7188) + + `data-table` already computed each row's `pendingChanges` entry in the row loop that + renders the editor; it now hands the injected editor that row merged with those staged + values as `pendingRow`, next to the persisted `row`. Nothing about `row`, `value`, + `stage`, `commit` or `cancel` changed. A host editor that scopes itself by a sibling + field can read `pendingRow` and follow an edit the user has made but not yet saved. +- 7bf244b: BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`, + `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, + `ChartStyle` and the `ChartConfig` type — are removed. `@object-ui/plugin-charts` + is the single implementation (objectui#7397, maintainer ruling 2026-09-04). + + **Migration: import the chart primitives from `@object-ui/plugin-charts`.** + + (The bump is `minor` by this repo's release model — objectui's major is pinned to + the `@objectstack` family major, and its own breaking changes ship as `minor` with + the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This + paragraph is that spelling-out: the break below is real and consumer-visible.) + + - **What breaks, by specifier**: `import { ChartContainer, ChartTooltip, + ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } from + '@object-ui/components'` and `import type { ChartConfig } from + '@object-ui/components'` no longer resolve — TS2305 at build time, `undefined` at + runtime. They were reachable through two `export *` hops (`src/index.ts` → + `./ui` → `./chart`), so this is a real removal from the published surface, not a + tidy-up of dead code. + - **Not affected**: `ChartSkeleton` — the chart-area loading placeholder in + `src/custom/view-skeleton.tsx` — is a different symbol and stays exported. + `@objectstack/spec/ui` still owns the authored-chart `ChartConfig`; only the + per-series style map published from this package is gone. `@object-ui/plugin-charts` + calls its own map `ChartContainerConfig`, so the two names no longer collide. + - **Why the copy had to go rather than be fixed in place**: it duplicated + `packages/plugin-charts/src/ChartContainerImpl.tsx` and carried the + label-resolution hole objectui#7248 had already fixed there. `ChartLegendContent` + resolves a label as `config[nameKey || item.dataKey || 'value']` while rendering + the colour swatch unconditionally, so a legend entry whose config lookup misses + paints an anonymous coloured dot — on a scatter that reads as a data point drawn + outside the plot area, which is exactly how objectui#7248 was reported. Two copies + of one primitive is how a fixed bug returns; consumers importing from + `@object-ui/components` were getting the unfixed one. + - **Why not re-export the plugin's copy from here instead**: `@object-ui/plugin-charts` + depends on `@object-ui/components` (`workspace:*`), so the dependency direction + forbids it. + - **Consumer census**: zero in-repo importers, measured with a lit control — no file + under `apps/**`, `examples/**` or `packages/**` imported any of these names from + `@object-ui/components`. `plugin-charts` reaches its own copy by relative path. + The `hotcrm` and `cloud` repositories could not be read from the seat that made + this change (HTTP 403), so no claim is made about them. + + `packages/components/shadcn-components.json` records `chart` under + `customComponents` with `movedToPlugin: "@object-ui/plugin-charts"`, which is what + keeps `pnpm shadcn:update-all` from re-fetching the primitive and silently undoing + this. +- f0bb9fa: **Renamed:** the declared input carrying the action's execution type on `action:button` and + `action:icon` is now `actionType`. It used to be `type`, which collides with the SDUI + envelope's component discriminator. **No alias and no transition window** — a declaration + still spelling it `type` sets the discriminator, not the input. + + Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4, + verbatim 「同意」). The objectui half; the pinned `sdui.manifest.json` in objectstack follows + as a separate change. + + **Why the old name could not stay.** `action:button` and `action:icon` were the only two of + the manifest's 57 components declaring an input named `type`, and on no tier could an author + actually set it: + + - **html tier** — `parse.ts` composes a node as `{ type: tag, ...props }`, props spread + last, so `type="api"` replaced the component discriminator and the node stopped resolving + to a component at all. `validate.ts` cannot report that: `type` is in `BASE_PROPS`, so it + is skipped before the declared-input check runs. Two mechanisms, one outcome, no + diagnostic. objectstack PR #14274 landed a refusal on this tier whose prescription + ("write the tag you meant") is wrong for exactly these two components. + - **react-page tier** — the wrapper stamps `type: tag` last and parks the author's value + under `specType` (objectui#2880), which neither action renderer reads. + - **JSON / `SchemaRenderer`** — the node's `type` is the component id, and the renderer + forwarded it to `ActionRunner` as the action type. `'action:button'` binds no handler and + no builtin: the click did nothing, with no error and no toast (the objectui#6306 shape). + + `actionType` is not a new vocabulary. It is the spelling this repo already used for this + exact value: `action:bar` renames the declared type as it spreads a member onto its child + (`type: componentType, actionType: action.type`), `ActionRunner.execute` resolves + `action.type || action.actionType || action.name`, and both renderers already read + `schema.actionType` FIRST. The rename makes the one working spelling the declared one. + It follows the resolution `page:tabs.type` got upstream for the same carrier collision + (retired in favour of `tabStyle`, objectstack#6776) rather than inventing a new convention. + + **Census of the authored corpus, measured before the rename, not assumed.** Across 4,797 + files in `examples/`, `apps/console/`, `content/docs/` and `packages/`: **zero** authored + nodes set the input. 517 JSON files (433 of them the schema catalog) contain 2,410 + `type`-bearing nodes and **no** `action:button` / `action:icon` node at all — control: 127 + plain `button` nodes on the same walk. 207 parsed fenced JSON blocks in md/mdx carry **5** + `action:button` nodes, all of them `{ type, label, icon?, action }` — the discriminator plus + the `action` channel, none setting an execution type — control: 28 plain `button` nodes, and + the count independently matches the corpus census already recorded in `SchemaRenderer.tsx` + ("`action:button` (5 nodes)"). No docs page documents the input; no catalog entry uses the + components. So the rename breaks no authored document in this repo. + + **What changes for a consumer.** The renderers no longer fall back to `schema.type` when + `actionType` is absent — that fallback is the old spelling, and the ruling forbids an alias. + A node that declares neither now forwards `type: undefined`, and `ActionRunner` falls + through to its own `action.name` leg instead of being handed a component id. Declarations + composed by `action:bar`, `action:group` and `action:menu` are unaffected: they carry the + spec `ActionSchema.type` inside an `actions` array, which is a different surface and is + unchanged. + + Pinned by `action-type-input-html-tier.test.tsx` (the html tier authors the renamed input + end to end; the manifest built from the live registry accepts `actionType` and reports a + bogus prop as the control) and by the rewritten standalone rows in + `action-bar-member-type-resolution.test.tsx`, one of which now fails if the `|| schema.type` + leg is ever added back. +- daf9d57: The last two consumer-side reads of `primaryField` off an OBJECT def are gone (objectui#7586). + + `primaryField` is a `DetailViewSchema` key (`@object-ui/types` `views.ts`) — a **view** key, + which `DetailView.resolveDisplayTitle` reads off `schema` and is welcome to. Read off an + **object** def it is undeclared: `@objectstack/spec`'s object schema is a `strictObject` + answering `unrecognized_keys: ['primaryField']`, and `ObjectSchema.create()` throws. + `primaryField` appears in **zero** files of the shipped `@objectstack/spec@17.2.0` dist, + against 68 for the canonical `nameField`. objectstack#6326 removed the identical read from + two lint rules; objectui#7287 / PR #7585 removed it from `resolveTitleField`. These two + survived it — and three of this repo's own changelogs already called the probe *"not a spec + property — always undefined"* while the code kept honouring it. + + **They are two different repairs, not one patch applied twice.** + + `@object-ui/components` — `PageHeaderRenderer`'s record-chip chain ranked + `objectSchema.primaryField` directly under `schema.title` and **above** the unified ADR-0079 + resolver, on the surface that renders the **actual H1** of a synthesized record page. The + rung is deleted, so the heading now comes from `titleFormat` → the ADR-0079 resolver + (`nameField` → `displayNameField` → type-aware derivation) → the record-key walk, the same + precedence `DetailView`'s own header uses. + + `@object-ui/plugin-detail` — `record:details`' dedupe ladder decides **which row the body + grid hides**, so that the field already shown as the H1 is not repeated underneath it. That + is a different question from "what is the title", and `primaryField` was its *first* + candidate. The rung is deleted; the ladder is the literal display-name walk that mirrors the + tail of the header chip's chain. Its docstring, which had described the chip as resolving + "from objectSchema.primaryField", went stale when PR #7585 landed and now describes the chip + as it is. + + **User-visible, deliberately.** A payload that carries the off-spec key anyway changes in two + ways: the H1 of its record page stops being `primaryField`'s value, and a different row + survives the detail grid. Nothing spec-legal can reach either path. + + `DetailViewSchema.primaryField` is untouched, and so is `ObjectDefLike.primaryField` in + `buildDefaultPageSchema` — a deliberate declaration (not a read) that keeps an external + caller's object literal type-checking. +- c15d7ec: One home for the `date` display convention in `data-table` (objectui#7620). + + `data-table`'s fallback cell (`formatCellValue`) sniffs ISO strings and + formats them. Its date-only branch built its own `Intl.DateTimeFormat` bag — + `{ year: 'numeric', month: 'short', day: 'numeric' }` — while the shared + `formatDate` drops the year inside the CURRENT year on purpose (the year + rarely helps on an in-progress record and crowds the cell). So one table + rendered two faces for the same value depending on which path a cell took: + the `date` field cell showed `Jul 4` and the fallback cell showed + `Jul 4, 2026`. The branch now calls `formatDate` (default style). + + **Visible change**: in every `data-table`, a current-year date-only value in a + column that renders through the fallback cell loses its year — `Jul 4, 2026` + becomes `Jul 4` in `en-US` — and now matches the `date` field cell beside it. + Past- and future-year dates are byte-identical (`Jul 4, 2024`), which is why + the split was easy to miss: the two faces only ever diverged on the dates + users look at most. The datetime branch, the non-date passthrough and every + cell with its own renderer are untouched. + + A column that genuinely wants the year on every row is an explicit `format` + style honoured by both paths, not a second option bag — the objectui#7443 / + objectui#4576 lesson, one type over. +- c14d3a0: Honour `options[].disabled` on a `combobox` node (objectui#7687). + + **User-visible behaviour change, deliberately — hence `minor`, not `patch`.** The + member was already declared by `@object-ui/types` (`ComboboxOption.disabled`) and + already validated by the zod mirror (`ComboboxOptionSchema`, pinned as `boolean` + on both faces by the objectui#7087 twin-symmetry ruling), but the component never + read it: `Combobox` mapped each option to a `CommandItem` carrying `key`, `value` + and `onSelect` only. So an option authored `{ value, label, disabled: true }` + passed `safeValidateSchema`, type-checked against the published `ComboboxSchema`, + and then rendered as an ordinary, fully selectable option — a declared key with no + read site behind it, the class the enforce-or-remove ledgers exist to close. + + An author who already writes `disabled: true` today gets a different combobox + after this change: that option now renders dimmed and can no longer be chosen, by + click or by keyboard. That is the intended repair — declared and validated should + mean enforced — but it is a change in what existing metadata does, not a silent + internal fix, so it is priced as a behaviour change rather than a patch. + + The alternative remedy, retiring `disabled` from `ComboboxOption` and the zod + mirror, was weighed and **not** adopted: it narrows a published surface and would + require the objectui#7087 twin-symmetry pin to be changed, where honouring the key + restores declared = enforced at the cost of one prop. The spelling follows the + sibling select renderer, which already sets `disabled={opt.disabled}` on its + `SelectItem`. + + Nothing else moves. The whole-control `disabled` prop (the one forwarded to the + trigger button) is untouched, no key is added to `@object-ui/types`, and no new + key is introduced — this release only starts reading one that was already + published. + + `@object-ui/types` is deliberately **not** given its own bump. The one file that + changes there is a test, `component-docs-disabled-inherited-7239.test.ts`: its + census over `content/docs/components` counts every documented `disabled?:` row, + and documenting the member adds a legitimate row that the ledger now claims as + INDEPENDENT (the shipped `ComboboxOption` declares `disabled` itself and does not + extend `BaseSchema`, so the narrow `boolean` spelling is correct for it). No + shipped type or value moves in that package, so there is no behaviour there to + version. +- cf1d29e: `ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one + (objectui#8067). + + A registration's `type: 'array'` said a value was a list and stopped there, so a member + that drifted from `@objectstack/spec` was invisible to every layer that reads a + declaration. `page:header.actions` is the measured cost: the contract declares + `z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` + objects, and the repo-wide parity gate in + `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the + whole life of the drift because both sides carried the key and neither could say what + was inside it. What settled it was a maintainer ruling, not a test — and even after the + fix, "these are ids" survived only as English in the registration's `description`. + + **What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind + vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an + `object` used as a map. One kind, or an array of them for a member contract that is a + union, with `type`'s semantics — a member passes when any declared arm accepts it. The + manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per + input instead of six. + + **Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 + is the precedent: five `ComponentInput` keys declared and read by nothing). The + repo-wide parity gate compares every declared `of` against the member kind + `ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; + `sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new + `member-type-mismatch` diagnostic naming the offending positions; and the generated + `sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is + `string[]` where it used to be `unknown[]`. + + **Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: + every container key's member position was probed with one value of each coarse kind and + a declaration written only where exactly one kind was accepted. A member contract that + admits several kinds — `record:highlights.fields` takes a field name or an inline field + object — is deliberately left undeclared and pinned with its reason, because picking one + arm there is a narrowing this repo leaves un-gated and picking all of them would + advertise shapes only a per-block pin can vouch for. + + **The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer + ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` + is the publication face's expression ceiling, and spec is the sole judge of values — + stands exactly as written. `of: 'object'` says the members are objects; which keys they + carry is still `description`'s job and `os validate`'s. + + **Nothing published before this changes.** An input that declares no `of` validates, + serializes and types byte-identically: `validateTree` checks no member, the serializer + emits no key, and the codegen emits the same `unknown[]`. +- dd19463: `stack` now reads its spacing from `gap` and nothing else — the undeclared + `spacing` key it also accepted is gone (objectui#4890). + + `StackSchema extends Omit<FlexSchema, 'type'>`, whose spacing key is `gap`. + `spacing` was declared by nothing: not the TypeScript interface, not the zod + mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway, + as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole + story, since it existed to get past the type system saying the key was not there. + A lenient consumer leg does not stay in the consumer: it becomes a second + de-facto contract that producers write to, and 135 nodes across 39 files of the + shipped schema catalog did exactly that. Every one of them rendered correctly, so + nothing ever pointed at it, while the examples went on teaching the key to every + author who copied them. + + The trap it was one edit away from springing: `flex` — semantically a `stack` + with a `direction` — never read `spacing`, so re-typing any of those nodes would + have dropped the spacing to the default silently. Fixed at the producer + (AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the + alias is deleted rather than legalised into `StackSchema`, where it would only + have been a second name for `gap`. + + **If you author `spacing` on a `stack`**, rename it to `gap`; the value and the + rendering are unchanged. A `stack` still carrying `spacing` now renders the + default gap, exactly as a `flex` always did. + + Also in the same sweep, and visible only in the published example catalog rather + than in any package API: 140 catalog nodes that were already `flex` / `stack` / + `container` stopped hand-writing their own declared props in `className` + (`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`, + `flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all + (objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in + `className`, because the props are not responsive. Both facts are ratcheted in + `examples/schema-catalog/test/layout-props-conversion.test.tsx`. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- aefe428: **Behaviour change.** Form section and field `visibleWhen` predicates that silently failed + OPEN now actually evaluate — and a rule that resolves FALSE now hides the field + (objectui#6010). + + `current_user` was bound on two of the three `visibleWhen` surfaces and not the third. A + page component or app/nav gate got it (`ExpressionProvider` → `SchemaRenderer`), and a + per-option gate got it (`resolveCascadingOptions(…, predicateScope)`), but every + `resolveFieldRuleState` call in the form renderer passed `undefined` for the scope + argument, so a form SECTION or FIELD predicate saw `record` and `previous` and nothing + else. `'sales_manager' in current_user.positions` therefore named an **unbound root** + there, and the visibility fallback is fail-open — so the gate did not hide the field from + the people it named, it **showed the field to everyone**, with no signal beyond one + deduped `console.warn`. + + **What changes for you, in the direction that matters:** if you authored a form-field or + form-section `visibleWhen` naming `current_user`, saw the field render, and concluded the + rule was permissive — it was not permissive, it was broken, and it is now enforced. That + same field will now **hide** for every user the predicate resolves FALSE for. Audit any + `visibleWhen` on a form field or section that references `current_user` / `user` / + `ctx.user` / `os.user` before upgrading; a predicate that was quietly inert becomes live. + + Two things deliberately do **not** change: + + - **A genuinely unbound root still fails open.** A predicate the engine cannot evaluate at + all still logs one warning and leaves the element visible. Only *evaluated-and-false* + hides. `visibleWhen` remains presentation, not access control — use field-level security + or RLS to stop someone reading something. + - **The deprecated `visibleOn` alias** on a form field now binds the same scope, because + ADR-0089 D2 folds it into `visibleWhen` at parse; binding one scope for the canonical + spelling and another for its alias would have reproduced the same defect one spelling + over. The synthesised legacy `condition: { field, equals }` predicate is unaffected — it + is generated from a structured object and can only ever name `record.<field>`. + + This restores the contract both ADRs already declared: ADR-0068 D1 — *"a predicate authored + against any one form evaluates identically"* — and ADR-0089 D1 — *"runtime record surfaces + bind `record` + `current_user`"*. All five surfaces are now pinned against one authored + predicate text in + `packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx`, + so the next divergence is loud instead of silent. +- 3ccd9e8: Split the static `table` column type off the rich shared `TableColumn` + (objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = + enforced holds per renderer. + + `TableColumn` is unchanged and remains the rich shape `data-table`, + `CRUDSchema` and detail-view relations honour. The static `table` renderer's + `TableSchema.columns` now declares the new narrow `StaticTableColumn` + (`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the + keys that renderer reads). The eleven keys the static renderer never read are + retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on + `TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / + `filterable` / `resizable` / `editable` / `cell` on its columns. + + Breaking for authored metadata that wrote those keys on a `type: 'table'` + node: they were silently inert before and are now refused loudly — a tsc + error on the interface (`?: never`) and a parse rejection naming the key in + `@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: + nodes that wanted the interactive behaviour move to `type: 'data-table'` + (whose columns keep the rich `TableColumn`); right-aligned columns on the + static table use `cellClassName: 'text-right'`; alternate-row styling uses + Tailwind on `className`. +- 72ffc34: Retire `ActionParamDialog`: the `custom` barrel's second action-param dialog is + removed, and the app-shell dialog is recorded as the surviving implementation + (objectui#5685, maintainer ruling of 2026-08-22). + + **Breaking for any out-of-repo host that imported it** (declared `minor` per the + repo's version-alignment rule — the major tracks `@objectstack`, never an + API-break count): `@object-ui/components` no longer exports `ActionParamDialog` + or `ActionParamDialogProps`. Measured at the branch point, the export had zero + production consumers — its only in-repo importers were its own five test files, + which retire with it, and no other repository in the organization imports the + symbol from this package. + + This file was the repo's SECOND implementation of the action-param surface, and + its audit trail is the reason it retires instead of being maintained: the last + close look (objectui#4758) found per-option `visibleWhen` not evaluated at all, + and the five hardcoded English strings this card originally recorded were the + next drift installment. A dormant second dialect of a governed surface is one + production import away from being live; removing it removes the whole drift + class. + + FROM → TO for an out-of-repo host: + + - `import { ActionParamDialog } from '@object-ui/components'` — no drop-in + replacement is published. The surviving implementation is + `@object-ui/app-shell`'s `ActionParamDialog` (`src/views/ActionParamDialog.tsx`), + rendered by app-shell's action runtime (`useConsoleActionRuntime`, + `RecordDetailView`) rather than exported standalone. A host that needs its own + param form builds on `@object-ui/fields`' shared field widgets + (`resolveFormWidgetType` / `getLazyFieldWidget`, ADR-0059) — the same seam the + surviving dialog renders through. + + The unreleased objectui#4758 changeset for this component (`select` options + through the shared option evaluator) is withdrawn in the same change: the + component retires before that fix ever ships, so the release notes carry the + removal rather than new behaviour of a surface this release does not contain. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 490f482: The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling. + + `renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a + cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared + on `TableColumn`, which declares `header` and `accessorKey` — both required + (`packages/types/src/data-display.ts`). This was the fourth site of the + column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid` + (objectui#5350). + + Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the + family direction — *retire the consumer-side alias; unify the producers* — and it + names this site: the declared `header`/`accessorKey` contract wins. + + What makes this site different from its three siblings is that the alias was not + merely tolerated, it was **published**. `content/docs/api/schema-reference.md` + §TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a + property row reading *"Column definitions with `name`, `label`, …"*, while + `packages/types` declared the opposite pair. Docs and type disagreed about one + type they both call `TableColumn`, each internally consistent. Retiring the alias + without correcting the page would have turned a documented, working example into a + silently broken one, so both halves land together: the page now authors + `accessorKey`/`header`. The same row also advertised a `render` property that + `TableColumn` has never declared — the renderer's hook is `cell` — and that claim + is dropped rather than re-spelled. + + The failure mode of a now-unresolvable column is worth stating, because it is + quiet: the column keeps its slot and its neighbours are unaffected, the header or + the cells simply render empty, and nothing throws. This renderer keys its cells by + index rather than by accessor, so unlike `data-table` it does not even produce + React's generic missing-key warning — a retired-spelling column is fully silent. + Whether that silence should become an authoring diagnostic is objectui#5349's + question; no diagnostic is added here. + + The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so + re-introducing an undeclared alias on this renderer is now a type error rather + than a reviewer's catch. + +### Patch Changes + +- d88e20f: Removed the block schema family (objectui#4895, ADR-0049 enforce-or-remove, maintainer + ruling 2026-09-02 — option C1, retire the family in one change, no transition window). + + **Breaking on a published surface, deliberately.** These names are gone from + `@object-ui/types`, from both subpaths that carried them: + + - `.` (types): `BlockSchema`, `BlockSlot`, `BlockLibrarySchema`, `BlockEditorSchema`, + `BlockInstanceSchema`, plus the support types with no other reader — `BlockVariable`, + `BlockMetadata`, `BlockLibraryItem` — and `ComponentSchema`. + - `./zod` (runtime validators): `BlockVariableSchema`, `BlockSlotSchema`, + `BlockMetadataSchema`, `BlockSchema`, `BlockLibraryItemSchema`, `BlockLibrarySchema`, + `BlockEditorSchema`, `BlockInstanceSchema`, `ComponentSchema`, and the + `BlockComponentSchema` union over them — which was also `AnyComponentSchema`'s block arm. + + The zod half is the one that mattered. `AnyComponentSchema.safeParse({ type: + 'block-library' })` returned **success** on 17.6.0 for a node no page can render, so an + author who copied the documented shape was told green by the shipped validator and then got + the registry's `OBJUI-001` "Unknown component type" panel. Validated-then-broken is worse + than never-validated, because the green light is what the author trusted. All five + discriminants — `block`, `block-library`, `block-editor`, `block-instance`, `component` — + are now **refused**, pinned in `phase2-schemas.test.ts` alongside the theme refusals + retired the same way. + + Evidence the family was declared-but-unenforced: zero `ComponentRegistry.register(...)` + sites claimed any of the five keys (positive control `'table'` resolves to two), zero + renderers, and zero readers outside `packages/types/src`. The liveness pass this card's + earlier deferral was keyed to (objectui#6935) established that external consumption of this + package is structurally unmeasurable — the certainly-live control `TableSchema` returns the + same zero external consumers — so the ruling was taken on the evidence in hand rather than + on a deferral whose exit cannot fire. + + ⚠️ **Not this family, and not touched.** The live slotted record-page vocabulary — + `PageNodeSchema.kind === 'slotted'` with `slots?: PageSlotMap` (`packages/types/src/layout.ts`), + rendered by `usePageAssignment` / `PageBlockCanvas` / `PageBlockInspector` in + `@object-ui/app-shell` — shares the words "block" and "slot" with the retired family and + shares no declaration, type or file with it. Neither is the `type: 'component'` NAVIGATION + item kind (`{ type: 'component', componentRef }`, `NavigationItemSchema` in + `zod/app.zod.ts`), a different declaration in a different module. + + `@object-ui/components` carries one forced consequence: `renderers/feedback/empty.tsx` + annotated its `action` child as the retired `ComponentSchema` and now says `SchemaNode`, + the node type `SchemaRenderer` actually takes. + + `packages/types/src/blocks.ts` and `packages/types/src/zod/blocks.zod.ts` are kept as + ADR-0049 tombstones exporting nothing, and `block-family-retired-4895.test.ts` pins every + retired name out of them. `content/docs/blocks/block-schema.mdx` is deleted with the family, + and objectui#7023 — the narrower validator-only fix — dissolves into this retirement. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- e304a4e: `ui:icon` and `ui:spinner` route their host spread through `toDomProps` + (objectui#5632, the `BARE_SPREAD_ON_SVG` slice of objectui#5574). + + Both renderers forwarded their whole prop bag to the SVG they render, so every + authored SDUI key on the node became an attribute — 14 per target, and + `icon="check"` on all 71 icon nodes in the schema catalog. `IconSchema` and + `SpinnerSchema` declare only `icon` / `size` / `color`, and both renderers + already consume all three by name, so the SDUI pass-through list withholds + nothing they need. + + Two user-visible behaviours change, both of which were invisible to the DOM-leak + gate because the judge counts `stroke` / `width` / `height` as legitimate on an + SVG host: + + - **`ui:spinner` now spins.** Its computed `class` (`animate-spin` plus the size + class) was being overwritten by the `className` carried in the spread, so a + spinner rendered through `SchemaRenderer` had neither. It is merged now. + - **A sized `ui:spinner` no longer emits invalid dimensions.** `size` is an enum + (`sm`/`md`/`lg`/`xl`) and the spread handed the string to lucide's numeric + `size` prop, putting `width="lg" height="lg"` on the element. + + Also: an `icon` node's `color` is a Tailwind class (as `IconSchema.color` + declares, and as every authored value in the catalog uses). It reached lucide's + `color` prop through the spread as well, emitting an invalid + `stroke="text-red-500"` beside the class that does the real work; only the class + path remains. An authored raw CSS colour (e.g. `color: "red"`) no longer tints + the glyph through that accident — declare the colour as a class, which is the + declared contract. +- fc62bb4: `TableColumn.type` now has ONE canonical value set across all three ends that disagreed + (objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is + canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | + 'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted + anything; the renderer branched on a third set and could only read the key through an + `as any` cast. + + ## ⚠️ Accept-set narrowing — these spellings stop validating + + `TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now + `z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse + time with `type` named in the error path. Spellings that validated before and are **refused + now**, grouped by why they were being written: + + - **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, + `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is + the card's headline case: it validated, matched no renderer branch, and the column fell + through to plain text rendering with nothing reported. That silent fall-through is the + lenient-validation face that lets AI-authored metadata errors through, and it is now a + loud parse failure. + - **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, + `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of + `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, + not on the column: a column gets its dedicated widget from the field definition behind its + `accessorKey`, never from `type`. + + **Authored metadata in this repo needs no migration.** Measured before tightening, across + `examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files + plus the docs and playground sources): **zero** authored `TableColumn.type` values outside + the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column + position anywhere in the repository. If you author `type` on a table column, check it + against the eight; if the value describes the FIELD rather than the column, remove it. + + ## The renderer's undeclared vocabulary disappears instead of being declared + + `int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` + and `datetime-local` had its own editor branch, none of them declared. They arrived because + column-inference producers forwarded an object schema's field type **verbatim** into + `TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred + value onto the declared vocabulary at their emit seam via the new + `normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, + `datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the + column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) + and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote + the raw field type into the same slot. + + Dropping the annotation is behaviour-preserving at the only consumer that reads the key. + `data-table`'s inline editor branches on `date`, `datetime` and the numeric set and + otherwise falls through to a text input — which is exactly the `undefined` path. The + dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, + which resolves the field through `column.accessorKey` and never reads `type`. + + ## New public API + + `@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single + declaration the zod mirror builds its enum from, so the two cannot drift), the + `TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast + in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared + spelling is a tsc error rather than a silent widening. + + A value-level parity pin covers all three ends + (`packages/types/src/__tests__/table-column-type-canonical.test.ts` and + `packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). + objectui#5684's guard is key-set only and cannot see value drift — `type` was present on + both sides the whole time — which is how this instance survived while its siblings were + caught. A future inference value turning that pin red is by design; the note at the pin says + so, and names the two correct repairs. +- 2c3cd1b: BREAKING (`@object-ui/core`): `ActionRunner`'s legacy `ActionDef.onSuccess` + chained-callback channel is retired — `onSuccess` now has exactly the meaning the + contract declares (objectui#5934, maintainer ruling 2026-08-31). + + (The bump is `minor` by this repo's release model — objectui's major is pinned to + the `@objectstack` family major, and its own breaking changes ship as `minor` with + the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This + paragraph is that spelling-out: the break below is real and consumer-visible.) + + - **What breaks, by specifier**: `import type { ActionDef } from '@object-ui/core'` — + `ActionDef['onSuccess']` was `ActionDef | ActionDef[]` (chained callbacks the runner + dispatched through `executeChain` after a success). It is now derived from the pinned + spec: `ActionSchema.onSuccess`'s closed strict `{ navigate: string, openIn?: 'self' | + 'newTab' }` block. Code that assigned a callback `ActionDef` (or an array of them) to + `onSuccess` no longer compiles, and at runtime a callback-shaped value gets NO reading — + no handler dispatch, no navigation, the action's own result untouched. `onFailure` is NOT + changed: the spec declares no such key, so it keeps its one runner-native meaning. + - **Why this is safe to take**: the channel was unreachable from validated metadata — + `@objectstack/spec` (17.2.0 pin) strict-refuses a callback shape inside `onSuccess` at + parse (`invalid_type` on `navigate` + `unrecognized_keys`), so no published/saved + metadata could ever carry one — and a producer census with a positive control found zero + producers outside the channel's own test pins. Migration for an out-of-repo consumer that + drove the channel programmatically: put the follow-up actions in `chain` (the runner's + declared chaining key, unchanged), or author the spec's `onSuccess` navigation block. + - `@object-ui/types` (minor): `UIActionSchema` now declares `onSuccess`, derived from the + spec's `ActionSchema.onSuccess` — the renderer view spells the key the four action + surfaces forward, so the forwards type-check. + - `@object-ui/components` (patch): the four action renderers forward `onSuccess` without + the `as any` casts (no behavior change — same key, same value, now typed). +- e30ed15: Correct two retired lucide spellings on component-registration `icon` meta + (objectui#5936, the behaviour-neutral slice). + + `ui:page` declared `icon: 'Layout'` and `record:alert` declared + `icon: 'AlertTriangle'`. lucide retires a spelling by dropping it from the runtime + `icons` record while keeping the deprecated named export, so both names still + import and still type-check while resolving to nothing through any resolver that + reads that record. They are now the live keys `panels-top-left` and + `triangle-alert`. + + **Behaviour-neutral by identity, in every resolution world.** The retired export + and the live record entry are the SAME OBJECT — measured against the installed + lucide 1.31.0, not asserted: `Layout === icons['PanelsTopLeft']` and + `AlertTriangle === icons['TriangleAlert']` are both true. So the repair cannot + substitute one glyph for another; it can only turn a name that resolves to + nothing into one that resolves to the glyph it always meant. + + - Through a record-reading resolver (`renderers/action/resolve-icon.ts`), the old + spellings resolve to `null` and the new ones to that shared object. + - Through the dynamic surface (`iconNames`, 2025 names, retired aliases included), + both spellings already resolved, and to the same glyph. + - The kebab spellings were chosen because they are the only ones live on BOTH + surfaces: `PanelsTopLeft` and `TriangleAlert` are record keys but are absent + from `iconNames`, which is kebab-case only. + + Nothing is retired and no gate is extended here. `check-lucide-icon-record-names.mjs` + deliberately does not judge a registration's `icon` meta, and it stays that way — + its verdict is unchanged by this diff (182 names judged, green, both before and + after). Whether the registration `icon` meta itself should be retired is the open + half of objectui#5936 and is a maintainer decision under ADR-0049. +- 194fae1: `ui:button` resolves its authored `icon` through the shared `resolveIcon` instead of a + byte-equivalent copy of it (objectui#5993). + + `renderers/form/button.tsx` carried its own `toPascalCase`, its own `iconNameMap` holding + the single `Home -> House` entry, and its own index into lucide's runtime `icons` record — + the same algorithm as `renderers/action/resolve-icon.ts`, but not the same function. The + `action:*` family, `complex/data-table.tsx` and both menu renderers already import the + shared one. The hazard was drift, not rendering: an alias added to `resolve-icon.ts` to + absorb a lucide retirement (the objectui#5586 / #5622 mechanism) reached every one of those + sites and silently missed `ui:button`, which would have gone on resolving the retired + spelling to nothing while the rest of the repo resolved it correctly. + + **No behaviour changes, and that is measured rather than asserted.** The two + implementations were compared over 3547 names — every one of lucide's 1767 record keys in + both spellings, plus kebab-case probes (`arrow-right`, `dollar-sign`, `user-plus`), the + `Home` alias, retired spellings and `undefined`: 3539 identical by object identity, 8 + differing only in the nullish flavour returned for a miss (the copy indexed the record and + got `undefined`; the shared resolver `?? null`s it), zero genuine forks. That one + difference cannot reach the DOM — `Icon` is consumed at exactly two sites, both + `{!isLoading && Icon && <Icon .../>}` truthiness tests, and React renders nothing for + `null` and `undefined` alike. Icon identity, `h-4 w-4` sizing, `iconPosition`, the loading + state and the `Loader2` spinner are unchanged, and are pinned by + `renderers/form/__tests__/button-shared-icon-resolver.test.tsx`. + + Because behaviour is unchanged, the usual red-before ablation does not exist for this + change and none was manufactured. The one row in that suite that discriminates is + structural: it spies on the shared module and fails when the glyph does not come out of it, + which is red on the copy and green on the import. + + `scripts/check-lucide-icon-record-names.mjs` drops `form/button.tsx` from + `DECLARED_RECORD_READERS` in the same commit — that gate rediscovers record readers from + source on every run and fails on drift in both directions, so the removal is verified by + the gate rather than declared. It is also what now guards the dedupe: a re-inlined copy + would be discovered as an undeclared record reader and fail. The census entry for the + `button` *type* stays, its resolver re-pointed at `resolve-icon.ts`, so `ui:button`'s + authored icon names are still judged against the live record. + + `renderers/basic/icon.tsx` keeps its own copy deliberately and is untouched: `ui:icon` + draws a `SquareDashed` placeholder and warns on an unresolvable name (objectui#5631), which + the shared resolver does not do. +- 546ddf7: A node-gate visibility predicate that FAULTS now says so in a production build, once per + distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the + silence is no longer an accepted property"). Observability only — no verdict moves. + + `SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated + resolves to the same answer as one that said yes, so a gate that stops biting looks + exactly like a gate the author got right. The diagnostic that names it (objectui#5454 / + objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection + channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition` + implements that by evaluating **twice** — too expensive to ship for every predicate of + every node. + + **What production actually printed before, measured per dialect on the built evaluator** + — the card's premise held for one dialect of three, and the other two failed in opposite + directions: + + | dialect | production console, before | + |---|---| + | bare string | **nothing** | + | `{ dialect: 'cel' }` envelope | one generic line, deduped per source | + | `${…}` template | one generic line **per evaluation**, never deduped | + + So the dialect objectstack#11254 measured a live gate breaking on was the silent one, + while the template dialect was the console flood the ruling's rate-limit clause exists to + prevent. + + **The fix reports the fault the evaluator already detected, at the same number of engine + calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s + `ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the + canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer + up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason + passback), and supplying it transfers reporting to the caller so one fault stays one + line. Pinned: the CEL branch performs the same number of record reads with the passback + as without it, and strictly fewer than the `throwOnError` probe. + + `SchemaRenderer` passes it in production and reports through the **same** reporter the dev + branch uses — same message, same severity, same dedupe `Set`, same key. Development and + production now print the identical line for the identical fault; the `__DEV__` gate no + longer decides *whether* a fault is reported, only *how* it is detected. + + `page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same + reporter and the same rate limit. It swallowed the identical fault under a different + helper, and it was the worse of the two: the node gate at least reported in development, + while a faulting item predicate was silent in *both* builds on a gate whose false verdict + removes an entire tab, header and panel. + + **Rate limit:** deduped per (node type, gate key, predicate source) — never per render and + never per node instance. A two-hundred-row list of one broken predicate is one line; a + second distinct predicate source still gets its own line. Both halves are pinned, because + a test that asserts only "a warning was emitted" is equally green on an implementation + that emitted fifty, and one that asserts only "exactly one" is equally green on an + implementation that suppresses everything. + + **Not changed by this card, deliberately:** the fail-open semantics themselves; the + objectui#5687 adapter-only `data.*` report, which stays development-only under its own + 2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against + the wrong object); and the `/forms/:name` scope wiring of objectui#6262, which lands in + its own PR. + + `reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`, + `UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported + from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one + reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one + authored predicate would be entitled to one line per package instead of one line. +- 129bcc5: Column width and order that a user drags in `ObjectGrid` now actually persist + (objectui#6175). Both halves of `saveColumnState`'s only two call sites were dead, so a + drag was written nowhere — not to `localStorage`, not through `onColumnStateChange` to the + host's `dataSource.updateViewConfig`. The saved state was read back correctly forever; it + was simply never written. + + Two independent breaks, one per package: + + - **`@object-ui/components`** — `DataTableSchema` has declared + `onColumnResize?: (columnKey, width) => void` all along, and `data-table.tsx` invoked it + **nowhere**: the resize drag updated the table's local `columnWidths` state and stopped + there. It now reports the settled width once, at `mouseup`. Once, deliberately — the host + turns this callback into a write to shared view config, so a per-`mousemove` callback + would be a write storm. + - **`@object-ui/plugin-grid`** — `ObjectGrid` emitted `onColumnReorder` (singular) while the + renderer invokes the near-duplicate `onColumnsReorder` (with the `s`), a different declared + key with a different signature. The producer now emits the spelling the renderer actually + invokes, mapping the reported `TableColumn[]` to the `accessorKey` order `columnState` + stores. + + **Nothing is retired.** Both spellings remain declared on `DataTableSchema`; + `onColumnReorder` stays declared and stays unwired, exactly as the `RuntimeOnlyDeclared` + ledger in `zod-mirror-parity.test.ts` records it. Which of the two survives is a + declared-surface ruling that stays open and is deliberately not settled here. + + ⚠️ Behavioural note for hosts: `onColumnStateChange` now fires where it previously never + did, which means `dataSource.updateViewConfig` is now reached on a column drag. That call + was unreachable by this path before, so any permission gate on that write now sees traffic + it never saw. + + The renderer's resize/reorder gestures, the inbound seeding of `columnState`, and the + declared surface are all unchanged. +- f8c70f4: `ui:context-menu` now resolves a menu item's authored `icon` to a glyph. It previously never read the key at all. + + Both arms of `renderContextMenuItems` — the leaf `ContextMenuItem` and the `ContextMenuSubTrigger` — ignored `icon` entirely, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew its label and nothing else. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `ui:dropdown-menu` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. This mirrors the repair `ui:dropdown-menu` received for the identical defect. + + The `components-overlay-context-menu/basic-context-menu` catalog fixture already declared four live names — `copy`, `scissors`, `clipboard`, `trash` — which drew nothing before this change and draw their glyphs now. Those names are also brought under `check:lucide-icon-record-names` by a new `context-menu` census entry, so a future retired spelling fails the gate instead of silently drawing nothing. +- f9c34df: An `action:icon` hosted by an `action:bar` now reaches its handler. It forwarded the + COMPONENT id as the action type, so the click resolved nothing at all — no error, no + toast, a button that silently did nothing (objectui#6306, the objectstack#2169 "Mark Done + does nothing" shape). + + `action:bar` does not route members through `SchemaRenderer`. It pulls each member's + renderer off the registry and RENAMES the declared type as it spreads it onto the child: + `type` becomes the component id (`'action:icon'`) and the real declaration moves to + `actionType`. `action:button` has always resolved that pair when it forwards + (`schema.actionType || schema.type`); `action:icon` read `schema.type` alone and dropped + `actionType` entirely. `ActionRunner.execute` resolves its handler from + `action.type || action.actionType || action.name`, and `'action:icon'` binds no registered + handler and no builtin — for a declaration carrying `target` rather than `endpoint` it does + not reach the legacy `navigate`/`api` fallback either, so it fell through to + `executeActionSchema` and the authored action never ran. + + **The bug was a function of the layout, not the declaration.** One authored action executed + or did nothing depending on which `component` the host picked for it — the same asymmetry + objectui#5493 fixed on this renderer for `onSuccess`, one key over. + + `check:action-forward-parity` could not have caught this and its green run was never + evidence: `type` **is** in the forward whitelist, and that gate diffs key PRESENCE against + the owed set. This is a wrong-VALUE defect behind a present key, a class the gate has no + opinion on by construction. The existing icon coverage could not catch it either — it + rendered `action:icon` bar members three times and asserted only `visible`/`enabled`, never + that a click reached a handler, which is exactly how this shipped. + + Pinned by `action-bar-member-type-resolution.test.tsx`, which executes clicks rather than + inspecting props. Every row that reads the icon member's zero renders a sibling + `action:button` member of the SAME declaration in the SAME bar and reads its one first, so + a zero cannot be "the harness never executed anything". One row registers a trap handler + keyed on the component id, making the unfixed behaviour a positive artefact (the trap + fires) rather than only a missing call. A standalone row stays green in both worlds on + purpose: it refuses a "fix" written as `schema.actionType` alone, which would trade this + defect for its mirror image on the surface where `type` IS the action type. + + Scope is this one renderer. `type: schema` appears in exactly two files under + `renderers/action/` — `action-button.tsx` (already correct) and `action-icon.tsx`; + `action:group` and `action:menu` compose their members differently and are untouched. +- f24de8b: The form renderer no longer leaks `FormSchema.previousValues` onto the `<form>` DOM node + (objectui#6396). + + `previousValues` is a declared schema key with a real consumer: the renderer's + `previousRecord` memo, which binds `previous` for field-rule CEL predicates and is the + INSERT/UPDATE signal the read-only submit strip gates on (objectui#3484). That consumer + reads it off `schema` and is unchanged. The defect was on the other channel — + `SchemaRenderer` spreads every non-metadata top-level schema key as a React prop *in + addition* to handing the node over as `schema`, so an edit-mode host (`ObjectForm`, which + is what the `object-master-detail-form` header composes) delivered a second, top-level copy + in `...props`. The renderer already consume-and-drops that whole family before its DOM + spread — `objectName`, `onDirtyChange`, `defaultValues`, `fields`, `layout`, … — + and `previousValues` was the one member missing from the list. + + Two things followed, on every edit-mode header render. React declined the prop and printed + `React does not recognize the previousValues prop on a DOM element`, and — measured on + React 19, and not recorded on the card — the persisted record was still stamped onto the + element as `previousvalues="[object Object]"`. Consume-and-dropping the duplicate removes + both. + + Scope is the runtime leak only. The declared key stays exactly as declared + (`packages/types/src/form.ts`, `packages/types/src/zod/form.zod.ts` are untouched): it has + a live consumer, so there is nothing here for the enforce-or-remove channel. +- aff3d7a: A `bind` authored on a `data-table` is now diagnosed at render instead of ignored in + silence (objectui#6575). + + `bind` is the data-scope vocabulary: a path string resolved by `useDataScope()`. + `list`, `tree-view` and the `object-*` plugin widgets read it. `data-table` does not + — it takes its rows from an inline `data` array on the node and never calls the hook. + A `bind` on a `data-table` was nevertheless accepted by every gate: the TS side via + `BaseSchema`'s index signature, the zod side via `BaseSchema` being `.passthrough()`, + which `DataTableSchema.extend(…)` inherits. Nothing read it at render, so the author + got a table drawing a correct-looking header over the "No results found" empty state, + with no error and no warning — a success receipt for a disagreement between the + author and the renderer, and the hardest failure shape for a human or an AI author to + self-check. + + The platform was already paying for this in teaching rather than in diagnostics: + `skills/objectui/rules/protocol.md` documents the pothole verbatim and a pin test + locks the behaviour. The warning now also reaches the console, where the author who + did not read the docs is standing: + + > `bind: 'customers'` is ignored: data-table does not read `bind`; it reads its rows + > from the inline `data` array on the node. This node has no inline rows, so the + > table renders its header over an empty body. + + It names the node's address, the path that was spelled, and the way out. The + consequence clause is measured rather than asserted: a node carrying BOTH `data` and + `bind` is not empty, and is told that its rows came from `data` and its `bind` + contributed nothing. + + **No behaviour change.** `data-table` still does not read `bind`, and per the + 2026-08-27 ruling it must not start — making it a `useDataScope` reader is a separate + published-surface question needing its own ruling, including a `data`-vs-`bind` + precedence. Refusing the key at parse stays blocked on the `.passthrough()` ceiling + (objectui#5155 / objectui#6269). The trap stops being silent; it does not stop being + a trap. The channel is the one `plugin-grid`'s `columnSpellingDiagnostics.ts` already + uses for this exact shape of failure — a pure describe function, a `useEffect` keyed + on the schema slice, one `console.warn`, no NODE_ENV branch. + + `ObjectDataTable` (`@object-ui/plugin-dashboard`) stops forwarding a `bind` it has + already consumed. It resolves the binding itself via `useDataScope(schema.bind)` and + then delegated with `{ ...schema, type: 'data-table', … }`, which handed the spent + key to a component that cannot read one. Without this, a correctly authored and + published-guide-taught `object-data-table` would have tripped the new diagnostic on + every render, over rows that were on screen precisely because its `bind` had been + honoured. The key is stopped where it was spent — the same shape its sibling + `DashboardGridLayout` already uses for `data`. Nothing else about that delegation + moved, and the bound rows still arrive. +- 9486ac6: `ui:header-bar` now resolves a crumb's authored `icon` to a glyph instead of drawing + nothing (objectui#6645). + + `HeaderBarSchema.crumbs` is typed `BreadcrumbItem[]` — the same declaration + `BreadcrumbSchema.items` uses — and its Zod mirror does not merely declare `icon`, it + **describes** it (`.describe('Breadcrumb icon')`), so any authoring surface that reads Zod + `describe` can already offer the key to an author. `header-bar.tsx` contained zero + occurrences of the substring `icon`. + + After PR #6644 repaired the breadcrumb side, **one declared key behaved differently on its + two consumers**: authored on a `breadcrumb` item it drew a glyph, authored on a + `header-bar` crumb it drew nothing. The asymmetry was invisible only because it had been a + uniform zero on both. It is now asserted directly — one crumb object is rendered through + both renderers and the resolved glyph compared, in the positive direction and on a retired + spelling. + + Resolved through the **shared** `resolveIcon`, never a local normaliser: objectui#5993 is + the standing lesson that a local copy is the same algorithm under a different function, and + the alias later added there to absorb a lucide retirement reached every `action:*` site + except `ui:button`. So this is the lucide **record** surface — a live name draws its glyph, + an unknown or retired spelling draws nothing rather than degrading to a wrong one. The + `home` -> lucide `House` rename lives only in the shared resolver's map, and a pin asserts + it from the outside, so a future local re-implementation is red. + + The glyph renders once per crumb inside `BreadcrumbItem`, **above** `BreadcrumbLabel`, so + all three of that helper's arms — the siblings quick-switch dropdown, the last crumb's + `BreadcrumbPage`, and every earlier `BreadcrumbLink` — carry it by construction rather than + one at a time. + + Scored `patch`, matching PR #6644's scoring of the identical repair on the sibling + consumer: no new capability, a declared key that drew nothing starts drawing. + + A `crumbs-with-icons` catalog fixture authors the key and the docs page documents it, so + this does not come back next round as "declared but unenforced". The icon-record gate gains + a `header-bar` census entry for the same reason `context-menu` gained one in objectui#6278: + until the repair the names reached no resolver and declining them was correct, and a census + entry is a fact about a renderer. That is not objectui#5992's blind spot, which is the gate + *guessing* at containers nobody read off a renderer. +- d6ceb8d: Implement `ListColumn.wrap` — a column that says it wraps now actually wraps + (objectui#6650, maintainer ruling 2026-09-02, Option B). + + `@objectstack/spec` declares `ListColumn.wrap` and describes it to authors as + "Allow text wrapping", and `packages/plugin-grid/README.md` shows it in its + authored-column example. No renderer anywhere implemented it. Long cell text + stayed clipped to one line, with no error, no warning and no feedback of any + kind — a promise made at authoring time and silently broken at render time. + + **What changes.** A `data-table` column with `wrap: true` renders its cell body + `whitespace-normal break-words` instead of the default `truncate`, so long text + flows onto further lines and the row grows to fit. `ObjectGrid.generateColumns()` + forwards the authored key into the column slot, and `TableColumn` declares it, so + the key is honoured whether it is authored on a spec list view or directly on a + `data-table` node. `ObjectGrid`'s own `LinkCell` — the record link that column one + of almost every grid renders through — honours it too, because its own `truncate` + would otherwise clamp the text back to one line inside a cell body that was + willing to wrap. `@object-ui/types`' zod mirror carries the key as well; without + that the non-strict mirror would silently strip an authored `wrap` on the parse + road, which is the same "renderer honours what the declaration refuses" gap + objectui#6424 and objectui#6425 closed for their keys. + + **Nothing changes for anyone not authoring the key.** `wrap` absent or `false` + renders exactly what shipped before, pinned as a control rather than assumed, and + the link cell's default markup is byte-identical to what it was. + + **Precedence, where the two keys conflict.** `fitContent` WINS over `wrap`. A fit + column is `width:1%` with no `minWidth`/`maxWidth` clamp, so the auto table layout + sizes it from its content alone, and `whitespace-nowrap` is what holds that + content's min-content width at its max-content width — one line. Drop nowrap and + min-content falls back to the longest word, so honouring `wrap` there does not + wrap the column, it collapses it: measured in Chromium with the cell shape + reproduced exactly, 463.9px wide on one line with nowrap against 70.9px wide over + ten lines without it — 6.5x narrower and 5.9x taller. The keys do not compose, and + the one that yields is the one whose outcome nobody asked for. + + The static `table` renderer does not gain the key: `StaticTableColumn` tombstones + it, so an author who writes `wrap` there is refused loudly at parse time with the + remedy named, rather than having it silently stripped. +- 4c68077: A non-array `data` authored on a `data-table` node is now named at render + instead of dropped in silence (objectui#6665). + + `DataTableRenderer` takes its rows from `data: rawData = EMPTY_ROWS` off the + node and then collapses `Array.isArray(rawData) ? rawData : EMPTY_ROWS`. Any + non-array value an author wrote therefore becomes zero rows with no error and + no warning, and the table draws a correct-looking header over `No results + found` — which reads as a success receipt, the hardest failure shape for a + human or an AI author to self-check. + + The spelling that opened the card is a `${...}` expression string, and it is a + defect rather than a design because the SAME expression is evaluated one key + over. Re-measured on merge-base `5967be095` through the real `SchemaRenderer` + (the table was previously quoted from `skills/objectui/rules/protocol.md` as a + measurement on `f1c27f037` and had not been re-run); all four legs reproduced, + and they are now pinned as tests rather than prose: + + | node | body | + |---|---| + | `{ "data": "${data.customers}" }` | `No results found` | + | `{ "props": { "data": "${data.customers}" } }` | `No results found` | + | `{ "properties": { "data": "${data.customers}" } }` | the two rows | + | `{ "data": [ two literal records ] }` | the two rows | + + The predicate is deliberately WIDER than the reported spelling: `data` authored + and not an array. The `${...}` shape only selects a sharper sentence, because + the swallow at `Array.isArray(...)` is general — a number, an object, a `null` + and a plain string are dropped exactly as silently, and a predicate keyed on + the expression shape would leave each of them to arrive as a fresh card. + + It reuses objectui#6575's channel (`dataTableBindDiagnostic.ts`) as a SECOND + predicate rather than a widened one. The nodes that trip this carry no `bind` + at all, so that diagnostic's silence on them is correct behaviour, not a gap. + + No behaviour change: node-level `data` still does not evaluate expressions. + Making it do so is a behaviour change on a published component and was ruled to + the maintainer, not to this change. Nothing is added to the published surface + either — the new predicate, message builder and prefix constant are + module-internal and are not re-exported from the package entry, matching + objectui#6575's own symbols. The trap stops being silent; it does not stop + being a trap. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 06b8c42: Re-key three more renderer effects onto the primitives they actually read, + instead of the memoised object identity that produced them (objectui#6697 — + the three census members from objectui#6592 that sit outside its + `getDataConfig(schema)` family): + + - `RelatedList`'s collection fetch now depends on `defaultSortKey` / + `filterKey` (the `JSON.stringify`-derived content strings the two memos are + already keyed on) rather than on `defaultSortSpec` / `listFilterNode`. + - `page:tabs`' related-count probe now depends on a serialised `probeKey` + rather than on the `probeTargets` `Map`. + - `ListView`'s data fetch now depends on the `expandFields` memo's own INPUTS + — `schema.columns`, the alternate views' binding blocks and + `objectDef?.fields`, all props and state a discard cannot move — rather than + on the `expandFields` array the memo returns. + + `useMemo` carries no semantic guarantee — React is permitted to discard a memo + cache and recompute even when its dependency array compares equal to the + previous render — and all three factories return a FRESH value on every call + (`normalizeSortSpec`/`toFilterNode` build a new array / a freshly lowered AST, + the probe factory builds a new `Map`, `buildExpandFields` returns a new array + in every branch). So each effect re-ran on a discard alone, with nothing an + author or a caller controls having changed: an extra `dataSource.find` for the + related collection, an extra `dataSource.find` for the list window, and a + redundant re-probe of every tab's count. Keying on the primitives makes a + cache discard a no-op and returns `useMemo` to being a pure optimisation. + + Severity is low and the fix is deliberately narrow: the observable was a + redundant round trip, never incorrect data, so only the re-run condition + moves — each effect body still reads the memoised value, and a genuine change + still refetches exactly as before. + + The three take two routes on purpose — key on the nearest DISCARD-IMMUNE + thing. `RelatedList`'s memos are keyed on exactly one primitive each, and + `page:tabs`' probe memo is keyed on another MEMO's output (`items`), which is + not discard-immune, so both take a content string. `ListView`'s memo is keyed + on props and state, so it names those directly: a value key over + `expandFields` would NOT have been content-equivalent there — `buildExpandFields` + collapses the collected set down to the relation roots, while the effect body + also builds `$select` from `schema.columns` and the view bindings — and it + would have defeated objectui#4567's live-dependency pin, which ruled that + "ListView's by-identity dependency is correct for a real column change" and + put the identity stabilisation at the PRODUCER. + + One correction to the census card's account, measured while pinning it: for + `page:tabs` the redundant probe costs nothing on the wire. + `RelatedCountStore.fetch` returns the cached count as its first act and dedupes + concurrent probes, so the extra work is the effect re-running, not an extra + request. +- 7c9b044: `flex` declares the containment it renders (objectui#6740). + + `flex` has always rendered `schema.children`, but its registration omitted + `isContainer` while `grid`, `card`, `container` and `stack` — same directory, + same `ui` namespace — all declared it. The render path never reads the flag, so + nothing was broken at runtime; its consumers are elsewhere, and the gap made + them contradict the renderer. + + MEASURED through the mechanism, not inferred from the property. Building the + manifest the way the app builds it (`getKnownTypes()` + `getMeta()` -> + `manifestFromConfigs`) and putting a `flex` node carrying children through + `validateTree` returned `["not-a-container"]`, while `grid` / `card` / + `container` under the identical probe returned `[]` — the control that makes + that reading real. Downstream, objectstack's three shipped + `examples/app-showcase` html pages drew 232 diagnostics, of which every one of + the 32 warnings was `not-a-container` on `flex`, and `flex` was their only + source. `validateTree` is not on objectstack's production gate path today, so + the warnings are currently unobserved — which is why this is worth closing + before that gate goes live rather than after. + + **Second consumer, and the reason this is not purely a declaration change.** + `renderers/layout/react-page.tsx` builds the JSX scope of every `kind:'react'` + page with `if (!tag || cfg.isContainer) continue;`. While `flex` omitted the + flag it was the one layout primitive of the five still injected there, so + `<Flex>` resolved in react page source — and rendered an EMPTY div, because the + injected wrapper drops `children`. It now behaves like its four siblings and is + not injected, which is what `content/docs/guide/react-pages.md` has documented + all along ("Layout containers are deliberately not injected ... `<flex>`, + `<grid>`, `<card>` and friends have no injected wrapper"). A react page that + wrote `<Flex>` moves from silently swallowing its children to the page-level + error panel naming the identifier, with that page's documented remedy being + real HTML: `<div style={{ display: 'flex', gap: 16 }}>`. + + Pinned over the family rather than over `flex` alone: the defect's shape was + "three declare it and one does not", and a pin covering only the one that was + missing would let the next registration rot the same way. +- 846889b: The seven semantic sectioning tags and `aspect-ratio` declare the containment + they render (objectui#6764). + + `renderers/layout/semantic.tsx` registers `aside`, `main`, `header`, `nav`, + `footer`, `section` and `article`, and `renderers/layout/aspect-ratio.tsx` + registers one more; all eight call + `renderChildren(schema.children || schema.body)` and none declared + `isContainer`. Nothing on the render path reads that flag, so children always + rendered — what the omission did was make `validateTree` warn `not-a-container` + on a child list it then rendered, on the tier built to accept AI-authored pages. + A warning that lies trains authors to discount the true ones. + + Same reasoning as objectui#3900 (`page-header`) and objectui#6740 (`flex`): + `children` is a base property of every node in the JSON protocol, not a + per-component authoring key, so the flag widens no spec surface. + + Scoped by measurement, not by sweep. The census behind this change rendered + every registered key through the real `SchemaRenderer` and put it through + `validateTree`: of 131 bare authoring tags, 58 render `schema.children`, 5 + declared the flag, and 53 did not. These 8 are the subset where the second + consumer is provably unaffected — `renderers/layout/react-page.tsx` drops + containers from the `kind:'react'` JSX scope, but it reads `getPublicConfigs()` + and none of the 8 is in the curated public contract. The remaining 45 are + reported on the card rather than swept in, `button` among them precisely because + it IS public. +- 26896c6: `element:*` renderers stop re-reading a degenerate config bag as its own character indices — the third and last channel of the objectui#6752 / objectui#6760 hazard (objectui#6783). + + Five modules under `packages/components/src/renderers/basic/` — `elements.tsx`, `data-list.tsx`, `text-input.tsx`, `record-picker.tsx`, `metadata-viewer.tsx` — each carried a copy of the same reader, `{ ...(schema?.props ?? {}), ...(schema?.properties ?? {}) }`. `??` only replaces `null`/`undefined`, so a non-object bag went into the object spread and came back out as indexed keys: for `properties: 'not-a-bag'`, the config bag a renderer received was `{ '0': 'n', '1': 'o', … '8': 'g' }` — nine keys nobody authored. The five copies are now one `readProps` (`renderers/basic/readProps.ts`) that asks `isConfigBag`, and a degenerate bag on either side contributes no keys. + + `@object-ui/react` exports `isConfigBag` from its package entry. That is the API addition here, and it is the reason the fix is not a sixth spelling of the predicate: objectui#6761 converged six occurrences of "is this a real config bag?" behind one definition in `packages/react/src/utils/configBag.ts` and pinned it, but the pin scans `packages/react/src` only — a copy written one package over would be invisible to it. `@object-ui/components` already depends on `@object-ui/react` (all five modules import from it today), so the reachable answer was to publish the definition rather than retell it. Same reason the node-gate predicate reporter is exported at that entry (objectui#6038): one definition, read by every package that asks. + + **What this does not change, measured rather than predicted.** No rendered output moves on today's tree. All five renderers read named keys off this bag, and the single onward spread — `metadata-viewer`'s `<StateMachineView {...props} />` — hands it to components that destructure named fields, so the indexed keys were computed and then dropped. The census behind objectui#6708 found zero authored nodes carrying a degenerate config bag, so this was a latent shape, not a live failure. What the guard buys is what objectui#6752 measured its own guard buys, one channel further down: the authored value's shape is not reinterpreted. objectui#5123's precedence is untouched — `properties` still wins a contested key, and a degenerate bag declares no key for either side to win. +- 00d3f09: `button-group` honors per-button `disabled`, and the catalog stops authoring 29 keys + nothing reads (objectui#7077, maintainer ruling 2026-09-04, decision batch #25). + + **The renderer change.** `ButtonGroupButton` declares `disabled?: boolean` and the + renderer read it nowhere — it mapped `schema.buttons` to `Button` elements passing + `variant`, `size`, `className` and `label` and nothing else, so a button declaring + `disabled: true` rendered live and clickable. It now forwards the value, matching every + sibling that declares item-level `disabled`: `tabs`, `select`, `dropdown-menu`, + `menubar`, `context-menu`, and `toggle-group` since objectui#4632. This is mechanical + consistency with an already-declared contract — no new key, no new type member, and no + change to what any schema accepts or refuses. + + **Migration.** None required. A `disabled: true` you already author starts taking + effect; that is the declared meaning of the key, and nothing in the shipped corpus + authored it. + + **The corpus change.** `button-group` stays a **presentational** group: no selection + state, no `selectionMode`, no group-level `value`, no per-button `value`, no per-button + `icon`. All six catalog fixtures authored those four undeclared keys — 29 occurrences — + and every one parsed green, because `BaseSchema` is `.passthrough()` and carries + `[key: string]: any`: admitted unexamined, never refused. The keys are gone from the + fixtures. Implementing selection instead was weighed and rejected: it is a capability + addition with zero measured pull, and the only things authoring it were fixtures we + wrote ourselves — the corpus reverse-defining the product. It remains available as a + fallback if a real consumer (an app or example needing a segmented control) is produced + first. + + This matters more than a demo tidy-up because the catalog is the corpus AI authoring + tools retrieve from. An author copying the old `single-selection.json` got a schema that + validated, published, and did nothing — a failure with no signal to self-correct from. + + `with-icons.json` is **removed** rather than emptied: with `icon` retired, a fixture by + that name authored no icons, and what remained duplicated `basic-button-group.json`. Its + demo slot on the docs page goes with it. The other five keep their labels and still + render — `icon-toolbar.json` rendered three blank buttons until objectui#6318 gave it + labels, and a pin now holds every fixture in the category to a non-empty `label` on + every button. + + `onClick` is **not** wired, and is not a gap: objectui#6124 (PR #7339) retired it two + days before this ruling. It is `onClick?: never` on the TypeScript face and a refusal by + name on the Zod mirror, so there is no declared-but-dead handler left to forward. +- 639114c: Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122). + + ⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.** + The exported `ObjectSchemaClientExtensions` narrows from + + ```ts + export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' } + ``` + + to + + ```ts + export type ObjectSchemaClientExtensions = Record<never, never>; + ``` + + Two breaking consequences for a consumer that names the type directly. **(1)** It + no longer declares `editMode`; the key is now carried by the spec's + `ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against + the extension type ALONE loses it. **(2)** `interface` → type alias also ends + **declaration merging**: a consumer that reopened + `declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }` + to add its own client-side member no longer compiles, because an alias cannot be + reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule — + objectui's own breaking changes are graded `minor` with the semantics stated in + the body, since any `major` in the fixed group would push all 39 packages off + `@objectstack`'s major. + + **`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured: + the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty, + declared as the same `'page' | 'modal'` union objectui carried). Its local copy + is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin + prescribed for this event, leaving the client delta empty. Nothing is removed + from the product: `editMode` stays authorable and stays typed on + `ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a + local member — and a published, spec-validated object document may now carry it, + which at 17.2.0 was refused by name. + + **`user:profile` is retired across all three sites.** 17.3.0 dropped it from + `PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly + `['user:profile', 'element:form']`, gained set empty). objectui went on knowing + it in three places, so all three moved together: the Studio palette exclusion + ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the + regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing + user-reachable went with it — neither type had a renderer, `user:profile` had + only the dashed "Component Placeholder" scaffold, and the app shell's own + profile affordance is a React slot, never this block type. A page schema still + naming it now draws the loud "Unknown component type" panel rather than a silent + grey box, which is this repo's standing treatment for a type outside the + supported surface. + + **`record:details` sections document the eight keys 17.3.0 added.** + `group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`, + `description` and `headerColor` are now declared on a section entry (4 → 12 + members). Six of the eight are already honoured by `DetailSection`, so the + `sections` input description now teaches all of them, and says plainly which two + are not read here. Designer controls for them are a separate feature and are + deliberately not added. + + **`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` → + `^17.3.0`, and this is the second half of its `minor`.** The package's emitted + `dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which + `17.0.0` does not export, so the old range was a claim the artifact did not + support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]` + and names `^17.3.0` as the lowest version carrying every symbol the package + references. Breaking for a consumer pinned below 17.3.0: it can no longer + resolve this package. That is the range stating the truth rather than a new + restriction — the artifact already required those symbols — and it is the + remedy the gate itself prescribes ("Raise that package's range to the lowest + version that exports the symbol… Do not add a tolerant re-declaration on this + side: the range is the claim, and the claim is what is wrong", objectui#5793). + `@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0` + and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is + this repo's normal state, not an exception. + + ⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing + and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main + workflow that cannot red a pull request, and `main` is green on it — the last + eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1 + with CI's own `--cross-check` invocation, and exits 0 with this raise, judging + 278 (subpath, symbol) pairs across 19 published packages either way. Its blocking + copy runs on the publish path, so leaving it would have surfaced as a cancelled + release rather than as a red check. The correction is recorded on objectui#7688. +- b9f5ff1: fix(components): eight widgets now consume `SchemaRenderer`'s evaluated `disabled` verdict instead of re-reading the raw authored key + + `disabled` on a schema node is `boolean | string` — the string being a predicate. `SchemaRenderer` evaluates `disabled` / `disabledOn`, strips the raw key from the props it spreads, and forwards the answer as a real `disabled` prop. `ui:form`, `ui:button`, `ui:input`, `ui:textarea`, `ui:checkbox`, `ui:select`, `ui:combobox` and `ui:collapsible` re-read the raw key beside that verdict, and an expression string is truthy however it evaluates. + + Two user-visible defects go away: + + - `ui:form` — a form declaring `disabled: "${...}"` greyed out every field, the submit button and the cancel button even when the predicate was FALSE. + - `ui:button` — `loading: true` did not disable the button when it was rendered through `SchemaRenderer`: the computed state was overwritten by the forwarded verdict arriving through the DOM pass-through spread, so the spinner ran on a live control. + + The six DOM pass-throughs kept their behaviour through `SchemaRenderer` and now read the verdict by name rather than depending on spread order. +- 4704aa4: A form no longer ends on a screen asserting both a failure and a success (objectui#7252). + + A refused submit raised an error toast that nothing ever retired, so when the user + fixed the input and submitted again the confirmation of that second attempt appeared + *beside* the refusal of the first — a wizard's last step showing "Invalid project + status transition." and "Your new project is ready…" at the same time. + + Every outcome toast a form raises now travels under one stable per-form id, so the + later outcome supersedes the earlier one instead of stacking beside it: + + - `@object-ui/components`' form renderer publishes its three outcome toasts (the + field-level rejection, an `onAction` error, and a rejected write) under that id, and + retires the previous attempt's toast in the same place it already cleared the + previous attempt's in-form banner. This is what fixes the reported wizard flow: the + refusal comes from this renderer while the success toast is raised by the host + (`WizardForm` / `ObjectForm`), so no single raiser could supersede the other before. + - the console's own `FormPage` publishes its confirmation and its submit failure under + one id, for the same reason. + + Toast durations are unchanged — this is about supersession, not lifetime. The + objectui#4190 arm is deliberately excluded: a refused redirect *destination* still gets + its own toast, because the write succeeded and that refusal has to stay readable beside + the confirmation it qualifies. +- 26e06d7: Delete the second, dead `SchemaRenderer` in `packages/components/src` (objectui#7319). + + `packages/components/src/SchemaRenderer.tsx` was a 28-line component carrying the same + export name as the real renderer in `packages/react/src/SchemaRenderer.tsx`. Nothing + reached it: it is absent from the package barrel, no file in the repo imports it by any + form, and the package's `exports` map has no subpath that resolves to it. + + **No behaviour changes.** The file was never in the runtime bundle — two markers unique to + it appear in zero `dist/` files, while controls for barrel-exported symbols appear in four + each. Its only shipped footprint was a stray types-only `dist/SchemaRenderer.d.ts` with no + runtime module behind it, reachable through no specifier; the published tarball loses that + file, and no importable surface changes in either direction. Hence a patch, not a minor. + + **Why deleting beat keeping.** The copy is a trap, which is what the card's triage asked + whoever took it to settle. It evaluates no predicate at all: of the real renderer's six + visibility legs (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` / + `hiddenOn`) it consults exactly one, `hidden`, and by bare truthiness rather than + evaluation — so a node declaring `hiddenOn` is never hidden, and the two enablement legs + (`disabled` / `disabledOn`) are not read at all. It then spreads `{...schema}` raw, so + `disabled` would reach the widget as an unevaluated value, which is the precise inverse of + the real renderer's contract: evaluate the predicate, strip the raw key, forward only the + verdict. +- 81a2eb1: One home for the `datetime` display convention (objectui#7443). + + `formatDateTime` gains a named `'compact'` style, selected through + `options.style` — the dense grid face, `7/4/2024 7:00 am` in `en-US` — which + `DateTimeCellRenderer` used to build from its own inlined `Intl` option bags. + The cell now reads `field.format` (it destructured `value` only, so a + `datetime` field could not reach the style vocabulary a `date` field has) and + renders through the shared function, and `data-table`'s `formatCellValue` + calls `formatDateTime` instead of a third, independently authored option bag. + Every existing cell without an authored `format`, and every cell authoring + `'compact'`, renders byte-identically; `'compact'` is today's face named and + rehoused, not a new one. A `datetime` field that authors any OTHER non-empty + `format` does change: the cell previously ignored `field` altogether and always + painted the compact face, and now anything other than `'compact'` selects the + verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in + `en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An + unrecognised value is neither rejected nor passed through; it silently lands on + that verbose face. No `datetime` field in this repository authors a `format`, so + no cell here moves — a consumer that authored one is the case this sentence is + for. Note that `format` has no declared value vocabulary to check a value + against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec` + carries one free-form `format?: string` on its shared field schema, described + "Format string (e.g. email, phone)" and accepting any string. `'compact'` is + therefore the only value with a defined `datetime` meaning, and every other + value means "the verbose face" by fallthrough rather than by design. + + Additive, no signature change: `formatDateTime(value, options?)` is unchanged + and `formatDateTime(v, { locale })` keeps meaning what it meant. + `DateDisplayOptions` gains an optional `style` key (read by `formatDateTime` + only; `formatDate` still takes its style positionally), and + `formatDateTimeCompactParts` is a new export of `@object-ui/core`, re-exported + by `@object-ui/fields`, returning the compact face as the two halves a grid + cell paints separately. `@object-ui/components` changes no rendered output — + the table's datetime cell is measured identical before and after in `en-US`, + `zh` and `de-DE`. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 2f61238: Remove `src/ui/toast.tsx`, an unreferenced primitive, and the dependency only it imported + + The file was reachable from nothing: no importer anywhere under + `packages/components/src`, and `ui/index.ts` never carried it, so the barrel's + `export * from './ui'` did not reach it either. It shipped all the same — + `dist/ui/toast.d.ts` was in the published tarball — while contributing nothing to + `dist/index.js` and nothing to the package's export surface. `ui/sonner.tsx` + (`Toaster`) is the live implementation and is unaffected. + + `@radix-ui/react-toast` is dropped from `dependencies` in the same change: the + removed file was its only importer anywhere in the repository, so it would + otherwise have stayed a declared dependency of every install with nothing to + resolve it. + + No exported name changes. A consumer who was resolving `@radix-ui/react-toast` + through this package's dependency was relying on hoisting rather than on a + declaration, and should declare it directly. +- f7ace0a: `@object-ui/components` no longer declares `recharts`. The dependency became + unused when objectui#7397 deleted `src/ui/chart.tsx`, which was its only + consumer inside this package; `@object-ui/plugin-charts` declares its own + `recharts` and remains the single implementation (objectui#7625). + + **No migration.** Nothing is added to or removed from the published surface, no + type changes, no behaviour changes. Since objectui#7397 this package publishes + no recharts-typed export at all, so there is no supported import that reaches + `recharts` through `@object-ui/components`. + + **What actually changes** is the install graph: every consumer of + `@object-ui/components` was resolving and installing a charting library it could + not reach. AGENTS.md section 3 constrains this package — the Atoms layer — to + "Shadcn primitives, zero heavy 3rd-party deps", and heavy widget dependencies + belong in `@object-ui/plugin-*`; the declaration outlived the reason it existed. + + - **Why `patch` and not `minor`.** This repo ships its own breaking changes as + `minor` with the break spelled out (`scripts/check-changeset-no-major.mjs`), + so the bump has to state which of the two this is. Nothing breaks for a + supported consumer: the built output is byte-identical (this package's Vite + `external` predicate is path-based and never reads `dependencies`, so no + import graph, chunk or `.d.ts` moves), and the package exposes no recharts + surface to import. The one consumer this can reach is someone importing + `recharts` without declaring it and getting it hoisted out of this package's + dependency by a FLAT `node_modules` layout (npm/yarn). That is a phantom + dependency — undeclared, unsupported, and un-typed here since objectui#7397 — + and it is named rather than omitted so the trade is on the record. + - **Not a bundle-size change.** `vendor-charts` stays eagerly reachable through + the plugins, so `check:eager-closure` is unaffected in both directions. The + cost this removes is install-graph weight, not shipped bytes. +- 8b7ea39: The zod mirrors stop authoring defaults; the renderer's fallback is the one + authoritative default (objectui#7735, director ruling, decision batch #69, + 2026-09-07 — maintainer reply 「其他同意」). + + **A validator validates; it does not write values into an author's document.** + `.default(v)` on a mirror member is not documentation — `parse` SUBSTITUTES `v` + into the output when the key is absent. One authored document therefore had two + shapes depending on whether it had been through `safeValidateSchema`, and where + the mirror and the renderer disagreed the mirror silently won: + + | key | mirror wrote | the renderer applies | + |---|---|---| + | `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | + | `FlexSchema.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | + + A `container` omitting `maxWidth` rendered `max-w-xl` as authored and `max-w-lg` + after a round-trip through the mirror; `'center'` is a value neither `flex` nor + `stack` applies at all. `StackSchema` already declared no defaults, so the file + was not even self-consistent. + + **What changed.** All 41 `.default()` call sites under `packages/types/src/zod/` + are removed — `layout.zod.ts` 22, `crud.zod.ts` 11, `form.zod.ts` 5, + `views.zod.ts` 2, `app.zod.ts` 1. `@object-ui/components` reconciles the third + face objectui#8229 found: `flex`'s registration `defaultProps.align` seeded + `'center'`, the value its own renderer never applies, so a designer-made node + laid out differently from a hand-authored one; it now seeds `'start'`. + + **Accept set unchanged.** Every one of the 41 was spelled + `<type>.optional().default(v)`, so none was carrying optionality: nothing that + parsed before is refused now, and nothing refused before is accepted. This is a + narrowing of `parse` OUTPUT only. + + **⚠️ Breaking for consumers that read `result.data`.** `validateSchema` / + `safeValidateSchema` no longer substitute: for a document that OMITS one of these + keys the key is now ABSENT from the parsed output instead of carrying a value the + document never authored. Every one of the 41 keys changes output this way. If you + read a default off the parsed document rather than applying your own fallback, + apply the fallback yourself. + + **And rendering changes too, wherever the mirror's value differed from the + renderer's.** An earlier draft of this note said rendering does not change on the + ground that "the two keys that disagreed were the bug"; that was the triage + sample of 7 keys, not a measurement of the population. Auditing all 22 layout + keys against their renderers finds **six** disagreements, not two: + + | key | the mirror wrote | what the renderer applies when the key is absent | + |---|---|---| + | `container.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | + | `flex.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | + | `grid.columns` | `3` | `grid.tsx`: `let baseCols = 2` | + | `text.variant` | `'body'` | `text.tsx`: none — a bare node, no typography class (objectui#6942) | + | `resizable.withHandle` | `true` | forwarded bare; `undefined` draws NO grip | + | `page.template` | `'default'` | `page.tsx`: a null template falls through to the `pageType` dispatch | + + So a consumer that renders `result.data` sees a different result on all six. What + does NOT change is rendering inside this repository: the render path never ran + this validator at all — `SchemaRenderer` calls the STRUCTURAL `validateSchema` + from `@object-ui/core`, which returns `{valid, errors}` and hands the renderer + the author's own object. The four modules that do import `@object-ui/types/zod` + were censused: two read only `result.success`, one discards `result.data`, and + `os validate` reads only `type` / `id` / `label` / `title` / `children`, none of + which ever carried a default. The four JSDoc `@default` tags that described the + mirror rather than the renderer (`grid.columns`, `text.variant`, + `resizable.withHandle`, `page.template`) are corrected in the same change. + + Note that `safeValidateSchema` still substitutes through subschemas imported by + reference from `@objectstack/spec` (`app.active`, `object-view.navigation.*`, + `list-view.sharing.type`, and others). Those values are written in that package, + not this one, and are unaffected by this change. +- 3c76801: `action:button`, `action:icon`, `action:group` and `action:menu` now forward the + declared `onSuccess` block, so the post-success navigation an author writes in metadata + actually happens on those four surfaces (objectui#5493). + + `onSuccess` — spec's closed strict `{ navigate, openIn }` object — became authorable on + `ActionSchema` with the `@objectstack/spec` 17.1.0 pin bump (objectui#5328), and the + runner has read it off the **forwarded** def since objectui#5221: + `ActionRunner.handlePostExecution` → `readOnSuccessNavigation` → `navigateOnSuccess`, + which hops through the app's own `navigationHandler` (a real SPA route change, immune to + popup blocking). Between those two halves sat these four forward whitelists, which never + carried the key. The action succeeded, the toast said so, and the declared hop silently + did not happen — the same "shipped green while dropped one hop before the runner" class + as `bodyExtra` (objectstack#6837), `bodyShape` (objectstack#6938) and `resultDialog` + (objectui#3646). + + Reachability before this change was a function of which host rendered the action: the + full-def-spread hosts (`DeclaredActionsBar`, `ObjectGrid.onActionDef`, + `RelatedRecordActionsBridge`, `useNavActionDispatch`) already carried it through, so the + same declaration hopped there and did nothing here — and on an `action:bar`, whether an + action lands inline (`action:button`) or in the overflow (`action:menu`) is decided by + `maxVisible`, 3 desktop / 1 mobile. The declared navigation therefore depended on + viewport width. + + Pinned end to end in + `packages/components/src/renderers/action/__tests__/action-onSuccess-forward.test.tsx`: + one row per surface drives the real renderer through the real runner and asserts the + `${result.*}`-interpolated url reaches `onNavigate`, with `openIn` exercised on both + branches. Each row asserts the action executed first, so a zero-navigation reading + cannot be a harness that did nothing. + + The four matching `KNOWN_GAPS` entries in `scripts/check-action-forward-parity.mjs` are + deleted — that ledger is ratcheted, so a stale entry excusing a key that is now + forwarded fails the gate. `element:button` is untouched and stays correct: `onSuccess` + is not on spec's `InlineActionSchema` pick list, so that surface never owed it. +- 2c8474c: Published declaration text for the react page renderer changed: `kind:'react'` page `source` styling is now stated in the shipped `.d.ts` + + `dist/renderers/layout/react-page.d.ts` is in this package's npm tarball, and the + declaration emitter reproduces that module's file-header documentation into it + verbatim. Two things a consumer reads — on hover over `ReactKindPage` in an editor, + or straight out of the tarball — now say something different: + + - The injected-scope note no longer tells authors that layout is left to "plain + HTML + Tailwind". It says plain HTML. + - A new paragraph states the styling contract for `kind:'react'` page `source`: + `source` is runtime metadata, not build input. Style with inline `style` objects + using `hsl(var(--token))` theme colors, and render overlays through `ObjectForm` + with `formType` `"drawer"` or `"modal"` rather than a hand-rolled `fixed inset-0` + backdrop. Do **not** author Tailwind utility classes in page `source`: the + console's Tailwind is compiled at build time by scanning the console's own `src` + and there is no safelist, so an authored utility class silently produces no CSS + and no error anywhere. `os validate` reports it as + `page-source-className-tailwind`. (ADR-0065; ADR-0080's 2026-06-30 amendment; + see `content/docs/guide/react-pages.md`.) + + That is guidance an author can act on, and it contradicts what this package's + declarations previously told them, so it is a change to the published surface + rather than an internal edit. No runtime behaviour changed and no export moved. + + This entry is corrective. The text landed under objectui#5461 with a changeset that + declared only `@object-ui/types`, on the stated grounds that the edits "do not + project into any `.d.ts`". Rebuilding the package shows that they do — the reasoning + and the measurement are recorded in `scripts/check-changeset-presence.mjs`'s header, + where the next author will meet them. +- ae61ad4: The console form now renders a refusal message the producer explicitly marked + as user-facing, instead of always substituting a generic string + (objectui#5210). + + An application's hook guards could not talk to their users. When a hook refused + a write with 403, the form replaced the server's text with + `form.noPermissionToSave` unconditionally — the recorded objectstack#3821 fix, + which exists because a raw refusal body puts untranslated platform diagnostics + (`FORBIDDEN: insufficient privileges to update showcase_private_note + pi-TgoJ4_DM55Fqz`) in front of end users. The external report behind this change + had 11 guards whose deliberate, localized guidance — which role owns the action, + whom to ask — never reached anyone, and named the incentive that creates: + returning 400 instead of 403 for permission failures, degrading the status + semantics logs, monitoring and API consumers depend on. + + The maintainer ruling (2026-08-19) was a producer-side opt-in rather than a + chattier 403 branch, and the platform half shipped as objectstack#9934: a hook + marks its refusal text with `userMessage` at throw time. This is the consumer + half. + + - `@object-ui/react` gains `declaredUserMessage(err)` — the one "is this + marked?" read. It answers the marking verbatim, from the two places the + adapter boundary parks the envelope (the error itself, where + `@objectstack/client` lifts it, and `details`), and `null` for everything + else. + - The form prefers a marked message over both its generic strings, on ANY + status — the marking is status-agnostic; 403 is where this was reported, not + a fence the contract draws. + + **Unmarked refusals are unchanged**: a 403 with no marking still shows the + generic `form.noPermissionToSave`, and the raw text still goes to the browser + console only. objectstack#3821's protection is preserved by construction, not + by re-guessing which 403 bodies are presentable — the mark and the marked text + are one field, so no boundary that rewraps or substitutes `message` can promote + platform prose into the user-facing channel, and platform code never sets it. +- fe76ece: The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. + + `vite-plugin-dts` emits one declaration file per source file, and TypeScript + copies a module specifier into the declaration verbatim. `export * from './ui'` + therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in + `@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted + trees. Node16/NodeNext resolution does not extension-search a relative + specifier, so the compiler could follow none of the hops and every symbol they + carried read as absent from the package: + + ``` + error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. + ``` + + Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls + in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from + `layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. + On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. + + The emitted `.js` never had the defect — rolldown resolves the same specifier + away — which is why `pnpm check:esm-specifiers`, whose verdict is about + specifier-preserving `.js` builds, correctly never scanned either package. The + fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, + shared by both `vite.config.ts` files), not in the sources: the same source line + produces a clean `.js` and a broken `.d.ts`, so no source edit can express the + difference. The rewriter resolves each specifier against the source tree the + output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — + throws on anything it cannot resolve, and after the build re-parses the emitted + declarations to assert every relative specifier both carries an extension and + names a file the build really emitted. + + `packages/fields` takes the `nodenext` pin as a result — the same two lines + `packages/react` has carried since objectui#4538 — so the property is enforced by + the compiler on the consumer side rather than by review. `packages/app-shell` + does not: it type-checks clean without the pin and still shows 23 errors with it, + none of them from these two packages. That residue is filed separately. +- 58770f3: **Behaviour change:** the eighteen renderers whose host element is a form + control no longer forward their whole prop bag to it. They route it through a + form-control DOM declaration — the same `pickDomProps` mechanism `grid`, `flex`, + `stack`, `container` and `text` were converged on — so an authored schema key + becomes an HTML attribute only if the contract declares it one (objectui#5632, + the `BARE_SPREAD_MINUS_NAME` slice of objectui#5574). + + The eighteen: `action:button`, `action:icon`, `ui:button`, `ui:checkbox`, + `ui:combobox`, `ui:date-picker`, `ui:email`, `ui:file-upload`, `ui:input`, + `ui:input-otp`, `ui:password`, `ui:radio-group`, `ui:sidebar-menu-button`, + `ui:slider`, `ui:sonner`, `ui:switch`, `ui:textarea`, `ui:toggle`. + + What this stops reaching the DOM: the renderer's own declared props, consumed + off `schema` to render the control AND spread onto the element a second time as + attributes HTML does not define, plus the authored node's SDUI metadata and the + flattened `props` container. Measured across `examples/schema-catalog`, rendered + through the real `SchemaRenderer`: 284 illegitimate attributes over 287 + form-control nodes — `button[label]` 140, `button[icon]` 25, `input[inputtype]` + 23, `input[label]` 19, `toggle[label]` 14, `radio-group[options]` 8, + `date-picker[placeholder]` 7, `file-upload[label]` 7, `file-upload[buttontext]` + 7, `checkbox[label]` 6, `textarea[label]` 6, `toggle[arialabel]` 6, + `switch[label]` 4, `password[label]` 3, `email[label]` 2, `radio-group[direction]` + 2, and six singletons. The same probe reads 0 after, with `grid`'s 26 nodes at 0 + both times as the control and an unchanged node census across the two runs. + + `name` and `disabled` are DELIBERATELY still forwarded. Both are legal on a form + control and neither was ever part of the leak — the ledgered shape for this + group is thirteen attributes, not fourteen, precisely because HTML defines + `name` on these hosts. The whitelist this group uses is therefore the shared + SDUI one PLUS those two, not the bare `toDomProps` the container renderers take: + that would have stripped the form-serialization key off every control and + silently re-enabled every disabled one, and no gate in the repo would have gone + red for it. + + Nothing an author writes renders differently: every leaked key was already being + read off `schema` and applied. `id`, `role`, `tabIndex`, `className`, `style`, + event handlers and the open `data-*` / `aria-*` families are unchanged. + + Anything that read one of the leaked attributes off the DOM — a CSS attribute + selector such as `[label="Save"]`, or a test asserting `inputtype` on a rendered + `ui:input` — must read the schema instead. No `@object-ui` code did; this is + called out because the attributes were externally visible while they lasted. +- 485f096: The form renderer now keeps a `defaultValues` reset off `onChange` and off the + `form_change` `onAction` for **every** caller — including one that memoizes the + callback (objectui#5235). + + "A record landing is not a user edit" was already this file's documented, + pinned behaviour, but two of the three channels delivered it by accident of + React's effect ordering: every layout DESTROY runs before any layout CREATE, so + a caller passing a fresh callback each render had its value subscription torn + down before the reset and re-established after. The guarantee was therefore + delivered by the callback's *identity changing*. Wrap the same callback in + `React.useCallback` — taught everywhere as a semantically neutral performance + optimization — and the identity stays put, the effect never re-runs, the + subscription survives the reset, and the whole loaded record comes back to the + host as if the user had typed it: the false "the user edited this" signal + objectui#2968 was filed about, in a form no type, doc or call site warned about. + + The reset now states what those two channels report, the way `onDirtyChange` + already did (it computes its payload against the freshly installed baseline and + calls the host outright). Callers passing inline arrows see byte-identical + behaviour; callers who memoize stop receiving a phantom edit. + + Not a contract change: whether a value channel *should* report a programmatic + reset stays open in objectui#5235. This only removes the answer's dependence on + caller identity. +- b655a9d: The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer + land on the rendered `<div>` as invalid HTML attributes (objectui#4787). + + `grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and + `style`, so everything else `SchemaRenderer` hands a registered component reached the + element. Measured on a canary node, eight attributes leaked: + `columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, + `props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an + unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as + `columns="[object Object]"`. Layout was unaffected, so every catalog grid example + rendered with them — the reason this went unnoticed. + + The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist + objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the + SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, + `role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the + designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be + forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly + any more, including keys `GridSchema` does not have yet; enumerating today's keys to + strip would have re-rotted on the next schema addition. + + No authored input changes and no layout changes: the grid's own vocabulary was always + read off `schema`, never off these props. +- d7bd274: **Behaviour change:** the `flex`, `stack`, `container` and `text` renderers no + longer forward their whole prop bag to the host element. They route it through + `toDomProps` — the same whitelist `grid` was converged on — so an authored + schema key becomes an HTML attribute only if the SDUI DOM contract declares it + one (objectui#5574). + + What this stops reaching the DOM: the renderer's own declared props, which were + consumed off `schema` to build the class list AND spread onto the element a + second time as attributes HTML does not define. Measured across + `examples/schema-catalog`, rendered through the real `SchemaRenderer`: 1194 + illegitimate attributes over 1141 nodes — `text[content]` 522, `flex[align]` + 198, `flex[gap]` 193, `stack[gap]` 153, `flex[justify]` 98, `container[padding]` + 14, `container[maxwidth]` 6, `flex[direction]` 5, `stack[align]` 4, + `text[value]` 1. The same probe reads 0 after, with `grid`'s 26 nodes at 0 both + times as the control. + + Nothing an author writes renders differently: every leaked key was already being + read off `schema` and applied as a class, so the markup loses attributes that + never had meaning and keeps the styling that did. `id`, `role`, `tabIndex`, + `className`, `style`, event handlers and the open `data-*` / `aria-*` families + are unchanged — including `data-obj-id` / `data-obj-type`, which now arrive + through the `data-*` family rather than by hand. + + Anything that read one of the leaked attributes off the DOM — a CSS attribute + selector such as `[gap="4"]`, or a test asserting `align` on a rendered `flex` — + must read the schema or the class instead. No `@object-ui` code did; this is + called out because the attributes were externally visible while they lasted. +- 9d9040d: `ui:dropdown-menu` now resolves a menu item's authored `icon` to a glyph instead of drawing the name as raw text. + + Both arms of the item renderer — `DropdownMenuItem` and `DropdownMenuSubTrigger` — rendered `icon` straight into a text node, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew the literal word `copy` beside its label. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. + + The `components-overlay-dropdown-menu/with-icons` catalog fixture declared the retired lucide spelling `edit`, which is absent from lucide's runtime `icons` record and would therefore have drawn no glyph; it now declares `square-pen`, the live key the retired export resolves to by identity. +- ab92940: Stop the record header title from collapsing when the action tail is wide + + The `page:header` record header laid the title column and the action tail out + in a `flex-nowrap` row. The tail is `shrink-0`, so the title column — the only + flexible item — absorbed the entire width deficit. On a record carrying three + labelled `record_header` actions plus the `⋯` and `⟳` chrome, a 799px viewport + left the title column 29.5px of a 687px header and the `h1` rendered as a + single character and an ellipsis, while the breadcrumb above it still showed + the full record name. + + The title column now carries a 12rem floor and the header may wrap at `sm` and + up, so a tail that cannot fit alongside a readable title drops to its own line + instead of starving it. Below `sm` the header is already a column and is + unchanged. Headers whose tail already fits keep their single-line layout. +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. +- fa140b8: `element:record_picker`'s `emptyText` now resolves the inline per-locale map its + contract has admitted since rc.6, and its published declaration says so + (objectui#5590). + + `@objectstack/spec` widened this key to the `I18nLabel` union + (`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA + still carries it — measured, not assumed: + `ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })` + succeeds. The renderer honoured only the string arm, handing the map straight to a + text node. React refuses a plain object in a child position rather than stringifying + it, so an author writing the map form the contract accepts did not get a mis-rendered + empty state — the whole picker subtree threw + `Objects are not valid as a React child (found: object with keys {en, zh-CN})`. + + The read site now resolves through `pickLocalized`, the objectui-side helper the + sibling text-node sites already read through (`element:text.content`, + `element:button.label`, `page:card.title`), which spells a miss as `''` rather than + the spec resolver's `undefined`. The default is applied before resolution, so + `emptyText` absent still means "No records" and an authored empty string still + renders empty. + + The `ComponentMeta` entry, which held a single `'string'` arm precisely because the + renderer dropped the other one, now declares `['string', 'object']`. That narrowing + was correct for exactly as long as it was true: with the map arm reaching the screen + resolved, withholding it would be the false declaration in the other direction — the + manifest gate reporting `type-mismatch` on a legal write the same input's own + `description` teaches the author to make. The `apps/console` specimen that pinned the + narrow arm named this release condition in its own words ("keeps its single `'string'` + arm until the render site catches up") and is flipped here, keeping its controls. + + Three comments in the renderer deferred this gap to objectui#4163, which closed as + completed on 2026-08-15 while the gap was still open; the file now carries no + reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very + key as its worked example of an arm deliberately withheld, and is corrected in the + same change so the example stays true. +- 71cba28: `element:record_picker`'s `label` is now programmatically associated with its + `SelectTrigger` combobox — fixing a case where the caption the block's own + registration prose advertised (`'Caption rendered above the picker, in a + <label> element'`) named nothing at all (objectui#5771). + + Pre-fix, the render body was `{label && <label className="…">{label}</label>}` + against a `SelectTrigger` with no `id`: neither end of the association carried + any wiring. That is a step worse than the sibling gap objectui#5735 closed on + `element:text_input` — there the `label` half was already correctly wired and + only `description` was adrift; here the label element had no `htmlFor` and the + trigger had no `id`, so the picker had no accessible name unless a + `placeholder` happened to render text into the trigger's content. + + The fix follows the shape objectui#3341 already ruled on for the same defect + class (`InlineCreateRelated`'s `<label>`/`<Input>` pair) and objectui#5735 + just landed on the complement block: `htmlFor`/`id` against the + `SelectTrigger` — a `button` with `role="combobox"`, and therefore labelable, + so no `aria-labelledby` is needed (Radix sets none on the trigger) — wired + **when `schema.id` exists**, matching `element:text_input`'s precedent rather + than an always-on `useId()` fallback. Only the author can supply `schema.id`, + so a picker authored without one keeps its pre-fix, unassociated caption text + exactly as before; nothing about this change mints an id the author did not + provide. The `<label>` element itself is also swapped for the shared `Label` + (`@radix-ui/react-label`) primitive `element:text_input` already uses for the + same wiring, rather than a bare `<label>` with a `htmlFor` bolted on. + + The registration prose for `label` is corrected to describe what the code now + does (tied to the control by `htmlFor` when the node carries an `id`) instead + of a caption association the renderer never performed. +- 190fbd0: `element:record_picker`'s `label` and `placeholder` now resolve the inline + per-locale map their contract admits, and their published declarations say so + (objectui#5637). + + These are the two remaining keys of the trio objectui#5590 fixed one of. All + three members of `ElementRecordPickerPropsSchema` are the `I18nLabel` union + (`string | Record< string, string >`) — measured on the installed + `@objectstack/spec` 17.1.0 pin, where each resolves to + `optional -> union -> string | record` and + `safeParse({ object: 'account', <key>: { en, 'zh-CN' } })` succeeds. The + renderer honoured only the string arm on both, and the two keys failed in two + different ways: + + - `placeholder` was read raw and handed to `SelectValue`. React refuses a plain + object in that position rather than stringifying it, so the whole picker + subtree threw + `Objects are not valid as a React child (found: object with keys {en, zh-CN})` + — the same harm objectui#5590 removed from `emptyText`. + - `label` went through the file's local `toText`, whose object branch ends + `String(o.label ?? o.name ?? o.title ?? o.en ?? '')`. Reaching `o.en` + unconditionally is an English pick wearing locale resolution's clothes, so a + `zh-CN` viewer was shown the English entry — and a map that simply omits `en` + resolved to `''`, which the `{label && …}` render site drops, making the + picker's label element DISAPPEAR with nothing thrown and nothing logged. + + Both keys now resolve at their own read site through `pickLocalized`, the same + helper the settled `emptyText` shape uses. `toText` is deliberately unchanged: + it is shared with the row values (`toText(row?.[labelField])`), which are record + field values rather than `I18nLabel`, so teaching it locale resolution would + have changed a second, unrelated call site. The `placeholder` default is applied + before resolution, so an absent key still means "Select a record…" and an + authored empty string still renders empty. + + The two `ComponentMeta` entries, which held a single `'string'` arm precisely + because the renderer dropped the other one, now declare `['string', 'object']` + — declared in the change that makes the arm render, never before, which is the + order `ComponentInput.type` prescribes and the order `emptyText` set. + + KNOWN GAP, unchanged by this release: the sibling `label` read sites in + `renderers/layout/containers.tsx` compose + `translateLabel(pickLocalized(…), language)`, and that second helper is not + applied here — `translateLabel` and its `KNOWN_LABEL_DICT` are module-private to + that file. Only the locale-map resolution lands in this change; a plain-English + string `label` is still rendered verbatim in every language, exactly as before. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- 5a07e67: The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it + states what it actually measured (objectui#5399). + + When an action's `visible` predicate references a `record.<key>` the bound payload + does not carry, the warner names the missing key and then explained the cause: + + > Hidden (hidden: true) fields are stripped from detail payloads server-side, so a + > predicate gating on one may evaluate to a hide-by-default verdict. + + That cause is false, and it names a mechanism this repo does not own. `hidden` is a + UI concern — the framework spec describes it as "Hidden from default UI" + (`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against + the framework checkout rather than taken on trust: ObjectQL's own strip for the + `__search` companion documents that the `hidden` / `readonly` / `system` markers are + "None of them is a PROJECTION rule", which is precisely why a dedicated strip rule + had to be written for that one column; drivers answer a query with no `fields` using + `SELECT *`; and `metadata-protocol` enumerates what the read path does drop — + `internal: true` columns and the `__search` companion, and nothing else. The only two + read-side uses of `field.hidden` in the framework are auto-view/auto-form column + generation and companion-source eligibility, neither of which removes a key from a + record body. + + So an author who read this diagnostic went hunting for a `hidden` flag they would + either not find, or find on a field the payload demonstrably still returns — while + the real source of the sparseness (a projected or partial read) went unexamined. A + confidently wrong cause in a diagnostic is worse than no cause, because it is + actionable in the wrong direction. + + The replacement states the fact this surface can actually see and the consequence it + does own: the page bound a payload without those keys, a projected or partial read + will not carry them, and the predicate therefore fails closed and hides the action. + The measured half of the message — action name, missing fields, predicate source — + is unchanged, and nothing about what triggers the warning changed. + + Message text only. The same false claim also sat in this warner's own doc comments + and in the comments of the test that pins the message; both are corrected here, and + the docstring now carries an explicit note against re-attributing the cause to + `hidden: true`. No other call site was swept. +- 45d8288: fix(components): the `page` wrapper now filters its DOM attributes through the shared whitelist instead of a hand-maintained list + + `PageRenderer` stripped PageSchema's descriptor keys with a hand-maintained + destructure list and spread the remainder onto its wrapper `<div>`. Whenever + that list fell behind the schema, an authored key was not dropped — it was + forwarded, and React stringifies unknown attributes in silence, so an authored + `actions: [{…}, {…}]` reached the DOM as `actions="[object Object],[object + Object]"`. + + The renderer now calls `toDomProps` from `@object-ui/core` — the same + whitelist every converged SDUI widget already uses (objectui#4425). Twenty-five + attributes measured leaking off the wrapper stop being emitted, including the + `context` bag the console injects into every page it renders. Everything the + wrapper legitimately carries is unchanged: `class`, `style`, `id`, `role`, + `tabindex`, `data-page-type`, `data-obj-id`, `data-obj-type`, and the open + `data-*` / `aria-*` families. + + This changes no schema's accept/reject behaviour — `BaseSchema.passthrough()` + is untouched — and adds no read point for any previously leaking key. +- 27308c5: `element:text_input` now ties its authored `description` to the field with + `aria-describedby`, so assistive tech announces the helper text as the input's + accessible description instead of leaving it as an unassociated paragraph + beside the field (objectui#5735). + + Before this the paragraph and the input were siblings with no programmatic + relationship: a screen reader moving to the field announced the label and the + value and never the helper text. The `label` half of the same block was already + wired (`htmlFor` against the input's `id`), which is what made the gap specific + to `description` rather than a general absence of a11y wiring — and the + identical key authored on a field INSIDE `renderers/form/form.tsx` has been + announced all along, so one authoring key behaved two ways depending on which + container the author reached for. It no longer does. + + The paragraph's id is minted per instance with `React.useId()` — the same source + `FormItem` mints the form renderer's description id from — and deliberately not + derived from `schema.id`. The two associations in this block need ids on + opposite ends: `htmlFor` names the INPUT, whose id only the author can supply, + so that wiring still holds only when they gave the node an `id`; `aria-describedby` + names the PARAGRAPH, which the renderer owns, so the description association + holds unconditionally and cannot collide when two nodes share an authored id. + The attribute is emitted only when a paragraph is actually rendered — an absent + or empty `description` leaves the input with no `aria-describedby` rather than a + dangling reference. + + The key's published `ComponentInput` description, which documented this gap in + so many words, is rewritten in the same change. Its closing advice — prefer + `label` for an instruction a user must not miss — is kept rather than deleted, + on a new basis: a description is announced after the field's name and screen + readers gate description text behind verbosity settings a user can turn down, so + it remains the half of the announcement most likely to go unheard. +- 8689166: `element:text_input` now DECLARES the inline-translation arm on `label`, + `placeholder` and `description`, so the manifest gate stops reporting + `type-mismatch` on a locale map its own renderer has always resolved correctly + (objectui#5717). + + `@objectstack/spec` types all three keys as the `I18nLabel` union + (`string | Record<string, string>`) — measured on the installed 17.1.0 pin, per + key, from the schema's own verdicts — and `text-input.tsx` has resolved all + three through `pickLocalized` at their read sites since it was written. Only the + `ComponentMeta` entries stayed at a single `'string'` arm. Driven through the + same `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the + save gate use, an author writing `{ en: 'Owner', 'zh-CN': '负责人' }` got three + warnings on a write that renders correctly in the viewer's language: + + ``` + <element:text_input> prop "label" expected a string + <element:text_input> prop "placeholder" expected a string + <element:text_input> prop "description" expected a string + ``` + + That is the INVERSE of the direction the rest of this family moved in. + `element:record_picker.emptyText` (objectui#5590) and that block's `label` / + `placeholder` (objectui#5637) each held one arm for exactly as long as their + render site dropped the map, and gained the object arm in the change that taught + the render site to resolve it — never declare an arm the renderer drops. Here + the render site was never behind, so the same rule's other half applies: + withholding an arm the renderer resolves is the false declaration in the other + direction, and noise on legal writes trains authors (AI authors included) to + dismiss the `unknown-prop` and `type-mismatch` reports that are real. + + Per-key, not blanket. `defaultValue` on this same block keeps `['string', + 'number']` — its contract has no object arm, measured the same way — and the + console specimen file keeps asserting that an `I18N_MAP` there still reports + `type-mismatch`. That control is what makes this a widening of three keys rather + than of a component. + + All three keys also gain a `description` written from what the renderer does + rather than from restating the contract: where each one lands in the rendered + output (a `<label>` above the field, the native `placeholder` attribute inside + it, a `<p>` below it), when each is omitted, and the locale fallback chain the + read site follows. No renderer behaviour changed. +- 9101be5: `ui:breadcrumb` and `ui:command` now resolve a child item's authored `icon` to a glyph instead of drawing nothing. + + `BreadcrumbItem.icon` and `CommandItem.icon` are declared keys of the protocol — present in `@object-ui/types`, mirrored in the Zod schemas, and documented on both components' pages — and neither renderer referenced `icon` at all. The catalog fixture literally named `components-data-display-breadcrumb/with-icons` rendered no icons, and the nine names the two `command` fixtures declare all drew nothing. Both names are now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `action:*`, `ui:dropdown-menu` and `ui:context-menu` resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. + + `breadcrumb` resolves once per item and draws the glyph above the page/link split, so the last crumb (rendered as the current page) carries it as well as the linked crumbs before it. + + Two fixtures declared retired lucide spellings that are absent from the runtime `icons` record and would therefore have drawn nothing: `components-data-display-breadcrumb/with-icons` declared `layout`, now `panels-top-left`, and `components-form-command/command-menu` declared `smile`, now `face-slightly-smiling` — in both cases the live key the retired export resolves to by identity. + + `ui:button-group` is NOT included: its fixtures author `icon` on `buttons[]`, but `ButtonGroupButton` declares no such key in `@object-ui/types` or its Zod mirror, so wiring it would first mean widening the published item contract. Routed for a decision rather than chosen (objectui#5931). +- 57f9b07: `ui:text` now honours `TextSchema.variant` and `TextSchema.align` + + The nine published variants (`h1`-`h6`, `body`, `caption`, `overline`) each map to + a distinct typographic class, and `h1`-`h6` render the heading element they name; + the four published alignments (`left`, `center`, `right`, `justify`) reach the DOM + as their Tailwind class. The registration's `inputs` list declares both keys, so + the JSX-page prop whitelist stops reporting `unknown-prop` for them. + + The published enum is unchanged — the renderer catches up to it, and a spelling + outside the enum is still refused by `safeValidateSchema` rather than quietly + ignored. Behaviour note: a document that already authored `variant: 'h1'` and saw + no heading will now see one. That is the declared behaviour arriving. A node that + never authored `variant` renders exactly as before. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [d2fb6ef] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [0db4fb3] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [4703651] +- Updated dependencies [2c1c967] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [46b9bc9] +- Updated dependencies [dbd5194] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [8d58f46] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [305205a] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [f53a8d0] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/sdui-parser@17.7.0 + - @object-ui/react-runtime@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/components/package.json b/packages/components/package.json index 97f1edb4f7..4e3a0cf466 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/components", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Standard UI component library for Object UI, built with Shadcn UI + Tailwind CSS", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 8da3d58842..b33d3306b4 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,2169 @@ # @object-ui/core +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- 053fdc8: Console half of `ActionSchema.onSuccess` post-success navigation. + + `@objectstack/spec` declares `onSuccess` as a closed strict object + `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to `type: 'api'` and + `type: 'script'` — the two action types whose success event carries a server response. + Nothing in this renderer read it, so an action declaring the hop navigated nowhere: the + block fell into `ActionRunner`'s older `ActionDef.onSuccess` chained-callback channel, + was dispatched as an action, and failed inside `executeNavigation` with "No URL provided + for navigation action" — a red toast and no jump. The motivating report is a clone action + that leaves the user sitting on the record they cloned from. + + `ActionRunner.handlePostExecution` now performs the declared hop through + `navigationHandler` — the same SPA seam every other navigator in that file uses, which + the console wires to react-router's `navigate`, so `openIn: 'self'` is a real in-place + route hop rather than a full-page load. `interpolateTarget` gains a `${result.*}` scope + alongside `${param.*}` and `${ctx.*}`, resolved against the handler's own return value + (the level `readActionPayload` reads, one below the action envelope) and supplied only by + this call site, so a target interpolated before its request still has no `result` to + name. `openIn` is read as the one member that changes the branch and no default is + written here — the spec materialises `.default('self')`, so parse output always carries a + resolved member — and the two `openIn` spellings stay apart: this reads + `onSuccess.openIn` (`'self' | 'newTab'`), never the top-level `type: 'url'` switch + (`'self' | 'new-tab'`), each of which spec refuses in the other's position. + + The console's server-action wrapper gains the matching handler-return half: a handler may + now return `openIn: 'self'` next to its `redirectUrl` to ask for the same-tab jump, while + a `redirectUrl` **without** `openIn` keeps its shipped new-tab behaviour unchanged. When + an action declares an `onSuccess` block, the wrapper defers to the runner and only tidies + its pre-opened tab, so one navigation happens rather than two. + + The pre-existing `ActionDef.onSuccess` chained-callback channel is unchanged. It is told + apart by the spec's own declaration — a non-array object whose `navigate` is a string — + and keeps running for every other shape. +- 490d9a9: Grid headers offer a sort click only on columns the PLATFORM says it will order by + (objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23: + the platform serves an explicit per-column sortability signal and the grid reads it, + rather than re-deriving "virtual ⇒ unsortable" from field type). + + `GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its + ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve + time from the platform's own storage predicates, deliberately beside `item` rather than + inside it so the key stays un-authorable. The signal was reaching the browser and being + discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps + the envelope to `item`, so every UI reader saw a document with no signal on it. It now + survives that unwrap, carried on the schema under a symbol key — invisible to + `JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata + write endpoint can never take it into a body the server parses strictly. + + `@object-ui/core` gains the one spelling of the consumer contract: + `isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and + says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown + name, a dotted path and an unprovisioned audit column, all three of which the runtime + doors reject — so the `!== false` spelling every other optional flag in this repo uses + would get exactly that family backwards. A projection that is absent ALTOGETHER is a + different question with a different answer (`undefined`: no signal was served) and is + typed apart from an empty one, so a deployment older than the upstream change keeps the + behaviour it had rather than being told, falsely, that nothing on the object is sortable. + + Three things follow in the grid. The header click on a refused column ceases to exist, so + neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is + reachable from it. A sort PERSISTED before the signal existed is filtered out of both what + the grid renders and what it emits, so a restored personalization cannot ride back into + the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the + PUT still fires. And the relational carve-out is untouched and deliberately not delegated + to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored + foreign key and both runtime doors accept ordering by it), while the grid withholds that + header for a different reason — a column of names ordered by an invisible id. + + Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts + those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced + drift in mirror image. +- 2c3cd1b: BREAKING (`@object-ui/core`): `ActionRunner`'s legacy `ActionDef.onSuccess` + chained-callback channel is retired — `onSuccess` now has exactly the meaning the + contract declares (objectui#5934, maintainer ruling 2026-08-31). + + (The bump is `minor` by this repo's release model — objectui's major is pinned to + the `@objectstack` family major, and its own breaking changes ship as `minor` with + the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This + paragraph is that spelling-out: the break below is real and consumer-visible.) + + - **What breaks, by specifier**: `import type { ActionDef } from '@object-ui/core'` — + `ActionDef['onSuccess']` was `ActionDef | ActionDef[]` (chained callbacks the runner + dispatched through `executeChain` after a success). It is now derived from the pinned + spec: `ActionSchema.onSuccess`'s closed strict `{ navigate: string, openIn?: 'self' | + 'newTab' }` block. Code that assigned a callback `ActionDef` (or an array of them) to + `onSuccess` no longer compiles, and at runtime a callback-shaped value gets NO reading — + no handler dispatch, no navigation, the action's own result untouched. `onFailure` is NOT + changed: the spec declares no such key, so it keeps its one runner-native meaning. + - **Why this is safe to take**: the channel was unreachable from validated metadata — + `@objectstack/spec` (17.2.0 pin) strict-refuses a callback shape inside `onSuccess` at + parse (`invalid_type` on `navigate` + `unrecognized_keys`), so no published/saved + metadata could ever carry one — and a producer census with a positive control found zero + producers outside the channel's own test pins. Migration for an out-of-repo consumer that + drove the channel programmatically: put the follow-up actions in `chain` (the runner's + declared chaining key, unchanged), or author the spec's `onSuccess` navigation block. + - `@object-ui/types` (minor): `UIActionSchema` now declares `onSuccess`, derived from the + spec's `ActionSchema.onSuccess` — the renderer view spells the key the four action + surfaces forward, so the forwards type-check. + - `@object-ui/components` (patch): the four action renderers forward `onSuccess` without + the `as any` casts (no behavior change — same key, same value, now typed). +- 44d075b: `ComponentMeta` at the registry is now DERIVED from the one declaration in + `@object-ui/types` instead of restating it, and `tags` / `description` reach the + registration surface (objectui#6067). + + ## The convergence + + `packages/core/src/registry/Registry.ts` declared its own `ComponentMeta`: thirteen + keys, of which nine were restated from `@object-ui/types`' `base.ts`, four were + registry-only (`tier`, `namespace`, `skipFallback`, `labelling`), and `tags` / + `description` were **absent** — although both are declared on the canonical type and on + the `ComponentMetaSchema` zod mirror. Two of the three authorities agreed and the + registration surface did not, so those two keys were unwritable at exactly the + declaration most component registrations import. That is the same two-key delta + objectui#5893 had just closed inside `@object-ui/types`, arriving a third time on a + third declaration, and objectui#5671 had already made the identical move for the sibling + type `ComponentInput` in this very file. + + It is now: + + ```ts + export type RegistryComponentMetaExtras = { + tier?: 'public' | 'internal'; + namespace?: string; + skipFallback?: boolean; + labelling?: 'control' | 'group' | 'display'; + }; + + export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras; + ``` + + `RegistryComponentMetaExtras` is newly exported from `@object-ui/core`. + + **What changes for a consumer: `tags` and `description` become writable on the registry's + `ComponentMeta`. Nothing narrows.** No key is removed, no key is renamed, and no key's + type changes, so no existing registration stops compiling — verified by type-checking all + 37 workspace consumers of `@object-ui/core` (`pnpm --filter '...@object-ui/core'`), which + is why this is a widening rather than the contract break a rename would have been. All + four registry-only keys have live consumers, and they are still declared here. + + This is `minor` under this repository's policy that its own breaking changes never declare + `major` (`scripts/check-changeset-no-major.mjs`); nothing here is breaking in any case. + + ## Converge rather than rename, and why the four keys did not move + + The alternative dispositions were to rename the type so the name stops claiming a mirror, + or to move the four registry keys onto `@object-ui/types`' `ComponentMeta` and re-export + it outright the way objectui#5671 handled `ComponentInput`. + + Renaming was rejected because it cannot be done without a break: `@object-ui/core` is + published, `ComponentMeta` is exported from it, and dropping the name would break every + external consumer — while keeping it as an alias would leave the mirror claim standing + under a second spelling, which fixes nothing. + + Moving the four keys was rejected because `skipFallback` and `namespace` are registration + mechanics — they describe how the registry keys an entry, not what a component is — and + `@object-ui/types`' `ComponentMeta` is the general, plugin-facing, AI-facing type. The + extension keeps them where they are read, under their own named type, while the eleven + shared members exist in exactly one place and can no longer drift. + + ## Pinned by key set, not by assignability + + Every member of both shapes is optional, so `extends` is mutually **true** across the + diverged pair — an assignability assertion is green on the defect and would not have + caught it. Measured on the emitted `.d.ts` of both packages, before and after: + + | reading | before | after | + |---|---|---| + | `Core extends Canonical` | `true` | `true` | + | `Canonical extends Core` | `true` | `true` | + | `Exclude<keyof Canonical, keyof Core>` | `"tags" \| "description"` | `never` | + | `Exclude<keyof Core, keyof Canonical>` | the four registry keys | the four registry keys | + + The new pin asserts the third row and names the fourth explicitly; the assignability pair + is kept beside it, labelled, as the control that shows what it cannot see. A source-level + assertion that the canonical members are not restated locally covers the remaining failure + mode — a member-identical copy, which every `keyof` comparison stays green on and which is + how the copy this replaces began. +- 8f1d995: `ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it + + `@object-ui/types` and `@object-ui/core` each published a declaration of + `ComponentConfig`, so an auto-import picked between two different types by + alphabetical order. After the `ComponentMeta` convergence the remaining + difference was genericity and the `component` slot: `@object-ui/types`' was + non-generic with `component: any`, core's was `<T = any>` with + `component: ComponentRenderer<T>`. + + `@object-ui/types`' declaration gains that type parameter, **defaulted**, so + every existing spelling keeps its meaning exactly — bare `ComponentConfig` is + `ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core` + re-exports it instead of declaring its own. + + The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`, + `deprecated`) were not dropped: they moved to a named extension, + `RegistryComponentConfig`, which is what `Registry.getConfig`, + `getAllConfigs` and `getNamespaceComponents` return. Those return values are + type-identical to what they returned before, so every read path is unchanged. + + **Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core` + *and* touches one of those five registry-only keys through that annotation must + switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig` + no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's + versioning policy: objectui's own breaking changes ship as `minor` with the break + spelled out here, because the whole publishable set is one changeset `fixed` group + pinned to `@objectstack`'s major. +- 7977ff9: Component deprecation is now DECLARED, not just warned about (objectui#6674). + + A deprecated component type used to be stated in exactly two places, neither of + which a gate, a test or a type can consult: a `console.warn` string literal + inside the renderer, and the word "(Deprecated)" inside a human-readable + `label`. Both gates that touch component types ask a different question — + whether the type RESOLVES — and a deprecated type resolves, which is how one + could be authored 85 times across 27 shipped exemplars with every check green. + + - `@object-ui/core` gains `ComponentDeprecation` / `AuthoringSurface` and the + `deprecated` key on the registration metadata, plus + `ComponentRegistry.deprecationFor(type, surface)` to read it back. The + declaration carries the SURFACES it applies to rather than being a boolean: + `div` and `span` are deprecated on the JSON authoring surface and are at the + same time permanent vocabulary of the `kind:'html'` tier, so a bare flag would + be false for one of its two readers. + - `@object-ui/components` marks `div` and `span` with the declaration their + console notices already state. Nothing new is deprecated and no build starts + failing: the catalog ratchet keeps the existing stock frozen, and draining it + stays objectui#3965's worklist. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts + (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer + ruling B, 2026-09-02). + + A dataset-bound chart's aggregate axis / legend title read the analytics + service's hard-coded English `Count` on a zh console whose category labels were + already Chinese. The renderer was passing `fields[].label` through verbatim — + correctly, for an author-declared measure (objectui#4106) — and had no way to + tell the server's built-in default apart from an author's label. + + The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural + discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | + 'count_distinct'`, populated only on the server-side built-in defaults + (objectstack#14492). This change is the consumer side of that contract: + + - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — + `ChartResultField` plus the optional `builtinAggregate` carrier + (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than + on it because the spec this release is built against does not carry the key + yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / + `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries + the locale strings in (core stays React-free and i18n-free — the same + division as `nullCategoryLabel`). A field carrying a recognised + discriminator resolves through that map; every other field keeps its wire + `label` verbatim — never by matching the label's text or the field's name + (the rejected option A). + - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings + through the existing `report.aggregate.*` keys (zh already carried 计数 / + 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). + - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), + `plugin-report` (`DatasetReportRenderer`): pass the resolved map to + `buildChartSeries`. + + Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same + chart titled `计数`; an `en` session still reads `Count`; an author-labelled + measure (`Tasks`) and a measure literally named `count` without the + discriminator are byte-for-byte unchanged. Until the upstream field is + populated the wire carries no discriminator and every chart renders exactly as + before. +- 81a2eb1: One home for the `datetime` display convention (objectui#7443). + + `formatDateTime` gains a named `'compact'` style, selected through + `options.style` — the dense grid face, `7/4/2024 7:00 am` in `en-US` — which + `DateTimeCellRenderer` used to build from its own inlined `Intl` option bags. + The cell now reads `field.format` (it destructured `value` only, so a + `datetime` field could not reach the style vocabulary a `date` field has) and + renders through the shared function, and `data-table`'s `formatCellValue` + calls `formatDateTime` instead of a third, independently authored option bag. + Every existing cell without an authored `format`, and every cell authoring + `'compact'`, renders byte-identically; `'compact'` is today's face named and + rehoused, not a new one. A `datetime` field that authors any OTHER non-empty + `format` does change: the cell previously ignored `field` altogether and always + painted the compact face, and now anything other than `'compact'` selects the + verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in + `en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An + unrecognised value is neither rejected nor passed through; it silently lands on + that verbose face. No `datetime` field in this repository authors a `format`, so + no cell here moves — a consumer that authored one is the case this sentence is + for. Note that `format` has no declared value vocabulary to check a value + against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec` + carries one free-form `format?: string` on its shared field schema, described + "Format string (e.g. email, phone)" and accepting any string. `'compact'` is + therefore the only value with a defined `datetime` meaning, and every other + value means "the verbose face" by fallthrough rather than by design. + + Additive, no signature change: `formatDateTime(value, options?)` is unchanged + and `formatDateTime(v, { locale })` keeps meaning what it meant. + `DateDisplayOptions` gains an optional `style` key (read by `formatDateTime` + only; `formatDate` still takes its style positionally), and + `formatDateTimeCompactParts` is a new export of `@object-ui/core`, re-exported + by `@object-ui/fields`, returning the compact face as the two halves a grid + cell paints separately. `@object-ui/components` changes no rendered output — + the table's datetime cell is measured identical before and after in `en-US`, + `zh` and `de-DE`. +- 00d2fa6: ListView: fold `data={{ provider: 'object', object }}` onto `objectName`, and read the + author's view kind from `specType` / `type` (objectui#7477 — step 6 of #2890, released + by the maintainer's ruling B on objectstack#14791, 2026-09-03). + + **What was broken.** A react page bound the way the published `react-blocks` contract + recommends — + + ```jsx + <ListView data={{ provider: 'object', object: 'crm_task' }} type="kanban" /> + ``` + + — validated green against `@objectstack/spec` and then rendered an **empty grid** with no + diagnostic. Both halves of that binding were inert in the renderer: `ListView` read + `data.provider === 'object'` at zero sites (`'value'` and `'api'` are both live there, so + the gap was real and not a dead instrument), and it read `specType` — the slot the react + page tier parks an author's `type` in, because the SDUI envelope claims the `type` key + (ADR-0078) — at zero sites, so an absent `viewType` forced the view to `grid`. + + **What changed.** `normalizeListViewSchema` (`@object-ui/core`) gains two folds. Per + AGENTS.md #0.1 they live in the one documented normalizer — not as a seventh per-block + copy of the six sibling `data.object` reads, and not as a renderer-side `??` dual-read. + + - `data: { provider: 'object', object }` → `objectName`. The `object` provider is a + `strictObject` carrying exactly `{ provider, object }`, so `objectName` captures all of + it. Two deliberate departures from the folds around it, both narrowing: an + already-present `objectName` **wins** (the fold only fills a gap and can never re-point + a binding that already resolves), and `data` is **not** deleted — it has four + providers, `api`/`value` are read live, and the block is forwarded to child views whose + own `getDataConfig` reads `data` before `objectName`. + - the author's view kind is read from `specType`, then from a bare `type` when it names a + kind ListView draws (the component discriminator `'list-view'` never does) — the same + two legs, in the same order, as `normalizeChartSchema`'s chart-family read. An explicit + `viewType` still wins; this only fills the gap that used to resolve to `grid`, and a + kind ListView does not draw is left to that `grid` default rather than written through. + + **Accept behaviour widens.** Metadata that previously had no effect now binds a view: a + list view carrying an `object` data source, or an author `type`, renders differently + after this change than before. Nothing that renders today renders differently. No + authored spelling is removed here — `objectName` / `viewType` remain accepted; their + retirement is objectstack's, after this ships. +- e62c44e: Re-home the breakpoint layout vocabulary and delete the two dead responsive + implementations (objectui#7580, maintainer ruling 2026-09-04, option A). + + **Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole + `ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, + `BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the + stated ground that the four types "had no other authorable carrier". That ground is + measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component + whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by + `resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in + `@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and + when a renderer implements it" — is already met here, so the two types a renderer reads + are re-homed rather than retired. + + What survives, under the same names and the same members: + + - `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead + of re-exported from the spec. **No consumer change**: same name, same six members, same + export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. + - `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), + verbatim from the retired `$strict` schema: six optional column counts, no index + signature. `responsive-grid`'s `columns` input and its resolver are unchanged. + + What is removed: + + - `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had + no read point in the package; it was published only because the retired + `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` + needed the type. With the schema gone there is no order vocabulary for it to be the type + of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. + - `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and + `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with + `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / + `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired + `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). + - `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once + the two implementations above went, dropped rather than re-declared locally, the same + disposition the retired i18n names in that file already carry. + + No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, + `ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and + `responsive-grid` itself — are untouched. + + **Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` + objectui#7580: the retirement is merged upstream and unreleased, so this must land first. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- d7573b3: `ComponentInput` is now declared once and re-exported, instead of restated in three + places (objectui#4972). + + `@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' + plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) + were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now + re-exports of that one declaration, which is the disposition objectui#4580 ruled for the + identical shape — *a structural copy would reproduce the defect the moment either side + moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. + + Either side had already moved. `base.ts` declared thirteen keys; both copies declared + nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every + component registration actually imports**. Those four keys were unwritable at any real + registration — a plain TypeScript error at the call site — while `ComponentInputSchema` + (the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face + advertised four keys the authoring face rejected. Measured over the repository, no + registration had tried to write one yet, so nothing a user hits was broken today; what + changes is that the four keys become writable, and there is no longer a second + declaration for the next widening to miss. + + `ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single + declaration imported by all three sites (objectui#3832); this converges the rest of the + interface. + + Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is + byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported + here only with its control — a probe that added a *required* key to `ComponentInput` left + the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of + `export *` lines that names `ComponentInput` zero times. The gauge that can actually fail + is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does + `@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted + declarations that change in either package. + + `WidgetInput`'s union-arm capability is deliberately untouched — a different gate path + and a separate judgment. +- bf3edfe: `ComponentRendererProps` is now declared once and re-exported, instead of + hand-declared a second time in `@object-ui/core` (objectui#4594). + + `@object-ui/core`'s `ComponentRendererProps` (`src/types/index.ts`) was a + non-generic interface typing `schema` as `SchemaNode`, while + `@object-ui/types`' declaration of the same name is generic — + `ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` with + `schema: TSchema`. Same name, both exported from their package entry, from two + packages the same consumers import together: which declaration a call site got + depended on which package it reached for, and the two disagree about whether a + primitive node is admissible. Core's is now a re-export of types', which is the + disposition objectui#4580 ruled for `SchemaNode` two lines above it in the same + file, and objectui#4972 for `ComponentInput` — *a structural copy would + reproduce the defect the moment either side moved*. + + **Published-surface effect, and the reason it is not neutral.** Resolved + through the TypeScript checker from `core/dist/index.d.ts` over a clean rebuild + of both legs, `ComponentRendererProps` as reached through `@object-ui/core` + moves from non-generic with + `schema: BaseSchema | string | number | boolean | null | undefined` to + `ComponentRendererProps<TSchema>` with `schema: TSchema`, defaulting to + `BaseSchema`. `schema` therefore **narrows** back to the object form — core's + copy had silently widened when objectui#4608 made core's `SchemaNode` a + re-export of types' union — and the type gains a parameter. **Nothing imported + it**, on either side, re-verified repo-wide on the merged ref, so no call site + can observe either move; the narrowing is recorded here because it is a change + to a published type, not because a consumer is affected. + + A compile-time pin now holds the reconciliation from + `@object-ui/react` — the only position that resolves both packages through + `node_modules` — alongside the existing `SchemaNode` one. It is a test-only + addition and emits nothing, so `@object-ui/react` takes no bump of its own. +- 5aed9e4: `DataScopeManager` now **denies** a row when a scope rule names something that is not the record's own data, and when an ordered comparison would only succeed by coercing one of its two sides. It used to **admit** those rows. + + Two fail-opens on a row-level permission boundary, both measured against the previous release's source, both the same silent direction as objectui#7378 — a result set that is too large, with no error and no console line, which looks exactly like a correctly configured permissive scope. + + **The field a rule names is now read as an own member of the record, or not read at all.** `{ field: 'constructor', operator: 'ne', value: anything }` returned the ENTIRE dataset: the name resolved on the prototype chain, `Function !== 'x'` is true, and every row passed the rule that existed to hide it. The read now has three cases instead of one. A name in the refused list (`__proto__`, `constructor`, `prototype`) denies. An own member is read as before. A name that is not an own member but still resolves on the record's prototype chain — `toString`, `valueOf`, `hasOwnProperty`, or a field inherited from an `Object.create` parent — denies, because the value exists but is not this record's data. A name that resolves nowhere is a genuinely absent field and still reads as `undefined`, so the ordinary "this row has no `status`" rules keep every verdict they have always had. + + That third case is deliberately stricter than `evaluateCondition` in `@object-ui/permissions`, which this card was filed to converge with. Reading with `hasOwnProperty` alone — the sibling's shape — collapses "inherited" into "absent", and absent ADMITS on a negative operator, so the sibling still returns `true` for `{ field: 'toString', operator: 'neq' }` on every record (filed as objectui#8044). Converging on the sibling's exact lines would have closed three spellings and left the class open, and would itself have widened one case: an inherited field value flips from denied to admitted under `ne`. Distinguishing inherited from absent closes the class and keeps the change a narrowing everywhere. + + **Ordered comparisons (`gt` / `gte` / `lt` / `lte`) now require both sides to be the same comparable kind.** `{ field: 'age', operator: 'gte', value: 0 }` admitted records whose `age` was `null`, `'10'`, `true`, `false`, `''` or `[]` — every one of them through a coercion to a number that the rule's author never wrote. Both sides must now be numbers, or both strings, or both `Date`s. + + Same KIND, not "both numbers". The sibling requires `typeof === 'number'` on both sides; copying that predicate would have denied every row for `{ field: 'created', operator: 'gte', value: '2023-01-01' }`, since ISO date strings, plain string ranges and `Date` objects all order correctly on this evaluator today and none of those comparisons coerces anything. The hazard is cross-kind comparison, so cross-kind is what is refused. + + **`contains` now requires its rule value to be a string** rather than calling `String()` on it, so `{ operator: 'contains', value: 1 }` no longer matches the record value `'10'`. Same unwritten coercion as the ordered arms; the sibling already refused it. + + **The narrowing, named plainly for anyone upgrading with rules already stored.** A legitimate rule loses rows in exactly three shapes. A numeric rule (`age gte 18`) over a dataset where numbers arrive as strings — from JSON, a CSV import, an unparsed form field — stops matching those records; `'20'` was admitted by coercion and is now denied, and the fix is to parse the field at the producer rather than to widen the rule. A rule reading a field that records inherit from a shared prototype rather than own stops matching. And a `contains` rule written with a non-string value stops matching. Measured over a 2772-case differential matrix of value kinds, operators and record shapes: 352 verdicts narrowed, **zero widened**, and zero change to the genuinely-absent-field family. + + Operator SPELLING is untouched, deliberately: `ne` / `nin` here versus `neq` / `not_in` in the sibling, and the sibling's `is_null` / `is_not_null` which this evaluator does not implement, remain exactly as they were. That divergence is objectui#7750's question. + + Graded `minor` because a release reader can observe the narrowing on stored data; no declared type changed and the set of inputs the evaluator accepts has not widened. +- 83c77dc: `DataScopeManager` now **denies** a row when a row-level scope rule carries an operator its evaluator does not implement. It used to **admit** the row. + + Behaviour change on a permission boundary, stated plainly. `evaluateFilter` implements nine operator spellings — `eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `contains` — and its `default` arm returned `true`, so a stored `RowLevelFilter` carrying any other spelling passed every record the rule existed to hide, silently: no error, no console line, only a result set that was too large, which looks exactly like a correctly configured permissive scope. The arm now returns `false`, the answer `evaluateCondition` in `@object-ui/permissions` already gives from its own `default` arm. Because `applyFilters` ANDs a scope's rules, one unrecognised rule now denies every row in that scope. + + Who this reaches, measured on this release's base rather than assumed. The `RowLevelFilter['operator']` union is closed, so no TypeScript caller can write an unimplemented spelling, and no code in this repository constructs a `RowLevelFilter` outside the evaluator's own test. The path that changes is scope configuration read back from stored or hand-written JSON and handed to `setFilters` / `registerScopeWithConfig`, where the operator arrives as a plain string the type never checked. A deployment holding such a rule with a spelling outside the nine — including the spec's canonical `equals` / `not_equals` / `greater_than` / `starts_with` and the null-ness family `is_null` / `is_not_null`, none of which have an arm — sees fewer rows from that scope after upgrading, never more. Those spellings are not implemented here; they are refused instead of admitted. Whether to canonicalise them through the spec's `canonicalAstOperator` is left open on objectui#7378. + + Graded `minor` because a release reader can observe the narrowing on stored data; the declared type is unchanged and the set of spellings the evaluator accepts has not widened. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- 105f3c5: Retire `CRUDSchema` and the `type: 'crud'` node spelling (objectui#5373, + maintainer ruling 2026-08-20, route 2) under ADR-0049 enforce-or-remove. + + `crud` had four declaration faces and no registered renderer, for the whole + life of the key: the TS interface (`packages/types/src/crud.ts`), the zod + mirror (`packages/types/src/zod/crud.zod.ts`), a dedicated branch in + `validateSchema` that affirmatively PASSED it, and `CRUDBuilder` in + `@object-ui/core`. A node spelling it painted the OBJUI-001 "Unknown component + type" panel, and `content/docs/api/schema-reference.md` published it as + reference material — so a reader (or an AI author) who copied the page got a + red panel. + + Removed from `@object-ui/types`: the `CRUDSchema` interface and its zod + mirror, the four shapes that existed only to type its keys — `CRUDOperation`, + `CRUDFilter`, `CRUDToolbar`, `CRUDPagination` and their zod mirrors and + `…SchemaType` aliases — and `CRUDSchema` as a member of `CRUDComponentSchema`, + which is what took it off the node union `AnySchema`. `ActionSchema`, + `DetailSchema` and `CRUDDialogSchema` are unchanged and remain the union's + members. + + Removed from `@object-ui/core`: `CRUDBuilder` and the `crud()` factory. + + Authoring `crud` is now REFUSED BY NAME rather than passed or silently + ignored. `validateSchema` returns an `error` with `code: 'RETIRED_TYPE'` on + `schema.type` — at any depth, since it is what `validateChildren` recurses + with — so `assertValidSchema` throws and `isValidSchema` answers `false`. The + message names the migration: `object-grid` for the record table with its + toolbar, filters, pagination and row/batch actions, `object-form` for the + create/edit form, and `detail` for the record view. `api/schema-reference.md` + is rewritten around those shapes. + + Note on blast radius: the repository itself contains zero authored `crud` + nodes and zero registrations of the key (measured on the merge base against + the doc gate's own 659-key registry derivation, which reads `register` and + `registerLazy` alike). That is an IN-REPO zero, not an npm zero — a published + consumer that imported the `CRUDSchema` type, called `crud()` / `CRUDBuilder`, + or authored `type: 'crud'` will see a compile error or a validation error + respectively. Both are the intended, loud replacement for a shape that has + never rendered. +- 83fe6e7: row predicates on runtime record surfaces resolve `record.*` only; the bare-field and `data.*` spellings are no longer bound + + Phase 2 of the row-predicate canon (objectui#5330, ruled 2026-08-20, option B; + Phase 2 ruled 2026-09-02 and amended 2026-09-05 on objectui#5741). Until now a + row predicate — `visible` / `disabled` / `enabled` on an action renderer, a row + action, a `record:alert`, a `page:header` action, a conditional-formatting + `condition` — bound the row three ways: canonical `record.status`, bare + `status`, and `data.status`. The two non-canonical spellings are retired on + every runtime record surface, in both evaluation tiers (`evalRowPredicate` / + `partitionRowsByPredicate` in `@object-ui/core`; `usePredicateRecordContext` + + `useCondition` in `@object-ui/react`) and for both dialects: a legacy + `${data.x}` / `${x}` string on a row surface retires with the CEL spellings. + + **What a retired spelling does now: it faults, exactly as it already did on the + server** (`buildScope({ record })` mounts exactly `['record']`, so `status` and + `data` are unknown variables there), and each surface applies its EXISTING + fault policy — no runtime detector, no "treat as absent" special case, no + uniform override: + + - `evalRowPredicate` / `partitionRowsByPredicate` (row kebab, selection bar, + `page:header` actions, conditional formatting): the caller's `fallback` — + hidden / every row excluded / no style — reported once by the existing fault + warning, which names the unknown variable (`Unknown variable: status`) and, + on the fast route, carries the `record.` hint. + - `useCondition` legs that opt into `throwOnError` (`action:button` and + `action:menu` `visible`, `DeclaredActionsBar` `visible`): fail-closed — + hidden on every row, reported once as `was hidden/disabled: its predicate + threw — status is not defined`. + - the non-throwing `useCondition` legs (`action:icon` / `action:group` + `visible`, every `disabled` / `enabled`, `record:alert`): fail-soft — shown / + greyed / enabled on every row, with the evaluator's own console line. + - a host scope that carries its OWN `data` (app-shell's ambient `data: {}`) is + left standing: `data.*` on a record surface then reads the host's object — a + constant, silent `false` — which is what "no longer bound to the row" means. + + The Phase-1 deprecation warning is removed with the bindings: + `warnNonCanonicalRowSpelling` and `resetRowPredicateCanonWarnings` are no + longer exported from `@object-ui/core`. `detectNonCanonicalRowSpelling`, + `ROW_PREDICATE_CANONICAL_ROOT` and the `NonCanonicalRowSpelling` type stay + exported — the offline instrument for sweeping authored metadata. + + The layer rule is unchanged: `data` remains the canonical root on + metadata-editing surfaces (ADR-0089 D3, `CANONICAL_ROOT_BY_LAYER`), and + app-shell's metadata-admin `SchemaForm` / `predicate.ts` keep binding + `{ data: row }` through their own evaluator. + + No stored-metadata survey, export or migration rewrite was run (the maintainer + ruled the stored population out of scope, 「不考虑存量」); the Phase-1 warning + period was the notice. + + Release note: Phase 1 (PR #5737 — the canon statement plus the warning) shipped + in `@object-ui/core@17.6.0` (npm, 2026-08-24) although its changeset + `.changeset/row-predicate-record-canon-5330.md` is still pending on `main`, so + the next CHANGELOG section lists Phase 1 and this Phase 2 together: the warning + it describes was live from 17.6.0 and is gone from this release on. +- d1ab06f: Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now + warn once, and are unchanged otherwise. + + A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row + scope, a `record:alert`) has bound the row three ways since objectui#4075 — + `record.status`, bare `status`, and `data.status` — without any of them being + declared the contract. The maintainer ruled that question on 2026-08-20 + (objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for + the identical renderer-tolerance shape: **the canon is `record.*`**, and the + other two enter a deprecation window. + + The canon states the **server's** accept set, which was this card's first + measurement and turns out to be strictly narrower than the renderer's. Measured + against `@objectstack/formula@17.1.0`, the engine the server evaluates with: + + | spelling | server runtime | server authoring oracle | + |---|---|---| + | `record.status` | `{ ok: true, value: true }` | accepted | + | bare `status` | `Unknown variable: status` | refused | + | `data.status` | `Unknown variable: data` | **silently accepted** | + + `buildScope({ record })` mounts exactly `['record']` — `data` is never bound and + the row's fields are never flattened to top level. The three-way binding is a + client tolerance with no server counterpart, which is why the warning belongs on + this side. + + `data.*` is the dangerous one, and the reason the warning exists. `data` is in + `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle + waves it through — that list is a deliberately generous "never faults" lint + baseline, not the runtime accept set. A `data.*` row predicate therefore passes + every authoring gate the platform has and then binds nothing at runtime: not an + error, a constant `false`. A `visible` that is constantly false is a button that + silently never appears — the objectui#4075 fail-closed signature. + + What ships: + + - `@object-ui/core` exports `detectNonCanonicalRowSpelling`, + `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and + `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, + which carries the canon statement and the measurement. + - Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and + `useCondition` (react, for bags bound by `usePredicateRecordContext`). + - Detection reuses the server's own oracles (`collectCelRootIdentifiers`, + `firstUndeclaredReference`) rather than a regex, so no second dialect + judgement is invented client-side. + + **No spelling is removed and no behaviour changes.** Every predicate that + resolved before resolves now — the ruling defers removal behind a stored-metadata + survey, and the warning is what makes that survey possible (ADR-0078: a + tolerance nothing ever reports can never be retired). + + The deprecation is scoped to the **runtime record layer**. `data` remains the + canonical root one layer over, in a metadata-editing form (ADR-0089 D3 + `CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 9801765: The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen` + per selected record (objectui#4420). It used to read that key as a bare boolean — the + object-level verdict only — so ticking a record the author's predicate excludes still + offered the red Delete, and pressing it deleted the record the predicate was written to + protect. The row kebab on the same screen hid its Delete correctly, so one declared key + meant two different things on two surfaces. + + Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the + operation and report the skipped**. The bar evaluates the predicate once per selected + record, the delete runs over the allowed subset, and the excluded records are reported + rather than silently dropped. The button itself is never hidden or disabled by the + predicate — a mixed selection is not punished for one stray tick — and a selection where + every row is excluded is a legible refusal rather than an unexplained absence. + + - `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of + `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once. + A bulk gate evaluates N records in a loop, which is why it can never be a hook. + - `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through + `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped + count; a selection with nothing excluded keeps the consumer's own delete flow + untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` — + the bulk half of the same predicates, gated on the object verdict rather than on the + row `onDelete` wiring. The dialog declines to run over zero records. + - `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters + the built-in `delete` to the eligible subset and states the skipped count inline. + + Custom bulk action ids are untouched: they route through the action runner carrying + their own gates. This is a UI affordance — server enforcement was never the leak. +- 546ddf7: A node-gate visibility predicate that FAULTS now says so in a production build, once per + distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the + silence is no longer an accepted property"). Observability only — no verdict moves. + + `SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated + resolves to the same answer as one that said yes, so a gate that stops biting looks + exactly like a gate the author got right. The diagnostic that names it (objectui#5454 / + objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection + channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition` + implements that by evaluating **twice** — too expensive to ship for every predicate of + every node. + + **What production actually printed before, measured per dialect on the built evaluator** + — the card's premise held for one dialect of three, and the other two failed in opposite + directions: + + | dialect | production console, before | + |---|---| + | bare string | **nothing** | + | `{ dialect: 'cel' }` envelope | one generic line, deduped per source | + | `${…}` template | one generic line **per evaluation**, never deduped | + + So the dialect objectstack#11254 measured a live gate breaking on was the silent one, + while the template dialect was the console flood the ruling's rate-limit clause exists to + prevent. + + **The fix reports the fault the evaluator already detected, at the same number of engine + calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s + `ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the + canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer + up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason + passback), and supplying it transfers reporting to the caller so one fault stays one + line. Pinned: the CEL branch performs the same number of record reads with the passback + as without it, and strictly fewer than the `throwOnError` probe. + + `SchemaRenderer` passes it in production and reports through the **same** reporter the dev + branch uses — same message, same severity, same dedupe `Set`, same key. Development and + production now print the identical line for the identical fault; the `__DEV__` gate no + longer decides *whether* a fault is reported, only *how* it is detected. + + `page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same + reporter and the same rate limit. It swallowed the identical fault under a different + helper, and it was the worse of the two: the node gate at least reported in development, + while a faulting item predicate was silent in *both* builds on a gate whose false verdict + removes an entire tab, header and panel. + + **Rate limit:** deduped per (node type, gate key, predicate source) — never per render and + never per node instance. A two-hundred-row list of one broken predicate is one line; a + second distinct predicate source still gets its own line. Both halves are pinned, because + a test that asserts only "a warning was emitted" is equally green on an implementation + that emitted fifty, and one that asserts only "exactly one" is equally green on an + implementation that suppresses everything. + + **Not changed by this card, deliberately:** the fail-open semantics themselves; the + objectui#5687 adapter-only `data.*` report, which stays development-only under its own + 2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against + the wrong object); and the `/forms/:name` scope wiring of objectui#6262, which lands in + its own PR. + + `reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`, + `UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported + from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one + reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one + authored predicate would be entitled to one line per package instead of one line. +- a26b9e4: `packages/core/src/adapters/README.md` now documents the adapters that are actually in that + directory, and the ObjectStack material it carried moved to the package that owns the behaviour + (objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a + README rides every tarball — so this was published documentation describing the wrong package. + + The page had been left behind when the ObjectStack adapter moved out to + `@object-ui/data-objectstack`: its headings, feature list, filter-operator table and + query-parameter table were all about that adapter, and its one-entry "Available Adapters" list + told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`. + `ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and + `emulateBatchTransaction` — the five exports that directory really ships — were named nowhere. + + - **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a + usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the + ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was + always about this directory. + - **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the + filter-operator and query-parameter mapping tables, the AST conversion example and the sorting + example. That material existed **only** in the `core` copy — this package's README documented + query translation as a single feature bullet — so it is ported, not dropped. + + No runtime behaviour changes; the duplicate copy of one package's documentation living under + another package is what goes away. +- 3c2b6f7: Two deprecation warnings pointed at `MIGRATION_GUIDE.md`, a file deleted from the + repository in `8c5d20455` (objectui#6342). + + `Registry.register()`'s missing-namespace warning now points at the live docs page + that documents namespaced registration + (`/docs/guide/plugin-development#namespaced-registration`) instead of the deleted + guide. `ValidationEngine`'s function-based-condition warning drops its `See:` line + entirely: the deleted guide covered component namespaces and lazy field + registration and never documented conditions at all, so that pointer was + misdirected as well as dead, and the warning already carries the complete + before/after migration inline. + + Both are console messages shipped to application developers, so neither can use + the immutable `git show <sha>^:<path>` provenance form objectui#6275 used for a + docblock — a reader of the npm package has no repository to run it against. +- 5961030: `@object-ui/core` and `@object-ui/data-objectstack` now declare + `"@objectstack/spec": "^17.2.0"` rather than `^17.0.0`, which is the lowest published + spec that carries every symbol each package's own build output references + (objectui#6361). + + `packages/core/dist/utils/column-sortability.d.ts` references + `FIELD_SORTABLE_UNPROVISIONED_ANCHOR`, `FIELD_UNSORTABLE_VIRTUAL_TYPE`, + `FieldSortability` and `ObjectSortability` from `@objectstack/spec/api`, and + `packages/data-objectstack/dist/index.js` references the first two — none of which + `@objectstack/spec@17.0.0` exports. Measured against the published tarballs rather than + the installed tree, by `scripts/check-spec-range-floors.mjs`: six `floor-too-low` + findings across the two packages, and `^17.2.0` is that gate's own computed answer for + both. So the old range was a claim neither package could honour: any consumer + resolution that lands 17.0.0 — a sibling pinning it exactly, an `overrides` entry, a + mirror two minors behind — satisfied `^17.0.0` and got a dangling reference. + + Nothing a consumer installs today changes: normal resolution already picks the newest + 17.x, and `pnpm-lock.yaml` still resolves `17.2.0` on both edges after the bump — only + the recorded `specifier:` moves. No source and no behaviour changes, which is why this + is scored `patch`, on the reasoning objectui#5793 used for the same remediation on + `@object-ui/plugin-detail`. + + The bump is release-blocking rather than cosmetic. `check:spec-floors` is deliberately + not a `pull_request` job, so every PR stayed green while its blocking copy on the + publish path — `pnpm changeset:publish` runs it before a single tarball reaches npm — + would have cancelled the next release. +- 299102e: `ExpressionEvaluator.evaluate` now reports a faulting `${…}` at most **once per authored + source** instead of once per evaluation (objectui#6444). It is the hottest of the three + predicate paths in this area — `SchemaRenderer` calls it for every `properties.*` value, + every `props.*` value and `content`, for every node, on every render — so a single broken + `${…}` prop in a 200-row list wrote 200 console lines per render, and 200 more on the next + one. Measured on the built evaluator before the fix: three identical faulting + `evaluateCondition` calls produced 3 lines where the `{ dialect: 'cel' }` envelope produced + 1; the 200-row list produced 200. After: 1 in every case. + + This is the one-per-source rate limit both sibling reporters already carry + (`warnPredicateFailure` in `fieldRules.ts`, `visibilityDiagnostic.ts` in `@object-ui/react`), + not a third mechanism. The dedupe key is the predicate's **authoring** identity — the fault + site plus the source text, never the scope it ran against — which is both the siblings' + precedent and the defect itself: the 200-row flood is one authored source evaluated against + 200 distinct scopes, so a scope-sensitive key would emit all 200 lines again. + + Nothing else moves. The two message texts are unchanged, a distinct broken source still gets + its own line, `EvaluationOptions.onFault` still fires on every fault (objectui#6038's passback + contract, so a caller doing its own warn-once bookkeeping keeps control of it), `throwOnError` + still throws on every evaluation, and no symbol is added to the published surface. +- 831be72: Dev-mode `validateSchema` no longer reports every expression-valued `visible` / `disabled` + gate as an invalid schema (objectui#6505). `BASE_SCHEMA_RULES` declared both keys + `typeof value === 'boolean'`, so the exact authoring form the docs teach — + `{ "type": "button", "disabled": "${record.stage == 'closed'}" }` — printed + `disabled must be a boolean` and its host element got `data-obj-schema-invalid`, the cue + apps are told to hang a red outline off. + + **The accept set widens to what the protocol already declares and the runtime already + accepts, not beyond it.** `AGENTS.md` §4 declares both keys as expressions, `SchemaRenderer` + evaluates them through `hasDeclaredPredicate` + `evaluateCondition`, `@objectstack/spec` + normalizes every authored predicate into a `{ dialect, source }` envelope, and the + objectui#3862 / objectui#3955 rulings are entirely about which expression spellings count + as declared. This table was the one place in the repo that disagreed, so this restores + declared = enforced rather than changing a contract. + + **The rule still bites, and that half is pinned separately.** The two keys stay in + `BASE_SCHEMA_RULES`: a number, `null`, `{}`, an array, `''`, whitespace-only predicate text + and the empty / blank-`source` envelope (objectui#3960) are all still reported at their own + path with `INVALID_TYPE`. Every one of those was reported before this change too — the + accept set is a strict superset of the old one, so nothing that validated stops validating + and nothing refused becomes accepted. The message now names both halves of the accept set + instead of only the half that did not change. + + The verdict is delegated to `hasDeclaredPredicate` (`evaluator/declaredPredicate.ts`), the + repo's single definition of "is a predicate gate declared on this value?" + (objectui#3850's ruling), rather than answered a second time in the validator — a + hand-rolled twin that agrees today and drifts tomorrow is the defect class this rule was + already an instance of. `packages/core/src/validation/__tests__/predicate-valued-gate-rules.test.ts` + pins the delegation behaviourally: the rule's verdict must equal + `boolean || hasDeclaredPredicate(value)` across every probe in the file. + + The explicit boolean arm is kept even though `hasDeclaredPredicate` already subsumes it, so + the superset relationship is provable locally: a future narrowing on the declaredness side + cannot silently start reporting `disabled: false` — the most explicit gate an author can + write — as an invalid schema. + + The zod `safeValidateSchema` surface (`@object-ui/types/zod`, objectui#6318) is a different + validator and is untouched. +- d0889e2: Resolve a relationship target from `reference` only — the spec spelling + (objectui#6528). + + `resolveReferenceTo` (dataset designer) and its sibling + `resolveRelationshipTarget` (`chart-series.ts`) each read a relationship field's + target through a four-spelling tolerant chain — `reference ?? reference_to ?? + referenceTo ?? reference_to_object`. Measured against every producer that can + reach them, three of the four are unfounded, so the chain is narrowed to + `reference` in BOTH places in one pass (they must not diverge — a fix leaving + them disagreeing recreates the defect one file over). + + The census, with `reference` itself as the positive control every zero is + measured against: + + | spelling | `ObjectSchema.safeParse` (spec 17.2.0) | producers on the object-metadata surface | + |---|---|---| + | `reference` | ACCEPTED | live — both designer writers emit it; 445 of 565 lookup/master_detail defs in the framework tree | + | `reference_to` | REFUSED BY NAME | 0 (live only on ObjectUI's own view/field schema — a different contract) | + | `referenceTo` | REFUSED BY NAME | 0 (producers retired by objectui#6041; stripped by the read door since objectui#6519) | + | `reference_to_object` | REFUSED (not even an alias) | 0 anywhere in either tree, outside the chain and its own test | + + Behaviour change, and it is deliberate: `chart-series.ts` reads + `GET /meta/object/:name` directly, with no read door stripping retired keys, so + a stored pre-objectui#6041 row spelling the target `referenceTo` no longer + resolves there. The walk is best-effort by construction — no entry is yielded + and the caller keeps the raw value — so such a row degrades visibly instead of + being silently absorbed. Per AGENTS.md #0.1 that row is a producer-side defect, + and a lenient consumer is where it would have stayed hidden. `reference` was + already head of the old chain, so any document carrying both is unaffected. + + The string / array / `{ object }` carriers are untouched: the carrier is a + separate axis from the spelling and narrowing it needs its own census. +- 4d5f9b4: Resolve a relationship target from a `reference` STRING only — the carrier axis + (objectui#6648). + + objectui#6528 narrowed both relationship-target resolvers to the single spec + SPELLING `reference` and left the CARRIER — the shape the value may take — + explicitly for its own census. That census is done, and it says the same thing: + `resolveReferenceTo` (dataset designer) and its sibling + `resolveRelationshipTarget` (`chart-series.ts`) each accepted three carriers on + the canonical key, two of which `FieldSchema` never declared. Both are removed + in BOTH files in one pass (they must not diverge — a fix leaving them + disagreeing recreates the defect one file over). + + The measurement, with the bare string as the positive control every zero is + measured against: + + | carrier | `ObjectSchema.safeParse` (spec 17.2.0) | producers at the field-def key position | + |---|---|---| + | `reference: 'crm_account'` | ACCEPTED | live — 587 across both trees | + | `reference: ['crm_account']` | REFUSED — `expected string, received array` | 0 | + | `reference: { object: 'crm_account' }` | REFUSED — `expected string, received object` | 0 | + + The census walked STRUCTURE, not text: JSON/YAML parsed and walked, TS/TSX read + through the TypeScript compiler API, each hit recorded with its ancestor + property chain and its enclosing object's sibling keys so a FIELD DEF is + separated from the other tiers that also spell `reference` (a form field + literally named `reference`, its translation entries, a JSON-Schema property + descriptor, a liveness-ledger row). Every dynamic initializer at the field-def + position resolved to a string-typed source, and every `reference` TYPE + declaration in either tree declares `string`. The detector is not blind to the + shape it hunted — it DID report array and `{ object }` carriers, and every one + was a test asserting this very tolerance plus one framework lint fixture whose + own rule already reads string-only. + + The array branch was also a silent PRODUCT decision: handed a multi-target + value it returned element zero and DISCARDED the rest. Nothing declares such a + value. Polymorphic lookup is an open, unbuilt gap in the spec's own audit report + ("Current `reference` only supports a single target", Tier 3), and the + platform's one polymorphic reference (ADR-0018 `xRef`) is a STRING with a + sibling discriminator, never a list. A multi-target lookup, if it lands, lands + as a declared spec shape — not as a carrier a consumer guesses at. + + Behaviour change, and it is deliberate: a field def whose `reference` is not a + non-empty string now resolves to `undefined` in both helpers. Such a document is + already refused by `ObjectSchema`, so per AGENTS.md #0.1 it is a producer-side + defect, and a lenient consumer is exactly where it would have stayed hidden. The + two unit assertions that pinned the tolerant reads are converted to refusal + pins, so re-widening the carrier turns red. +- 9e37d9b: `binding` on a component input is framework-set, not author-declared: `@object-ui/types` gains `InjectedComponentInput`, the `'field'` binding arm is retired from `@object-ui/sdui-parser`'s `RegistryConfigLike`, and the `as ComponentMeta` cast at the injection seam in `@object-ui/core` is gone (objectui#6950; maintainer ruling of 2026-09-07, director decision batch #69; ADR-0049 enforce-or-remove). + + **What was measured.** `binding` was published — the manifest serializer forwards it — and read — `validateTree` records a binding site for it — while `ComponentInput`, the authoring type every registration writes against, did not declare it. The one writer in the tree, `ELEMENT_DATA_SOURCE_INPUT`, therefore carried a hand-written inline type and reached a registration's `inputs` through `as ComponentMeta` in `Registry.register`. Declared narrower than enforced, on a published type — and `ComponentInput`'s own docblock listed `binding` among the forwarded per-input keys. + + **The ruling** answered the product question the card asked — may an ordinary registration declare a binding input? — with no. So: + + - **`@object-ui/types`** exports `InjectedComponentInput`, an `interface … extends ComponentInput` with the required marker `binding: 'object'`. `ComponentInput` itself does not change: no member is added, and authoring `binding` on a registration stays an excess-property `tsc` error — now on purpose and documented at the interface. The two tombstone docblocks that listed `binding` as a forwarded key now say it is forwarded from the framework's injected input, not authored. + - **`@object-ui/core`** types `ELEMENT_DATA_SOURCE_INPUT` as `InjectedComponentInput` and splices it through a typed local; the cast is gone. Runtime behaviour is unchanged — the same key, `type`, `binding` and `description` reach the manifest, and `validateTree` still records the binding site. + - **`@object-ui/sdui-parser`** narrows `RegistryConfigLike.inputs[].binding` from `'object' | 'field'` to `'object'`. The `'field'` arm had zero writers — every `binding:` literal in `packages/`, `apps/` and `examples/` is `'object'`, 7 of 7 at this change's merge-base — and nothing on either side of the manifest resolved a field binding. **Breaking, deliberately:** a config that feeds `manifestFromConfigs` a `binding: 'field'` input is now a type error instead of a manifest entry the server would never resolve. `ManifestInput.binding`, the manifest reader's vocabulary, is not narrowed by this change. + + If a real need for author-declared bindings is ever measured, it is filed as a widening of `ComponentInput` with the vocabulary decided then — not by putting the cast back. +- 48c19bd: Render a dataset measure over a date field as a date (objectui#7178, maintainer + ruling 2026-09-02, director summon #8 — option A). + + `formatMeasure` opened with `if (typeof v !== 'number') return String(v)`, + placed **before** `format` was ever read. So a `min` / `max` measure over a date + or datetime field printed its stored value verbatim — a 24-character ISO string + in the KPI tile's `text-2xl font-semibold`, wrapping to two lines — and the + `format` that `DatasetMeasureSchema` accepts was unreachable for those values. + A date-shaped value now routes to the date display path before that + short-circuit, so all four dataset-bound surfaces are served at once: the metric + tile, chart values, dataset table cells, and the metadata-admin dataset preview. + + `min` / `max` over a date stays a legal measure; nothing in `@objectstack/spec` + narrows. `PivotTable` takes a `number` outright and is unchanged. + + **No second date formatter was written.** `formatDate`, `formatDateTime`, + `formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`' + barrel down into `@object-ui/core` (`utils/date-display.ts`), which is the same + remedy objectui#4576 applied to `formatDisplayNumber` and for the same reason: + `core` is the React-free engine and could not import from a React package, so + the alternative was a parallel date convention in `dataset-format.ts` — exactly + the drift that once had a list cell rendering `1.234,5 %` beside a dashboard + measure's `1.234,5%`. `@object-ui/fields` re-exports all four names unchanged, + so no consumer's import path or behaviour changes, and a reference-identity test + pins that the cell renderer and the measure formatter call the same function. + + **What `format` can say for a date measure, measured rather than assumed.** The + shared date path takes a named STYLE, not a date pattern: `'short'` and + `'relative'` are honoured — the same words `DateCellRenderer` honours from + `field.format` — while a pattern such as `'YYYY-MM-DD'` renders the locale + default. That limit is unchanged by this release (`plugin-dashboard`'s + `recordFields` already routed a date-shaped `format` into the same style slot) + and is now pinned by a test instead of being silent. + + **Numeric measures are byte-identical.** 33,696 argument forms + (value × format × currency × percentScale × locale) were compared against a + verbatim copy of the pre-fix function: the only values that moved were the four + ISO-shaped, parseable ones. Numbers, numeric strings (`'1751612400000'`, + `'2026'`), the nullish em dash, arbitrary prose and non-strings all render + exactly as before. +- a6d8b8d: Fix: a grid grouped by a field it does not also show as a column no longer collapses + every row into one `(empty)` group (objectui#7179). + + `$select` was built from the view's `columns` and nothing else, so a view declaring + `grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns + never asked the server for that field. It was `undefined` on every row by the time + grouping ran, and the grouping label builder — correctly, for a genuinely empty value — + answered `(empty)` for all of them. The result was one collapsible group holding every + record, with no error, no warning and no empty state: a grid that looked like it grouped + and did not, reading as "these records have no value for this field". + + The grouping fields are now unioned into the projection, at both places it is built — + `ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the + rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that + fetches a bare foreign key without populating it buckets by raw id instead of by name, + which is a different wrong answer rather than a fix. + + Authors do not need to mirror a grouping field in `columns` any more. That was never + required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of + it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their + `groupByField` with no column needed. Refusing the configuration at author time was + considered and rejected: it would make the grid the odd one out and reject working + intent that the schema explicitly allows. + + The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]` + entry carries a bare string that has never been through column validation, and some + backends answer an unknown `$select` key with an empty result set rather than ignoring + it. Unioned unguarded, a grouping field naming something the object does not declare + would have turned this bug into a strictly worse one — no rows at all, equally silently. + Grouping fields are therefore intersected with the object's declared fields and passed + through the same field-level-security gate as columns and predicate operands before they + reach the query. +- e75f4c9: `colorField` now means the same thing in the gantt, the calendar and the timeline + (objectui#7243). + + **The inversion this fixes.** `gantt.colorField` is documented as "field that drives the + bar color", and the renderer passed the stored value straight into the bar's + `backgroundColor`. Pointing the key at a select field therefore emitted + `backgroundColor: "open"` — not a colour, so the browser dropped the declaration and + every bar rendered identically. OMITTING the key was strictly better: the absent-key + branch derived a real colour per status. Declaring the documented key was worse than not + declaring it, with no error, warning or console message either way. + + The same key also meant three different things across the three lenses: the timeline + resolved the field's authored option `color`, the calendar hashed the raw value onto a + fixed palette, and the gantt emitted the raw value. An author colouring three views by + one field got three unrelated results, one of which was no colour at all. + + **The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's + resolver, lifted so all three call it: + + 1. the field's own option `color` for the record's value; + 2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`, + `#rrggbbaa`, `rgb(...)`, `hsl(...)`); + 3. else each renderer's own last rung, which is deliberately NOT shared — the gantt + derives a semantic-token hex (a bar must be painted), the calendar keeps its + theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its + default marker. + + **What changes for authors.** A gantt or calendar whose `colorField` points at a select + field with authored option colours now paints those colours. A gantt value that is + neither an option colour nor a colour literal now derives a colour instead of emitting + an invalid CSS value — including a palette NAME (`red`), which now resolves to that + palette's hex, the behaviour the key's own contract has always promised ("hex or + semantic name") and the one `borderColorField` already had. `gantt.borderColorField` + takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps + today's behaviour otherwise and deliberately gains no derivation rung, since the stroke + is opt-in and deriving one for every record would draw an alert on records that have + none. + + Calendars whose `colorField` points at a plain categorical field are unchanged: that + value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline + is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar + already accepted. +- 19f1639: A synthesized default list view now always leads with the object's name field + (objectui#7245). + + **The defect.** An object that declares no list view gets its default grid columns + synthesized from `highlightFields`, taken verbatim. But `highlightFields` is ADR-0085's + *"most important fields"* role, not a column list — and its first consumer, the + detail-page highlight strip, **deliberately removes the title field**, because the page + H1 directly above it already shows one. So metadata that is entirely correct routinely + omits the record's name from `highlightFields`. The showcase `showcase_account` declares + `nameField: "name"` and `highlightFields: ["status", "industry", "annual_revenue"]`, and + its default `所有记录` grid rendered 14 rows whose columns were `#` / Lifecycle / Industry + / Annual Revenue / actions — no name column, and no way to tell one account from another. + + A list has no H1 to lean on, so the same declaration needs the opposite treatment here. + This is not a new convention: `deriveLookupColumns` in `@object-ui/fields` already leads + its record-picker columns with the display field and filters it out of the declared list. + The list faces now agree with it. + + **What changed.** `@object-ui/core` gains two exports on the ADR-0079 title ladder: + + - `resolveNameField(objectDef)` — *which field* titles an object: the declared + `nameField` (then its deprecated `displayNameField` / `NAME_FIELD_KEY` aliases), else + the type-aware derivation. The name-space twin of `getRecordDisplayName`, which answers + what that field *says* on one record. Both now read one spelling of the declared + pointer, so they cannot drift into naming different fields. + - `leadWithNameField(objectDef, columns)` — moves that field to the front of a + **synthesized** column list. + + All three faces that synthesize default list columns call it: `ObjectView` + (`defaultListColumnsFromObject`), `InterfaceListPage` (`defaultColumnsFromObject`) and + `ObjectGrid`'s own derivation. The name field is **moved**, not merely appended, so an + author who lists it third still gets it first — "the column that identifies the row" + means first. On the two capped faces the lead is applied *before* the 5 / 6-column slice, + so an object declaring its name field late no longer loses it off the end. + + **Scope, deliberately narrow.** Author-declared column lists are untouched — a view or + grid that declares `columns` / `fields` said what it wants, and reordering it would be + renderer-side second-guessing of metadata. Three cases also decline to lead: a name field + the object carries no field def for (never fabricate a column), one marked + `hidden: true` (the author said don't show it), and a *derived* pick that lands on a + system-managed column — `deriveTitleField` filters by type only, and leading a default + list with a raw id is the regression objectui#2702 / #2777 fixed. A *declared* + `nameField` pointing at a system field still leads: `sys_migration` really does point at + `id`, and an explicit designation is not a heuristic misfire. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 52cac38: `formatDate` reads `options.style` (objectui#7745). + + `DateDisplayOptions` is the one bag `formatDate` / `formatRelativeDate` / + `formatDateTime` share. `style` was added to it for `formatDateTime`'s `'compact'` + grid face (objectui#7443, PR #7621) and only `formatDateTime` read it, so on + `formatDate` the key was inert — and inert beside a POSITIONAL parameter of the same + name. `formatDate(v, undefined, { style: 'short', locale: 'en-US' })` rendered + `Jul 4, 2024`, the default face, with no diagnostic; it now renders `Jul 4, '24`. + This is the additive half of the maintainer's long-run ruling on objectui#7443: + both functions accepting `options.style`. + + **The precedence is pinned: the positional argument wins.** `options.style` is + consulted only when the positional slot is `undefined` (`??`, not `||`, so `''` + still counts as given). That is the only direction that is purely additive — it + fires exactly on the input that is a silent no-op today, so no call that renders a + face today renders a different one after. The reverse would let a key aimed at a + SIBLING function outrank an argument written for this call: the bag is shared, and + carrying `{ style: 'compact', locale }` built for `formatDateTime` into + `formatDate(v, 'short', bag)` must not cost that call its short face. + + **What changes for you.** Only `formatDate(value, undefined, { style: 'short' | 'relative' })` — + a call that silently rendered the default face before. Every call that passes the + style positionally, and every `formatDateTime` / `formatRelativeDate` call, renders + byte-identically to before. + + `formatRelativeDate` still does NOT read `style`; the ruling names `formatDate` + only. Its out-of-window fallback to `formatDate` strips the key so that the new read + cannot leak in through the delegation — which also keeps + `formatRelativeDate(v, { style: 'relative' })` from recursing. +- fc32921: Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing + (objectui#8266). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `field` — the normal way to author "how many records per status" — rendered an + empty chart. No error, no empty state: a plot frame with the category ticks drawn and + not one mark in it, which reads exactly like "this object has no rows yet". + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + built the series binding as `aggregate?.field || (options.yField || 'value')`, which for + a fieldless count resolves to `'value'`. The rows an object-bound fieldless count + returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned + since framework#3701. A `dataKey` naming a column no row carries plots nothing, and + neither of the renderer's two guards fires on it: the rows DO carry the category key, + and the series array is not empty. + + **Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to + `chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the value column an object-bound aggregate produces". Both relays now consult it, and + the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed + through the same function, so the two halves of the question cannot drift again. + + **What moves on screen.** A chart that was blank now draws. Charts that already drew are + unaffected: a field-bearing aggregate resolves to its raw field under both the old and + the new reading, and a chart with no `aggregate` at all keeps the author's `yField`. + One authored key changes meaning: a `yField` written on an object-bound chart that + ALSO declares an aggregate no longer wins over the aggregate's own column — it named a + record column that a grouped aggregate never returns, so it plotted nothing before. + + **Not fixed here, and out of scope.** The same widget with no `options.xField` is + refused by the category-axis guard naming `name`, a key the author never wrote (they + wrote `aggregate.groupBy`). That is the category half of the same relay gap and is + filed separately. +- 8f9d87a: Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being + refused for lacking a `name` column (objectui#8269). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `options.xField` rendered a refusal instead of a chart: + + > This chart cannot plot its category axis: no row has a `name` field. + + The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that + had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent + them to debug the wrong layer. + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + floored the category binding on a literal — `options.xField || 'name'` — and handed it to + the `object-chart` node without ever consulting the aggregate that decides it. An + object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no + row carried `name` and the category-axis guard (framework#4033) fired correctly on a + binding that was already wrong when it arrived. + + **Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to + `chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the category column an object-bound aggregate produces", and the published sibling of the + `chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now + consult it for the object-provider branch. + + **What moves on screen.** A widget that rendered a refusal now draws. Measured through + `ChartRenderer` at 480x320 over the rows a fieldless count returns + (`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from + `xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`, + 1 series and 2 marks with the category ticks drawn. + + **Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows + are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it + returns a single row with no category column), and the authored-literal-rows branch — the + `chart` node composed after the object-provider check fails — keeps its floor unchanged. + One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField` + written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the + aggregate's own column — it named a record column a grouped aggregate never returns, so it + produced the same refusal before. +- 81c0bc4: The dev-mode unknown-key warning stops stating a fact that was retired, and + sends the author to the file that actually declares the interface + (objectui#5642). + + Both halves of the message's tail had outlived the change they described. It + told the author the key was warned about rather than rejected because + `ActionDef` "still carries `[key: string]: any`" — objectstack#4075 step 3 + deleted that index signature, and `actionKeys.pin.test.ts` pins the deletion in + the opposite direction (`{ ActionDef: false, ActionContext: true }`), while + `actionDef-closed-surface.test.ts` pins that `tsc` now rejects exactly such a + key at the construction site. And it prescribed promoting the key to an explicit + field on `ActionDef` "(packages/core/src/actions/actionKeys.ts)" — that file + holds the INVENTORY (`ACTION_DEF_KEYS`); the interface is in `ActionRunner.ts`. + The wrong pointer had teeth: an author who followed it edited the inventory + alone, which is precisely the half-change the pin test reddens on, since it + re-derives the inventory from the interface's AST. + + The tail now carries the reason the module's own header already gives for why + this warning survived step 3 — the two mechanisms cover disjoint populations. + `tsc` sees action literals authored in code; the warning sees actions that + arrive as data, from stored rows that are rehydrated unparsed and that no + compiler ever looked at (objectstack#3903). The prescription names + `ActionRunner.ts` for the field and `ACTION_DEF_KEYS` as the same-commit second + edit, saying why. + + No behaviour change: the classification logic, the key inventory and its + derivation are untouched, and the warning fires on exactly the same actions as + before. Two comments in the same file and two in the pin test that described the + pre-step-3 world were refreshed in the same pass, and the message text is now + pinned — the printed interface path is resolved off the message and read, so a + move or rename reddens by name instead of shipping a second dead prescription. +- 8ebd57f: Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction` + JSDoc are now qualified, so each resolves to the entity it means (objectui#5580). + + `ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method + on `ExpressionEvaluator` (bare expression, throws) and the module-level export + (context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block + referred to both under the one spelling, four lines apart. + + The prose link was not merely ambiguous, it was bound wrong. Measured with + `checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}` + resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the + sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}` + binds to the method, but only because no module-level `evaluate` exists to outrank + it, so the rule "an unqualified link resolves to the enclosing class's member" does + not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`, + which the checker resolves to the `MethodDeclaration`. + + The `@example`'s final line is the module-level export — its second parameter is a + context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat + two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover + carries no import to disambiguate. It now names the module-level export and shows the + import it needs. + + This is prose only: the diff is confined to a block comment and no declaration moves. + It is scored `patch` rather than the empty-frontmatter form because the block is + emitted into what npm ships — measured, this edit moves both + `dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js` + (this package builds with a bare `tsc`, which preserves comments in the JS emit), and + the ten changed lines in that JS are all comment lines. + + `registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather + than asserting it in prose, since a `{@link}` that binds to the wrong entity is + indistinguishable in source from one that binds right. +- 7138bc1: The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's + privileged-override safety copy (objectui#5611). + + `ActionRunner.execute` classifies the object it was HANDED, and a console host + hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes + `overrideNotice` on that dispatch and two param-collection handlers read it — + yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a + key two files read as one "no reader recognizes", and prescribed promoting it to + an explicit field on `ActionDef`. That prescription is the one shape the + 2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic + walked an author into a rejected design. A false warning on the product's own + privileged path — the branch that finalises an approval over approvers who have + not acted — is how a dev console gets muted. + + Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to + `actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the + fourth input to that set and the first one that is not an authored-surface + mirror. Measured before and after on the exact dispatch the bar composes: the + warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS` + grew by exactly one member, and an action carrying a real typo alongside it + still warns — naming `targt` only. + + The authored surface does not move. `overrideNotice` is still NOT declared on + `ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal + remains a compile error, and the AST-derived pin over the interface is unchanged. + Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what + an author may write — `actionKeys.pin.test.ts` now pins both halves, including + the new list's exact contents so a second member cannot arrive quietly. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- af3861f: `normalizeListViewSchema` now folds the four per-view-type config aliases phase 3 + carried over — `kanban.groupField` → `groupByField`, `kanban.cardFields` → + `columns`, `gallery.imageField` → `coverField`, `timeline.dateField` → + `startDateField` (objectui#2890). + + These are the pre-#2231 objectui spellings, kept declared alongside the spec keys + so stored view metadata would keep validating. They now fold at the same + component boundary as the A1–A5 vocabulary folds, in the same one-directional + shape: the canonical key wins when a config carries both, and the legacy key is + removed from the result so a missed read-site fails loudly instead of quietly + taking the legacy path. + + One rendering behaviour changes, and it is a correction of the same inverted + precedence the `densityMode` fold fixed: `ListView`'s kanban adapter resolves the + card field list as `cardFields || columns` — legacy over canonical — so a kanban + config carrying **both** rendered the legacy `cardFields` value and silently + ignored the spec-canonical `columns`. After the fold the authored `columns` is + what reaches it. Configs carrying only one of the two are unaffected, and every + other reader of these four keys was already canonical-first. + + `calendar.defaultView` is deliberately **not** folded: it aliases nothing and has + no spec counterpart, so it wants promotion upstream rather than a rename. +- c00bf28: `getRecordDisplayName`: stop consulting the undeclared object-level + `titleField`, restoring `nameField` as the top of the object ladder + + Step 0 of the unified record-title resolver read + `options?.titleField ?? objectDef?.titleField`. The second leg ranked an + object-level `titleField` above `nameField` — the pointer ADR-0079 Phase 2 made + canonical — and above the deprecated `displayNameField` alias and the legacy + `titleFormat` template. + + `@objectstack/spec`'s object schema does not declare that key, and it is not + merely undeclared: the schema is a `strictObject`, so + `ObjectSchema.safeParse({ …, titleField: 'x' })` fails with `unrecognized_keys` + — the same code a nonsense key gets — while `nameField`, `displayNameField` and + `titleFormat` all parse and survive. A producer census across both repos found + nothing that puts the key on an object-shaped payload: not the metadata, not any + `getObjectSchema` implementation (the ObjectStack adapter stamps only reference + keys and field-widget hints), not the lookup-chip path, not the + search-candidate path, and not the platform's own server-side resolver + (`@objectstack/objectql#titleFieldOf` reads `nameField` → `displayNameField`). + Reading a key no producer can ship is a consumer-side alias — the shape + Commandment #0.1 bans — and it inverted the governed-authority default on top of + that. + + No authoring surface changes and no view loses its author-chosen title field: + `titleField` remains a real, declared VIEW key (`ui/CalendarConfig`, + `ui/GalleryConfig`, `ui/GanttConfig`, `ui/ListMapConfig`, + `ui/ObjectKanbanProps`, `ui/TimelineConfig`), views hand it in as + `options.titleField`, and that half of step 0 still wins over everything. + The behaviour change is confined to an object payload that carried a key the + contract rejects: it now resolves through the declared ladder instead. +- f2158ec: `ExpressionEvaluator.registerFunction` now documents the case-fold it has always + performed: the name is stored — and must be called — in UPPER CASE + (objectui#5363). + + `registerFunction('formatCurrency', fn)` registers `FORMATCURRENCY`, because the + method delegates to `FormulaFunctions.register`, which stores under + `name.toUpperCase()`. That fold is correct for the spreadsheet-style built-in + vocabulary (`SUM`, `IF`, `UPPER`) and is unchanged here — but nothing declared + it on the public method, and two things keep it from being self-evident at the + call site. The registry API stays case-insensitive, so `getFormulas().has()` and + `.get()` both answer to the original spelling and never reveal the fold; only + expressions see the stored key, because the evaluation scope is built from + `FormulaFunctions.toObject()`, a plain object whose identifiers are matched + case-sensitively. And a wrong-case call site does not raise: `evaluate()` + catches, warns, and returns `defaultValue ?? expression`, so the template + renders its own `${...}` source as literal text on screen rather than erroring. + + Behavior is untouched — this is the declaration catching up with what the code + enforces. It ships as a patch rather than as an empty changeset because the + JSDoc is emitted into the published `dist/evaluator/ExpressionEvaluator.d.ts`, + so it is what consumers see on hover. + + `ExpressionEvaluator.test.ts` gains three cases pinning the half that was + uncovered — that the given spelling does *not* resolve in an expression, that + the failure renders the raw template source instead of throwing, and that the + registry API stays case-insensitive underneath — so making registration + case-preserving fails a test instead of silently invalidating the new JSDoc. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- ed71d9e: Withdraw the entry-surface justification on core's `SchemaNode` re-export, and put + the gauge that can actually fail in its place (objectui#5673). + + `packages/core/src/types/index.ts` carried, as the justification for the #4580 + re-export convergence, *core's own entry surface is unchanged (`dist/index.d.ts` is + byte-identical across the change — measured, both rounds)*. The reading was real and + it certified nothing. `core/dist/index.d.ts` is emitted from a barrel that only + FORWARDS the symbol, and forwarding never restates a shape — not `export *`, and not + the `export type { … }` line that names this one. Only the module that DECLARES the + symbol can move, so that file is byte-identical under any change to a re-exported + declaration's shape, and it could not have failed for the change class it was quoted + against. + + Measured for this change rather than argued. One optional key was injected into + `BaseSchema` — the shape `SchemaNode` publishes — both packages were rebuilt from a + cleared `dist/` and a cleared `tsconfig.tsbuildinfo`, then the probe was dropped and + both rebuilt again: + + | emitted file | base | with probe | probe dropped | + |---|---|---|---| + | `@object-ui/types` `dist/base.d.ts` — declares the shape | `31b5a01d…` | `8487500e…` **moved** | `31b5a01d…` | + | `@object-ui/core` `dist/types/index.d.ts` — forwards it | `0e64c8c6…` | `0e64c8c6…` | `0e64c8c6…` | + | `@object-ui/core` `dist/index.d.ts` — entry barrel, names it | `5cca207a…` | `5cca207a…` | `5cca207a…` | + + The corrected block states that calibration as a recipe with its failure mode, so the + next reader inherits a gauge that can be checked instead of a sentence that cannot. + The `ComponentRendererProps` block below it already reached the right verdict, but + gave a narrower reason for it — that core's entry is an `export *` barrel — which is + not the mechanism, and is wrong for a symbol the barrel names on its + `export type { … }` line; it now states the forwarding rule. + + Documentation only, in a published declaration file: these docblocks sit on export + specifiers, so `core/dist/types/index.d.ts` carries them into the tarball, while + `core/dist/index.d.ts` does not move for them either — the same insensitivity, + demonstrated once more on this very change. No type moves and no runtime behaviour + changes. +- 7776fc2: fix(core): `ValueDataSource` applies the filters it is given instead of returning every row + + `matchesASTFilter` recognised only two node shapes — a logical `and` / `or` head + and a three-element comparison — and answered `true` for everything else. Three + consequences, all silent: a legacy flat implicit-AND array (`[[…], […]]`) applied + no filter at all, at top level and as a nested child of `and` / `or` alike; the + null-ness operators had no arm, so `is_null` / `is_not_null` selected every row; + and 16 of the spec's 20 canonical view operators — `equals`, `greater_than`, + `starts_with` among them, the spellings `toFilterNode` lowers a stored view's + rules into — fell through the same way. + + The matcher now canonicalises operators through the spec's own + `canonicalAstOperator` and reads all four shapes `FilterArraySchema` declares, so + an in-memory `provider: 'value'` list applies the same filter the wire would. An + operator or shape it cannot execute now excludes the row and logs once per + `find()`, rather than passing every row with no signal anywhere. +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [d2fb6ef] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [d6ceb8d] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [6bca0e4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index fb341c8ce4..3ef252875c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/core", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/create-plugin/CHANGELOG.md b/packages/create-plugin/CHANGELOG.md index 9e3b4d302a..5e1f0eae00 100644 --- a/packages/create-plugin/CHANGELOG.md +++ b/packages/create-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @object-ui/create-plugin +## 17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/create-plugin/package.json b/packages/create-plugin/package.json index 4271b6a799..948a029bb3 100644 --- a/packages/create-plugin/package.json +++ b/packages/create-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/create-plugin", - "version": "17.6.0", + "version": "17.7.0", "description": "CLI tool to scaffold ObjectUI plugins", "type": "module", "license": "MIT", diff --git a/packages/data-objectstack/CHANGELOG.md b/packages/data-objectstack/CHANGELOG.md index 731d4a9c83..99eb6598f8 100644 --- a/packages/data-objectstack/CHANGELOG.md +++ b/packages/data-objectstack/CHANGELOG.md @@ -1,5 +1,910 @@ # @object-ui/data-objectstack +## 17.7.0 + +### Minor Changes + +- 993f312: Parse a write-strip's `reason` against the spec enum at the boundary + (objectui#4934). + + `notifyDroppedFields` filtered a create/update response's `droppedFields` on + SHAPE alone — a hand-written `e is DroppedFieldsEvent` guard that checked + `Array.isArray(fields)` and nothing else — so a `reason` outside + `'readonly' | 'readonly_when' | 'primary_key'` reached every subscriber typed as + though it were inside the union. A deployed client normally runs BEHIND the + server it talks to, so a reason from the future is the expected skew direction, + not a corrupt payload; the interior was typed to trust a union no one had + checked, and nothing in the repo could say so. `notifyBatchDroppedFields` did + the same through its `entry as DroppedFieldsEvent & { index?: number }` cast. + + Both paths now read `reason` against `DroppedFieldsEventSchema.shape.reason` — + the enum the installed pin declares, derived rather than restated, so a pin bump + that adds an arm widens the accept set on its own: + + - **Every entry is kept.** Dropping the unparsable ones would tell the user + nothing about fields the server really did strip, which is exactly the silence + objectui#3484 removed. + - An unrecognized `reason` arrives on a named skew arm, + `UnrecognizedDropReasonEvent`, carrying `UNRECOGNIZED_DROP_REASON` plus the + wire value **verbatim** in `unrecognizedReason` — never coerced onto a known + arm, because claiming `readonly` for a reason we cannot name is a false + statement about the user's data. + - `WriteWarningEvent['droppedFields']` is therefore the two-arm + `DroppedFieldsNotice`. The spec type stays the canonical arm and is not + widened to `string` (objectui#3160): the skew arm is not assignable to + `DroppedFieldsEvent`, so a consumer branching on `reason` now hears about + server skew from `tsc` instead of from a per-consumer discipline. + + Runtime wording is unchanged: the one reader, the app shell's write-warning + toast, already answered an unrecognized reason with its cause-free line. + + **Blast radius — the compile error IS the intended signal, not a regression.** A + consumer that branches exhaustively on `reason` — a parameter, a `Map` key or a + `Record` annotated `DroppedFieldsEvent['reason']` — stops compiling against this + release, with a `TS2345` at each such site. That error is the notification, and + the only one: the skew arm is deliberately NOT assignable to the spec union, so + `tsc` reports server skew at the one place the wire is read rather than leaving + it to a per-consumer discipline. Do not cast it away. Widen the annotation to + `DroppedFieldsNotice['reason']`, and where the two arms have to be told apart, + narrow with `entry.reason === UNRECOGNIZED_DROP_REASON` and read the wire value + verbatim from `unrecognizedReason`. + + Widen the LOOKUPS, not the table. A `Record` that must stay exhaustive over the + SPEC arms keeps its `DroppedFieldsEvent['reason']` key: widening that one would + trade away the guarantee that a newly pinned spec reason fails `type-check` + unworded (objectui#3935). + + In this repo the entire blast radius is the app shell's write-warning toast — + two type annotations, no runtime change. Its executable JavaScript is byte-identical + and its wording tests pass unchanged, because the file was already written for + this value: its own docstring says the runtime `reason` may sit outside the spec + union and that the cause-free fallback is reachable, not dead. Only the parameter + and the `Map` key had been left narrower than that documented contract. +- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's + ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a + list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user + dragging a column or cycling density was re-styling that view for the entire organization. Nothing + in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, + v18) and is deliberately not built here — which is precisely why the write has to be gated rather + than narrowed: there is no second, private store for it to fall back to. + + `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability + set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` + (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the + `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — + before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. + It is on the write rather than on the toolbar button on purpose: withholding the affordance would + leave the method still accepting the call from anything else holding the adapter, whereas a gate on + the write is inherited by every caller, present and future. + + `manage_metadata` is not a newly minted name. It is the capability this repo already treats as + metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself + refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very + same ADR-0005 metadata door, so this applies the authority the server is already applying instead of + inventing a parallel one. + + **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating + ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a + client-side refusal on missing data cannot protect anything and can only break a permitted user. A + *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new + `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. + + The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure + into `console.error`, which for a debounced toggle whose UI has already moved would have left the + operator looking at a density they did not get; a denied write now raises a toast. And the "View + settings" popover — where density and field visibility are actually changed — now states the scope + before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who + uses this view."*, translated in all ten packs. +- 94e2fa7: `MetadataClient.layered()` validates the ADR-0010 protection envelope against the + producer's own schema at the boundary, instead of casting ten wire fields through + unchecked (objectui#5676, triage adjudication 2026-08-22). + + The envelope arrived by ten `as` assertions over a raw `res.json()` body — no parse, no + allowlist, no default. The consumer that reads it opens the metadata lock banner on + `layered?.lock && layered.lock !== 'none'`, true for **any** non-`none` value, so a server + sending a lock state this console had never heard of opened the amber box, drew the padlock + and the border, and rendered an empty title. No fifth state ever had to be added to this + repo for that to happen: a union types what this repo writes and constrains nothing about + what a server sends. + + The boundary now runs `GetMetaItemLayeredResponseSchema.safeParse`. On the conforming path + every value is the producer's schema output and the ten assertions are gone. `safeParse` + and never `parse`: a metadata console that rejected every dialect it had not been compiled + against would answer a newer server with a blank page, which is strictly worse than the + wrong render being fixed. Values the schema rejects are still **forwarded** — dropping them + would be that same refused rejection wearing different clothes — and are named in a new + optional `MetadataLayered._unrecognized`, absent whenever everything parsed. This extends + to the whole envelope the "pass through and label" treatment objectui#5672 chose for `lock` + alone; the banner's existing unrecognised-token title is unchanged and needed no edit. + + The labelling is per field, which is the part that makes it a degrade rather than a subtler + version of the same bug. Measured on the installed spec (17.2.0): + `GetMetaItemLayeredResponseSchema.safeParse(body)` is all-or-nothing — one unknown `lock` + returns `success: false` with `data` undefined — so the failure branch re-checks each key + against that schema's own `shape[key]`, where only the offending field fails and the other + six still arrive typed. Absence is never "unrecognised": the four resolved verdicts are + required upstream on this path, so a pre-ADR-0010 backend takes the failure branch with + nothing flagged and behaves exactly as before. + + One consequence of the same ruling, fixed alongside because it defeats it: a 200 answer + whose body was a bare JSON string or number **rejected** the promise with a + `TypeError: Cannot use 'in' operator`, from the envelope-detection guard's bare truthiness + check. A malformed body must degrade, never throw. +- f07b976: `aggregate()`'s spec-shape branch now REFUSES an unlowered `where` instead of + posting it (objectui#6825, maintainer ruling 2026-08-30 — option A). + + **Breaking for callers that were already broken, so read this if you call + `aggregate()` with a `where`.** `aggregate()` has two branches. The analytics + branch takes `filter` and lowers a rule-shaped array before the wire (#6302). + The spec-shape branch — entered when `params` carries an array `groupBy`, an + array `aggregations`, or ANY `where` key — takes `where` and posts it to + `POST /data/:object/query` verbatim. It never lowered, and it still does not: + it now says so. + + **What now throws that previously went through.** A `where` that is an ARRAY + the spec's own `isFilterAST` gate rejects — an unlowered + `[{ field, operator, value }, ...]` above all, plus the infix join dialect + (`[condA, 'or', condB]`), a tuple whose operator is outside the AST vocabulary, + `['and']` with nothing to join, and an element that is not a condition. The + throw is an `UnloweredAggregateWhereError` (exported), carrying the same + `code: 'INVALID_FILTER'` / `httpStatus: 400` pair as `MalformedFilterError`, so + `isMalformedFilterError()` recognises it and a failed widget renders "this + filter is malformed" rather than "check your connection". + + **This adds no new failure — it relocates one.** Every shape now refused is one + the receiving engine already refused (`is not a filter`, 400 `INVALID_FILTER`, + before the store is touched). What changed is WHERE you find out and whether you + can act on it: previously the predicate was lost on the wire — or dropped + outright, leaving a chart rendering confident, wrong numbers with no signal to + its author. The refusal is now raised at the producer, names the value it + received, names the shape expected and where the spec declares it, and says + nothing was sent. + + **What an affected caller should send instead.** Lower the rules to a filter AST + BEFORE calling `aggregate()`: + + ```diff + - adapter.aggregate('opportunity', { + - groupBy: ['stage'], aggregations: [...], + - where: [{ field: 'stage', operator: 'equals', value: 'won' }], + - }) + + adapter.aggregate('opportunity', { + + groupBy: ['stage'], aggregations: [...], + + where: ['stage', '=', 'won'], + + }) + ``` + + Or keep using the analytics branch, which lowers for you: pass the rules under + `filter` with the legacy `field` / `function` / `groupBy` params. + + **Unchanged, deliberately.** The analytics branch still lowers `filter` exactly + as #6302 left it. On the spec-shape branch a `FilterCondition` object + (`{ stage: 'won' }` — what `QuerySchema.where` actually declares), an empty + array (`[]` is "no filter", and the engine agrees), and every already-valid + filter AST all reach `client.data.query` byte-unchanged. +- 6a99bb2: `createObjectStackAdapter` declares the adapter it returns, not the shared `DataSource` + interface (objectui#7323). + + The factory returned `new ObjectStackAdapter(config)` while declaring `DataSource<T>`. + A wider value is assignable to a narrower annotation, so nothing ever failed to compile + — the loss was entirely on the reading side. Measured against the shipped + `dist/index.d.ts` with the doc-snippet gate's own compiler options, nine reads through + `ReturnType<typeof createObjectStackAdapter>` failed with TS2339: `getClient`, + `getCacheStats`, `invalidateCache`, `clearCache`, `getConnectionState`, `isConnected`, + `onConnectionStateChange`, `onBatchProgress` and `setSystemCapabilities`. Eight of those + nine reads are on this package's README API Reference list, and four whole README + sections are built on them; the ninth measured read is the one the factory's own JSDoc + links to (`[ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}`). The README + list is itself **nine** adapter-only members, not eight — `connect()` is adapter-only + too and was documented all along; it simply was not one of the reads the card's + reproduction measured. So the file's own doc comment pointed the reader at a method its + declared return hid, and the two documented ways to obtain the same object — the factory + and `new ObjectStackAdapter(…)` — handed back different type surfaces. + + **What the declared return now is: the whole class, not those nine reads.** The nine + above are what the reproduction measured, not the size of this change. The factory's + declared return is now `ObjectStackAdapter<T>` itself, so **every public member of the + class** is part of what the factory promises. Against `DataSource` that is **20** + members, not nine — `tsc`-computed as + `Exclude<keyof ObjectStackAdapter<unknown>, keyof DataSource<unknown>>`: `clearCache`, + `connect`, `getCacheStats`, `getCached`, `getClient`, `getConnectionState`, + `getDiscovery`, `getItems`, `invalidateCache`, `invalidateViewKeys`, `isConnected`, + `listImportMappings`, `onBatchProgress`, `onConnectionStateChange`, `onSaveAdvisory`, + `onWriteWarning`, `probeAppAccess`, `queryDataset`, `setSystemCapabilities`, + `updateDashboard`. The eleven past the documented nine were already in the shipped class + type — none is `@internal` or `@deprecated`, `stripInternal` is not set, and all were + already reachable through `new ObjectStackAdapter(…)` and through every + `ObjectStackAdapter`-typed seam in `@object-ui/react` and `app-shell` — so what widens + here is what the **factory declares**, not what the package ships. Two are escape-hatch + shaped and worth knowing before building on them: `getCached(key)` is a raw cache read, + and `getDiscovery()` reaches an internal property of the underlying `ObjectStackClient`. + + **Branch taken: A (widen the factory's declared return), and why.** The card offered + three. B — moving caching, connection state and batch progress onto `DataSource` — was + rejected because those are this adapter's concerns, not every data source's; every other + `DataSource` implementation would then declare members it does not have. C — documenting + a cast — teaches a cast around a declaration that is merely narrower than the value, + which is the opposite of `declared = enforced`. A is one line and makes declared match + shipped for every documented member at once. + + Two questions decided the shape and both were answered from the code before the diff. + `ObjectStackAdapter` was **already** exported from the package's only entry + (`src/index.ts`, tsup's single entry; the class is in the shipped `dist/index.d.ts` + export list, two pin tests assert the exported spelling, and `apps/console` re-exports it + by name) — so widening the return exports nothing by implication. And the narrow return + was **not** a deliberate swappability guarantee: no comment, ADR or test pinned it, and + the commit that added `autoReconnect` / `maxReconnectAttempts` / `reconnectDelay` to the + factory's own config bag left the members that observe those features off the factory's + declared return in the same change. + + **One caller shape breaks: a structural stand-in for the factory's return.** A + hand-written object literal annotated `ReturnType<typeof createObjectStackAdapter>` no + longer satisfies that type, because it is now a class with private members (TS2740) — + annotate such a fake as `DataSource` instead, which is what it was standing in for. + Nothing else moves: a wider return is assignable to the narrower annotation, so + `const ds: DataSource = createObjectStackAdapter(…)` keeps compiling and keeps giving + the narrow surface to anyone who wants it. + + The README's note saying the page could not yet teach the factory's shape is removed, and + the four sections built on the adapter-only members (Metadata Caching, Connection State + Monitoring, Batch Operation Progress, Troubleshooting → Cache Issues) now continue from + Basic Setup's `createObjectStackAdapter(…)` call instead of declaring the class by hand. + The docs-site page `content/docs/utilities/data-objectstack.mdx` is corrected the same + way: its prose, its factory signature fragment and its "hold the class type to reach + these" section described the old narrow return, and its Mutations and Troubleshooting + examples told the reader to construct the class by hand to reach members the factory now + declares. `src/adapterFactoryReturn.types.test.ts` pins the card's TS2339 reproduction + inverted, + with two controls: the adapter-only members stay absent from `DataSource` (fires on + option B), and the widened return stays assignable to `DataSource` (swappability kept). +- a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered" + (objectui#7741). + + `ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the + import wizard hides its saved-mapping selector on an empty list. So "the server served + zero mappings" and "the server refused, or broke" produced the identical UI on every + deployment — the feature simply absent — with a `console.warn` as the only + discriminator, in the browser console, with nothing pointing at it. That silence did not + merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter + (objectstack#14026 was filed, routed and worked by two seats against a wizard that had + been correct since `@object-ui/data-objectstack@17.1.0`). + + **The empty-list return is unchanged.** `listImportMappings` still answers + `Promise<any[]>` and still never throws, on every arm including the loud ones — this is + a channel added ALONGSIDE that contract, not a change to it. + + - **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe + channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a + metadata read failed in a way that is NOT the supported "this deployment does not + serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the + object, whether the server `refused` this caller or the answer was `unreadable`, and + the server's own ADR-0112 code, HTTP status and message. + - **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported + so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code` + first, the status only where no code was declared — and never "is the result an empty + array", which is what both conditions produce and so can never tell them apart. + - **The older-server case stays quiet.** A deployment that does not serve the `mapping` + kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list + door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no + event. That is a real, supported deployment shape and it must not become a visible + fault. + - **The console now says so.** `AdapterProvider` subscribes to the new channel and + renders a warning toast naming the object, the remedy and the server's own words, so a + user without devtools open can tell "there are none" from "we could not find out". + Three new `console.importMappings*` keys ship in all ten locale packs. + + This applies framework #13906 decision 1 option A — *a thing that could not be READ is + not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not + a new principle. +- 53ded82: Array filters on analytics aggregates were posted un-lowered and refused by the runtime with 400; they are now lowered to the canonical `FilterCondition` before the wire. + + An `element:number` or an `object-metric` whose filter is authored as an array (`[{ field, operator, value }, ...]`, a comparison tuple, or an `and`/`or` group) reached `POST /analytics/query` as an array. That route parses the body with `AnalyticsQueryRequestSchema` first, and its `where` is a `FilterCondition`, so the widget answered `400 Invalid AnalyticsQuery body: where: ...` instead of its number — leaving the MongoDB-style record as the only authoring form that still worked. + + `aggregate()` now lowers the array through `parseFilterAST`, the single sink `@objectstack/spec` names for turning a `FilterArray` into a `FilterCondition`, so the posted `where` is the shape the wire declares. An empty array posts no `where` at all, a record-shaped filter is unchanged, and an array the sink cannot lower — an infix join such as `[condA, 'or', condB]` — is refused with this adapter's `INVALID_FILTER` / 400 error rather than posted or silently dropped into an unfiltered aggregate. +- d5c1f52: `classifyAnalyticsFailure` now reads a 400 as a refusal of the query body we + sent regardless of which ADR-0112 `code` it carries, so `aggregate()` no + longer answers a rejected filter with client-side numbers from a different + door (objectui#7755). + + Before this fix, only 400 `VALIDATION_FAILED` (and a code-less 400) threw + `AnalyticsQueryRejectedError`. Any OTHER coded 400 — `service-analytics` ships + its own 400 `INVALID_FILTER` on a filter shape it refuses — matched none of + `classifyAnalyticsFailure`'s branches and fell through to `unknown`, which + `aggregate()`'s catch has no arm for, so it silently degraded to + `aggregateViaFind`: a re-read through `find()`'s `$filter` query-string + contract, which accepts array shapes the analytics request body does not. A + filter the analytics route refused could still be answered — with a + plausible, wrong number, and no sign the request had a defect. + + The fix is a floor UNDER the existing code branches, not a replacement for + them: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` still win `not-installed`, + `VALIDATION_FAILED` / `UNAUTHENTICATED` / `CUBE_NOT_FOUND` still win their own + outcomes first (objectui#5721). Only a 400 that none of those four already + claimed now falls to the new floor instead of past it. An unmatched NON-400 + coded error (e.g. a coded 5xx) is unaffected and keeps degrading exactly as + before — this fix is scoped to the 400 case only. +- b2065e7: Classify `/analytics/query` failures by their ADR-0112 `code` rather than their HTTP status, so a chart is no longer answered from a different code path behind the wrong explanation. + + `classifyAnalyticsFailure` tested `status === 404 || status === 501` before the code operands on the same line, so the status short-circuited every one of them: any 404 on this face was classified "the analytics capability is not installed" whatever code it carried, and `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the conditions they name. Three unrelated conditions answer 404 on this url, so the status cannot tell them apart — the `code` is the contract. + + Two conditions change behaviour: + + - **404 `CUBE_NOT_FOUND`** (a misspelled or unregistered cube — an authoring mistake) now **throws** the server's own error verbatim, keeping `code` and the producer's repair instructions. It previously warned "install `@objectstack/service-analytics`" and silently degraded to `find()` + client-side aggregation — which cannot answer it anyway, because the fallback re-reads the same name through `/data`, where an unregistered object is a 404 `OBJECT_NOT_FOUND`. + - **401 `UNAUTHENTICATED`** (an anonymous or lapsed session) now **throws** `AnalyticsUnauthenticatedError` instead of degrading silently behind a `find()` that is about to be refused the same way. + + Unchanged: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` and code-less 404/501 answers still degrade loudly to the client-side fallback, 400 `VALIDATION_FAILED` still throws, and 5xx / network failures still degrade silently. +- b470e91: The metadata designer states its package on the publish step, not only on the save (#5420) + + Studio's designer save→publish loop bound the draft to a software package on the + save (`PUT ?mode=draft&package=<id>`) and then sealed it with a publish that named + no package at all. `objectstack#10354` (shipped in `@objectstack/rest` 17.2.0) taught + `POST /meta/:type/:name/publish` to accept `?package=<id>`, so the second call can now + state the same binding the first one already states. + + - `MetadataClient.publish()` accepts `packageId` and sends `?package=<id>`, the same + wire spelling and the same `encodeURIComponent` treatment `save()` gives it. + - `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation + (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The + `?package=all` "show everything" scope keeps folding to "no package". + + The parameter is **omitted**, never sent empty, when the designer holds no binding. + Empty and absent are the same to the framework's normaliser today, but absent is the + shape the save door already followed, and the framework's promotion path branches on + the key being present downstream. + + What this buys is **reachability**, not speed: it lets `#9612`'s package-closure + narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That + narrowing has a second, independent gate this does not touch — objects carrying no + `_packageId` provenance are kept unconditionally — so on a tenant-authored overlay + corpus stating the package still narrows nothing. +- 8e00bfd: **Breaking (published surface):** remove `options.actor` from `MetadataClient`'s + `save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor` + request header. + + The server stopped honouring that header. objectstack#7941 ruled that the + recorded actor is the identity the request was authorized as, and removed the + header limb from the `/meta` write resolver — attribution cannot drift from + authorization. The option therefore typed cleanly, sent a header, and could not + influence the audit or history row it appeared to address: a false affordance + that promised attribution and silently failed to deliver it. + + Three declarations go: `MetadataClientSaveOptions.actor` (inherited by + `MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`), + and the inline `{ actor?: string }` on each of `publish` and `rollback`. + `MetadataAuditEntry.actor` is unaffected — that is the server's read-back of + who acted, and it remains the way to see attribution. + + Marked `minor` rather than `major` per this repo's version-alignment policy + (the fixed group's major tracks `@objectstack`, and `major` in a changeset + would drag all 39 packages off that cadence). + + No caller in this repo passed `actor`; the census found the only in-repo + occurrence was the client's own unit test. Callers outside this repo that still + pass it are unaffected at runtime beyond losing a header the server already + ignored — the property is dropped rather than forwarded, pinned by + `metadata-actor-retired-4834.pin.test.ts`. +- fd8dace: Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save + + objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. + + The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door. + + `MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones. + + One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb. + + **BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group. + + Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) still discards per-draft advisories server-side — objectstack#9343, open and unruled — and nothing here compensates for that from the client side. A test pins the absence, so a later traversal of a batch-shaped `published[]` cannot be added without turning it red. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 490d9a9: Grid headers offer a sort click only on columns the PLATFORM says it will order by + (objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23: + the platform serves an explicit per-column sortability signal and the grid reads it, + rather than re-deriving "virtual ⇒ unsortable" from field type). + + `GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its + ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve + time from the platform's own storage predicates, deliberately beside `item` rather than + inside it so the key stays un-authorable. The signal was reaching the browser and being + discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps + the envelope to `item`, so every UI reader saw a document with no signal on it. It now + survives that unwrap, carried on the schema under a symbol key — invisible to + `JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata + write endpoint can never take it into a body the server parses strictly. + + `@object-ui/core` gains the one spelling of the consumer contract: + `isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and + says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown + name, a dotted path and an unprovisioned audit column, all three of which the runtime + doors reject — so the `!== false` spelling every other optional flag in this repo uses + would get exactly that family backwards. A projection that is absent ALTOGETHER is a + different question with a different answer (`undefined`: no signal was served) and is + typed apart from an empty one, so a deployment older than the upstream change keeps the + behaviour it had rather than being told, falsely, that nothing on the object is sortable. + + Three things follow in the grid. The header click on a refused column ceases to exist, so + neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is + reachable from it. A sort PERSISTED before the signal existed is filtered out of both what + the grid renders and what it emits, so a restored personalization cannot ride back into + the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the + PUT still fires. And the relational carve-out is untouched and deliberately not delegated + to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored + foreign key and both runtime doors accept ordering by it), while the grid withholds that + header for a different reason — a column of names ordered by an invisible id. + + Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts + those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced + drift in mirror image. +- a26b9e4: `packages/core/src/adapters/README.md` now documents the adapters that are actually in that + directory, and the ObjectStack material it carried moved to the package that owns the behaviour + (objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a + README rides every tarball — so this was published documentation describing the wrong package. + + The page had been left behind when the ObjectStack adapter moved out to + `@object-ui/data-objectstack`: its headings, feature list, filter-operator table and + query-parameter table were all about that adapter, and its one-entry "Available Adapters" list + told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`. + `ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and + `emulateBatchTransaction` — the five exports that directory really ships — were named nowhere. + + - **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a + usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the + ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was + always about this directory. + - **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the + filter-operator and query-parameter mapping tables, the AST conversion example and the sorting + example. That material existed **only** in the `core` copy — this package's README documented + query translation as a single feature bullet — so it is ported, not dropped. + + No runtime behaviour changes; the duplicate copy of one package's documentation living under + another package is what goes away. +- 5127378: `ObjectStackAdapter.aggregate()` lowers rule-shaped filter arrays before the + analytics wire, reusing the lowering `find()` already runs (objectui#6302). + + `find()` has translated `[{ field, operator, value }, ...]` into the server's + filter AST for as long as `convertQueryParams` has existed. The analytics path + did not: `aggregate()` assigned `payload.where = params.filter` verbatim and + posted it to `/analytics/query`. + + The two doors are not equally forgiving, so the gap had a user-visible end. + `lowerAnalyticsWhere` in `@objectstack/service-analytics` — shared by both + aggregation strategies — accepts AST tuples and throws on an array of rule + objects. A stored `ViewFilterRule[]` that a LIST renders correctly therefore + rendered `element:number` into its error state on every analytics-capable + deployment, which is the default one because the CLI always loads analytics. + + An array filter now goes through the same `translateFilterArray` the `find()` + path uses — one lowering, so the two paths cannot disagree about one stored + filter. Rules spread into a logical node (`['and', ...rules, ...tuples]`, the + commonest composite there is) are lowered at depth, as they already were on + `find()`. Non-array filters are untouched: the MongoDB-style object this branch + was written for is what `/analytics/query` already accepts, and translating it + would be a semantic change this fix does not make. Already-AST arrays, + record-shaped filters, and the no-filter case are byte-unchanged. +- 5961030: `@object-ui/core` and `@object-ui/data-objectstack` now declare + `"@objectstack/spec": "^17.2.0"` rather than `^17.0.0`, which is the lowest published + spec that carries every symbol each package's own build output references + (objectui#6361). + + `packages/core/dist/utils/column-sortability.d.ts` references + `FIELD_SORTABLE_UNPROVISIONED_ANCHOR`, `FIELD_UNSORTABLE_VIRTUAL_TYPE`, + `FieldSortability` and `ObjectSortability` from `@objectstack/spec/api`, and + `packages/data-objectstack/dist/index.js` references the first two — none of which + `@objectstack/spec@17.0.0` exports. Measured against the published tarballs rather than + the installed tree, by `scripts/check-spec-range-floors.mjs`: six `floor-too-low` + findings across the two packages, and `^17.2.0` is that gate's own computed answer for + both. So the old range was a claim neither package could honour: any consumer + resolution that lands 17.0.0 — a sibling pinning it exactly, an `overrides` entry, a + mirror two minors behind — satisfied `^17.0.0` and got a dangling reference. + + Nothing a consumer installs today changes: normal resolution already picks the newest + 17.x, and `pnpm-lock.yaml` still resolves `17.2.0` on both edges after the bump — only + the recorded `specifier:` moves. No source and no behaviour changes, which is why this + is scored `patch`, on the reasoning objectui#5793 used for the same remediation on + `@object-ui/plugin-detail`. + + The bump is release-blocking rather than cosmetic. `check:spec-floors` is deliberately + not a `pull_request` job, so every PR stayed green while its blocking copy on the + publish path — `pnpm changeset:publish` runs it before a single tarball reaches npm — + would have cancelled the next release. +- 639114c: Reconcile `@object-ui/data-objectstack` with the `@objectstack/*` family at + 17.3.0 (objectui#7122). + + `@objectstack/client`, `core`, `formula` and `lint` each pin `@objectstack/spec` + EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO + copies of it. Moving the family with it in `pnpm-lock.yaml` collapses the + duplicate; every declared range already admitted 17.3.0, so no manifest moved. + + The one source change the family bump forces is a type reconciliation, not a + behaviour change. `client.analytics.query` resolved to `Promise<any>` at 17.2.0 + and resolves to `Promise<AnalyticsResult>` at 17.3.0, so the pre-envelope + branches of `aggregate`'s row-shape fallback stopped type-checking. Those + branches are read through a widened alias rather than deleted: the client's own + docblock records the runtime change behind the narrower type ("BREAKING since + objectstack#13079 — read `result.rows`, not `result.data.rows`"), and deleting + them is a runtime compatibility decision about servers older than that, not a + type repair. The alias restores exactly the compile-time latitude 17.2.0 gave + the same expression and changes no runtime byte of it. +- f57ca75: fix(studio): one draft-envelope reader, and it strips the framework's read decorations + + `client.getDraft()` serves a DECORATED body — the draft branch stamps + `_draft: true` and then `decorateMetadataItem` attaches `_diagnostics` for any + type with a registered Zod schema. The spec names both READ-TIME decorations + precisely because a served body "is NOT a valid input to the schema that + produced it until these are removed" (`METADATA_READ_DECORATIONS`). + + objectui#7603 taught `ResourceEditPage` to strip them. It could only teach one + site, because `extractDraftBody` existed **four times** — three verbatim copies + plus a hand-rolled one in `ObjectHooksPanel` — and six more consumers unwrapped + the envelope inline. Ten readers, one of which knew the rule. + + **The user-visible half.** The pending-changes sheet's per-entry diff compares + the published body against the draft body key by key. Those two reads are + decorated ASYMMETRICALLY — only the draft branch stamps `_draft` — so the sheet + listed `_draft` under "Also changed:" on every entry that has a published + counterpart, and `_diagnostics` alongside it whenever the two read-time verdicts + differed. Framework-internal keys were being presented to the author as their + own edits, on the screen where they decide whether to publish. + + **The rest.** Six sites merged a decorated body into a document they then wrote + back through `save(..., { mode: 'draft' })` — the Studio app / page / object / + flow surfaces, the package OWD panel, the object hooks panel, and the + adapter's `updateView`. Today's server absorbs that (it strips read decorations + on ingress, before its own schema gate), so nothing 400s; this is still a client + emitting a body its own spec calls invalid, and the fix belongs at the producer. + + The cure is one function rather than ten strips: `extractDraftBody` is now + exported from `@object-ui/data-objectstack`, beside the `getDraft` whose + envelope it decodes. The key list is the spec's exported + `stripReadDecorations` — never a second hand-maintained copy in this repo. The + presence verdict still runs BEFORE the strip, so removing our own annotations + can never turn a served draft into "nothing pending", and the ADR-0010 + protection envelope (`_lock`, `_provenance`, `_packageId`, `_packageVersion`) + is deliberately untouched: those keys are declared by the closed schemas. + + No schema was loosened, and no gate was taught to tolerate `_diagnostics`. +- b2e85a9: `ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata + types in the singular, matching the other twelve `client.meta.*` call sites in this + file (objectui#4940). + + `getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed + the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued + its singular spelling was deliberate — the plural site was silent. Both plural sites + resolved today only because the server folds plural → singular + (`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`, + confirmed by reading both the mapping and the by-name route handler that calls it), so + this is consistency restoration rather than a behavior change — nothing a user hits was + broken, and nothing a user hits changes. + + `appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended + with two new cases asserting `getApp`/`getPage` pass the singular type to + `client.meta.getItem`, so a future revert to the plural spelling fails a test instead of + depending on the server-side fold staying in place. +- c7cd2b6: `ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's + ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an + unauthenticated session stop being reported as a missing analytics capability + (objectui#5663). + + Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`: + the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and + the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The + mapping tested `res.status === 501 || res.status === 404` and called all of it + "the analytics capability is not installed", so every unknown dataset produced a + banner telling the operator to install `@objectstack/service-analytics` and mount + `AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on + four HotCRM Executive Overview widgets while the analytics service was installed + and answering — the real condition was an installed `app.objectstack.hotcrm` at + 1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of + the system from the remedy the banner named. + + Three conditions now get three answers, each keyed on the code the framework + declares for it: + + - `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and + `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing + `AnalyticsNotInstalledError` and its copy — one remedy, one message. + - `NOT_FOUND` (404, unknown `datasetName`) throws the new + `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the + dataset and pointing at the installed app's version rather than at the server. + - `UNAUTHENTICATED` (401, `enforceAuth`) throws the new + `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the + request was refused before it ran and therefore says nothing about the + capability. + + The banner also used to print the server's own message in a parenthetical while + contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not + found.` under a headline claiming a missing capability. That is now structurally + impossible rather than merely fixed: the headline is a pure function of `code` and + the parenthetical is a verbatim quote of `message`, both read off the same + response, and a test walks every branch asserting each message carries its own + headline and none of the others'. + + Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its + constructor signature (it gains an optional third `serverCode` argument and a + `serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including + the metadata-admin dataset preview — are unaffected. A 404 carrying a code this + client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now + keeps its server detail instead of being relabelled as a missing capability; a 404 + or 501 carrying no code at all is still read as the capability being absent, since + the route's own `NOT_FOUND` always ships a code. +- 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no + title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three + times (objectui#5024). + + `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four + states out by hand, 42 lines apart in one file, compared by no gate. They are now + one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s + `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies + were restating a schema rather than filling a gap. + + The user-visible half is the banner. Its title was three independent `&&` branches + with no fallback, while the switch that opens the banner is true for any non-`none` + value — so a lock state outside the four opened the box and left the headline + empty. That is reachable without a fifth state ever being added here: + `MetadataClient.layered()` casts the wire value through unchecked, so a newer + server reaches this banner as-is. Measured, not assumed — feeding `no-publish` + through the page rendered the padlock, the border and an empty title. The title is + now a keyed lookup with a loud fallback that names the unrecognised token, so a + fifth state fails `type-check` here and, if one arrives from a server anyway, the + operator reads a sentence instead of a blank box. +- 9118a31: Preserve the producer's `userMessage` marking when `normaliseClientError` re-wraps a refusal. + + `ApiErrorSchema.userMessage` (objectstack#9934) is the opt-in channel an application author + sets at throw time to say "this text is for the end user", and the contract states it + status-agnostic — any refusal status may carry it. Both of the shapes this adapter re-wraps + into typed errors dropped the marking: a hook that refused a write with `VALIDATION_FAILED` + or `CONCURRENT_UPDATE` and marked its own sentence had that sentence discarded at the + adapter boundary, before any surface could render it. Nothing threw and the typed error was + otherwise correct, so the only symptom was the user reading a generic string instead of the + sentence their administrator wrote. + + The marking now rides both re-wraps, in the form the shared reader (`declaredUserMessage`) + already looks for: on the details bag for `DataApiValidationError`, exactly the way `fields` + already survives, and on a new readonly `userMessage` member for `ConcurrentUpdateError`, + which has no details bag. Unmarked refusals are untouched — they carry no key and still read + as `null`, so nothing a producer did not opt into can reach a user. +- f75810e: Parse a `droppedFields` wire entry's `fields` elements and its `object` at the + write-warning boundary instead of asserting them. + + The structural gate checked `Array.isArray(fields) && fields.length > 0` and then + asserted the entry into a type declaring `fields: string[]` and a required + `object: string` — reading neither. A response carrying `fields: [42]` reached + `onWriteWarning` subscribers typed as a field name (the shell rendered it as the + label `42`), and an entry that omitted `object` arrived claiming a string that was + not there. + + Now the wire type declares only what the gate establishes, and the notice is + parsed: non-string `fields` elements are refused, an entry naming no field at all + is dropped as `fields: []` already was, and a missing or non-string `object` is + healed from the object the write targeted. Warnings are never silenced for a + field the server really did name. No published type changes — `WriteWarningEvent` + and `DroppedFieldsNotice` keep their shapes, and a subscriber's `fields: string[]` + is now true rather than asserted. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [d2fb6ef] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [045d20b] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json index ddbfd882c7..7af0ccf699 100644 --- a/packages/data-objectstack/package.json +++ b/packages/data-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/data-objectstack", - "version": "17.6.0", + "version": "17.7.0", "description": "ObjectStack Data Adapter for Object UI", "license": "MIT", "type": "module", diff --git a/packages/fields/CHANGELOG.md b/packages/fields/CHANGELOG.md index c64ede5243..192f959019 100644 --- a/packages/fields/CHANGELOG.md +++ b/packages/fields/CHANGELOG.md @@ -1,5 +1,1580 @@ # @object-ui/fields +## 17.7.0 + +### Minor Changes + +- 8631c32: **BREAKING (stored data): `LocationField` reads and writes the spec's `{ lat, lng }`** + + FROM — the widget read `value.latitude` / `value.longitude`, each behind `|| 0`, and emitted + `{ latitude, longitude } | null`. + + TO — it reads and writes `LocationValue` from `@objectstack/spec/data` + (`{ lat, lng, altitude?, accuracy? }`), re-exported here rather than re-declared, and reads + nothing else. A pair is read only when BOTH `lat` and `lng` are finite numbers. + + **The behaviour change, stated plainly:** a `type: 'location'` record stored in the retired + `{ latitude, longitude }` spelling — including one this widget itself wrote before this + release — now renders **EMPTY in the edit surface**, where it used to render its + coordinates. It keeps rendering correctly in detail views, list cells and on the map, which + read `lat`/`lng` first. Re-saving the record through this widget, or fixing the value at the + data layer, restores it. There is deliberately **no compatibility fallback**: the maintainer + ruled the bare flip (2026-08-28, objectui#6272 option A1) explicitly over a dated read-side + shim, choosing zero dialect over softening this cost. + + Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` outside an + `@objectstack` major sync); the break is real and is stated here. + + **Why the widget was the side that moved** + + `@objectstack/spec@17.2.0` exports `LocationValue = { lat, lng, altitude?, accuracy? }` as + the canonical stored shape and deprecates `LocationCoordinates` (`{ latitude, longitude }`). + Measured through the contract itself, `valueSchemaFor({ type: 'location' })` **rejects** + `{ latitude, longitude }` with `invalid_type` at `[lat]` and `[lng]`, and **accepts** + `{ lat, lng }`. So this widget was the one `location` surface producing a shape the + platform's own validator refuses, and `LocationCellRenderer` / `ObjectMap` reading + `lat`/`lng` first is correct by contract, not tolerance. + + The user-visible defect it fixes: a spec-canonical `{ lat, lng }` record rendered **`0, 0`** + in the edit box — not an error state but a valid coordinate in the Gulf of Guinea — while + the same record rendered correctly one panel away. The `|| 0` defaults are gone with the + rename, so a half-stored pair (`{ lat }` alone) no longer invents the coordinate it is + missing; it reads as unset. A stored `{ lat: 0, lng: 0 }` still renders `0, 0`, because that + is now the only way those digits can appear. + + `GeolocationField` is **not** part of this change: `geolocation` is not a member of the + spec's closed `FieldType` union and its value schema accepts both spellings, so it keeps its + own `{ latitude, longitude }` shape. +- e552c31: fields: validate a STORED `location` value on an edit form (objectui#6744) + + `buildValidationRules` is the producer of the host-side `error` prop that every + field widget's published objectui#3222 slot reads, and it had no branch for + `location`. So a coordinate that was **already in the record** and violated the + spec's range was never validated on an edit form: the control rendered it, + nothing marked it invalid, and submitting re-wrote it unchanged. + + It now compiles a `validate.location` entry that adjudicates a present value + against `valueSchemaFor(field, 'stored')` — the platform's own value-shape + contract (ADR-0104 D1), the same schema the engine's record validator checks a + stored `location` against. An out-of-range stored value now marks the control + invalid, renders the spec's own complaint, and blocks the write; a legal value + is untouched. + + ⛔ The bounds are not restated in objectui. A hand-copied range would be a second + contract free to drift from the spec (AGENTS.md #0.1), so the schema is asked and + the message is built from its issues — the same discipline `LocationField`'s own + range refusal already follows. + + Deliberately unchanged: + + - **Input-time refusal (objectui#6714/#6716) is still the widget's.** A refusal + means `onChange` never fires, so the typed text never becomes a form value and + this rule is handed `undefined`. The two do not overlap. + - **Absence is `required`'s business.** The spec's schema refuses `null` and + `undefined` outright because it describes a *present* value, so the rule asks + core's `isMissingForRequired` — the repo's single presence contract — rather + than inventing a second definition of "empty". A create form with an untouched + location field is unaffected. + - **A field-authored `validate` keeps running**, composed under its own key + rather than replaced. + - **Scope is `location` only.** Whether other field types have the same + stored-value gap is a separate question and was not surveyed here. +- 7b90231: A `type="number"` field no longer displays one value and stores another in + silence (objectui#6780). + + `NumberField`, `CurrencyField`, `PercentField` and `GeolocationField` now + announce when the browser reports `validity.badInput` — it is holding text it + cannot read. The control is marked `aria-invalid="true"` and draws + `Not saved: the text in this box is not a number. Enter a plain decimal + (example: …).`, reusing the refusal shape objectui#6716 introduced for + `LocationField`. + + Measured in Chromium 141.0.7390.37 (Playwright 1.62.1), typing `1e` into an + empty number box leaves it **visibly displaying `1e`** while `.value` reads the + empty string. Before this change the widget emitted `null`, `aria-invalid` + stayed `"false"`, and nothing was said — on a money field. Nine keyboard + reachable states behave that way (`1e`, `1e-`, `1e+`, `5e`, `-`, `.`, `+`, `-.`, + `e`), and none of the six values a real browser actually emits trips the guard. + + Both a change arm and a **blur** arm are wired. Pasting `1e` into an empty box + never moves `.value` off `''`, so React's input-value tracking suppresses the + change event entirely and blur is the only arm that sees it. `PercentField`, + `NumberField` and `GeolocationField` had no `onBlur` before; the new one + composes any handler a host supplied rather than replacing it. + + The guard ANNOUNCES; it deliberately does not refuse. Refusing would leave the + React `value` prop unchanged, and React's `updateInput` writes it back over the + raw text — wiping the very entry the message points at. + + ⚠️ **Filtering truncation stays silent, and cannot be made otherwise.** Pasting + `1.2.3` into a currency field stores `1.23`; `0x10` stores `10`. The browser + discards those characters as they arrive, before any widget code runs, so no + widget-side guard can refuse them — only abandoning `type="number"` could, which + would reverse objectui#2572's deliberate `min`/`max`/`step` and mobile numeric + keyboard affordances. This asymmetry is documented for users in + `content/docs/guide/fields.md` and on the currency, percent and number field + pages, because a control that warns about `1e` while silently truncating `1.2.3` + teaches people that no warning means the value is right. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 0caacca: `GeolocationField` emits `null` for a cleared coordinate, not `undefined` (objectui#6848). + + Emptying a latitude or longitude box now emits `{ …, latitude: null }` where it previously + emitted `{ …, latitude: undefined }`. `CurrencyField`, `PercentField` and `NumberField` all + already emitted `null` for the identical user action, and `LocationField` emits `null` too — + this composite was the only widget of the class that did not. + + **Why `undefined` was the wrong sentinel.** It cannot survive serialization: `JSON.stringify` + drops an `undefined`-valued key outright, so the moment the emission left memory it stopped + saying "the user cleared this" and started saying nothing at all. `null` says it explicitly and + keeps saying it on the wire. + + **Scope — what this is NOT.** The card was filed on the reasoning that the dropped key reaches + a PATCH-shaped update as an ABSENT key, which conventionally means "leave this field alone", + so a cleared coordinate would silently fail to persist. That was measured before this fix was + chosen, and the second half does not hold for this widget: the dropped key is nested one level + below the key the write path merges on. The request body still carries the composite's own key + (`{ <field>: { longitude: … } }`), a `location` value is stored as a single JSON column, and + nothing on the path deep-merges — so the whole value is replaced and the cleared coordinate + does not come back. No silent data loss was found, and none is fixed here. What is fixed is + the emission: a widget that could not express "cleared" in a form that survives serialization, + in a class whose other members could. + + **`GeolocationValue` widened** — `latitude`, `longitude` and `accuracy` are now + `number | null | undefined`. `undefined` stays admissible, because an untouched coordinate is + genuinely absent; `null` is now admissible because a cleared one is explicitly empty. Code that + reads these coordinates with a falsy or `== null` test is unaffected. Code that distinguishes + `=== undefined` specifically will now see `null` after a user clears a box. + + A legitimate `0` coordinate (the equator, the prime meridian) is unaffected and is now pinned: + the emptiness test reads the raw input string, and `'0'` is not an empty string. +- f08bcd9: `FieldEditWidget` now delivers the NON-DOM half of the contract it declares (objectui#7008). + + objectui#7009 made the factory forward its declared DOM pass-through block. The rest of + `FieldWidgetComponentProps` was still dropped: `error`, `onUploadingChange`, and the whole + "Host plumbing" block (`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`, + `emptyHint`, `onSelectRecord`, `onCreateNew`). A host could pass any of them with no type + error and the widget never received it — the "declared but not delivered" class this + package treats as first-class. + + `error` was the live one. `InlineFieldInput` has passed `error` into this factory since + PR #7109 and the factory dropped it, so an inline-edit control that had failed validation + never reported `aria-invalid`: a sighted user saw the red hint, a screen-reader user was + told nothing. The kanban `RequiredFieldsDialog` had the same hole from the other side — it + computes the validation state and could not hand it over — and now passes `error`, so its + controls are marked. Delivering `error` buys the a11y MARKING only; the message text stays + with the host, per the objectui#3222 contract. + + The keys travel through a new sibling executor, `toHostProps` (exported alongside + `toDomProps`), never through the DOM whitelist — none of them is DOM-legal, and routing a + `dataSource` adapter there is the `[object Object]` leak that whitelist exists to stop. + Three compile-time assertions make the two executors partition the contract, so a future + declared key cannot go undelivered silently. + + `dataSource` precedence is stated rather than left to emerge: a host's explicit + `dataSource` prop WINS over `SchemaRendererContext`. That is the order `LookupField` + already implements; the factory is a conduit and resolves nothing. A host that passes no + `dataSource` keeps reading the context exactly as before, so no in-repo host changes + behaviour. +- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one + spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). + + **BREAKING, deliberately, with no deprecation window.** + + Two published contracts declared OPPOSITE dialects for the same four lookup keys, and + `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object + contract *refuses* outranked the one it *declares*: + + | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) | + |---|---|---| + | camelCase | **declared** | compile error (`TS2561`) | + | snake_case | refused (`unrecognized_keys`) | **declared** | + + `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the + snake members are **removed**: + + | before (removed) | after | + |---|---| + | `display_field` | `displayField` | + | `description_field` | `descriptionField` | + | `lookup_filters` | `lookupFilters` | + | `id_field` | `idField` | + + **Migration.** Rename those four keys wherever you author lookup or user field metadata + — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema` + that returns them. The old spellings are no longer read: a def still carrying + `display_field` falls back to the referenced record's generic name heuristic rather than + the field you named. + + `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name + hydration — committing a record field other than the id as the lookup's stored value — + which is picker behaviour with no `FieldSchema` twin, and none owed. + + **Not renamed** (outside this ruling's four keys, still snake on the widget bag): + `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`, + `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's + `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def. + + Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose + snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the + inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s + `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict + class; its gate is restated to assert the class no longer exists rather than passing + vacuously. +- 81a2eb1: One home for the `datetime` display convention (objectui#7443). + + `formatDateTime` gains a named `'compact'` style, selected through + `options.style` — the dense grid face, `7/4/2024 7:00 am` in `en-US` — which + `DateTimeCellRenderer` used to build from its own inlined `Intl` option bags. + The cell now reads `field.format` (it destructured `value` only, so a + `datetime` field could not reach the style vocabulary a `date` field has) and + renders through the shared function, and `data-table`'s `formatCellValue` + calls `formatDateTime` instead of a third, independently authored option bag. + Every existing cell without an authored `format`, and every cell authoring + `'compact'`, renders byte-identically; `'compact'` is today's face named and + rehoused, not a new one. A `datetime` field that authors any OTHER non-empty + `format` does change: the cell previously ignored `field` altogether and always + painted the compact face, and now anything other than `'compact'` selects the + verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in + `en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An + unrecognised value is neither rejected nor passed through; it silently lands on + that verbose face. No `datetime` field in this repository authors a `format`, so + no cell here moves — a consumer that authored one is the case this sentence is + for. Note that `format` has no declared value vocabulary to check a value + against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec` + carries one free-form `format?: string` on its shared field schema, described + "Format string (e.g. email, phone)" and accepting any string. `'compact'` is + therefore the only value with a defined `datetime` meaning, and every other + value means "the verbose face" by fallthrough rather than by design. + + Additive, no signature change: `formatDateTime(value, options?)` is unchanged + and `formatDateTime(v, { locale })` keeps meaning what it meant. + `DateDisplayOptions` gains an optional `style` key (read by `formatDateTime` + only; `formatDate` still takes its style positionally), and + `formatDateTimeCompactParts` is a new export of `@object-ui/core`, re-exported + by `@object-ui/fields`, returning the compact face as the two halves a grid + cell paints separately. `@object-ui/components` changes no rendered output — + the table's datetime cell is measured identical before and after in `en-US`, + `zh` and `de-DE`. +- 7ed9808: One home for the `date` display convention in the readonly field widgets + (objectui#8194). + + Four readonly `date` faces in `@object-ui/fields` called + `toLocaleDateString(locale)` with **no options bag at all** — `Intl`'s numeric + default — so they never implemented the year-dropping decision the shared + `formatDate` documents and every `date` CELL already follows. They now call + `formatDate` (default style): + + - the readonly `DateField` (the form / detail face, and what `FieldEditWidget` + renders in the grid and detail inline editors), + - the sub-grid `GridField`'s readonly `date` column, + - a `FormulaField` declaring `return_type: 'date'`, + - the lookup picker's plain-text `$date` fallback (`lookupColumnDisplay`), + which sits in the same function as the descriptor path that already rendered + through `formatDate`. + + **Visible change**: every one of those faces changes shape in every locale, in + every year — not only the year token. In `en-US` a date renders `Jul 4` this + year and `Jul 4, 2024` for a past year, where it used to render `7/4/2026` and + `7/4/2024`; in `de` `4. Juli` / `4. Juli 2024` for `4.7.2026` / `4.7.2024`; in + `zh` and `ja` `7月4日` / `2024年7月4日` for `2026/7/4` / `2024/7/4`; in `ar` + `4 يوليو` / `4 يوليو 2024`. Each now matches the `date` cell beside it. This is + a larger move than the sibling change in `@object-ui/components` + (objectui#7620), whose former face already asked for a short month and so only + lost its year token — these four passed no bag whatsoever. + + A value the formatter cannot parse now reads `—` at three of the four sites + instead of the literal `Invalid Date`. The sub-grid keeps showing the raw + stored string for an unreadable value, unchanged (objectui#3569). + + Untouched: the `datetime` readonly faces (`DateTimeField`, the sub-grid's + `datetime`/`time` branch). They are the same omission one type over, but their + home is `formatDateTime`, whose named faces are a separate display-convention + question; they are recorded on their own card rather than picked here. + + A surface that genuinely wants the year on every row is an explicit `format` + style honoured by both paths, not a second option bag — the objectui#7620 / + objectui#7443 / objectui#4576 lesson, one surface over. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- 9a1fb41: **API addition (public-surface widening):** `FileCell` — the compact upload + control `@object-ui/fields` exports for line-item grid cells — gains the + published optional `error?: string` slot, mirroring `LookupField` and + `FileField`: the same validation slot `@objectstack/spec/ui`'s + `FieldWidgetPropsSchema` declares and `FieldWidgetComponentProps` names + (objectui#3222). When set, `FileCell` puts `aria-invalid` on its own focusable + picker button; the message text stays with the host (objectui#5431). + + `GridField` now passes that slot for a required-but-empty `file` cell — the one + cell type objectui#3318's per-cell `aria-invalid` delivery left out. Before + this, a required `file` cell flagged only the visual ring and `title` on the + `td`; no element in the cell subtree announced the state, so assistive tech was + told nothing (a wrapper-only mark is exactly what objectui#5223 forbids). Text, + number, select, and lookup cells were wired in PR #5429; `file` cells now + behave identically. + +### Patch Changes + +- 06a8af5: `QueryParams.$filter` now declares both shapes the data sources actually accept — the + MongoDB-style field-keyed record, or a `FilterArray`, the ObjectQL AST sugar bound from + `@objectstack/spec/data` (objectui#3909). + + **Nothing is narrowed and no accepted value changes.** `Record<string, any>` already + accepted arrays structurally — they satisfy its string index — so the union documents + shapes that were always legal rather than admitting new ones. Measured both ways under + `tsc --strict`: all five inputs `translateFilterToAST` enumerates assign to the old and + new declarations alike, and both reject a bare number and a bare string identically. A + downstream `turbo run build` over all 43 dependent packages is green, which is the + evidence a published type change breaks no consumer. + + The harm was entirely on the type face, and it was two-sided. The declaration blocked + nothing while describing one legal shape as though it were the only one — objectui#3831 + is what that cost, a rule array accepted by a `Record<string, any>` slot, object-spread + flattened to `{"0": {...}}`, types green, and the query filtering on a column literally + named `0`. And someone writing a new consumer would read the type and its record-only + `@example`, conclude the array path was illegal, and add a tolerant conversion for it — + the "widen the consumer to tolerate the producer" shape AGENTS.md #0.1 forbids. Two + producers have fed arrays through this slot all along: `plugin-list`'s + `buildEffectiveFilter` (grid and export) and `plugin-view`'s `ObjectView` (calendar / + kanban / gallery / timeline). The runtime was right; the declaration was narrow. + + The array half is **bound** to the spec's `FilterArray` rather than restated locally, so + it cannot fork from the vocabulary the servers parse — the same failure two hand-written + operator lists had in objectui#3948. The doc comment names `translateFilterToAST` as the + authoritative accepted set instead of carrying a second list to drift from. + + `@object-ui/fields` drops the local cast this defect forced. PR objectui#3908 wrote + `filter as Record<string, any>` at one assignment in `useRecordQuery`, deliberately, as + debt rather than widening the shared type. `hasFilter` is now a type predicate narrowing + to the `$filter` slot's own type, so the assignment needs no cast and the guard cannot + drift from the declaration it guards. Type-only throughout; no runtime behaviour changes. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 88e9109: Declare the two consumed-but-undeclared field-metadata keys ruled on + objectui#6140 / objectui#6153 (maintainer 2026-08-25, Option A), and de-cast + the widget reads they legalize: + + - `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` (`@object-ui/types`) + — the inline-editor height `RichTextField` has always read through an + `as any` (default 8), following the `TextareaFieldMetadata` precedent. NOT a + spec key: `@objectstack/spec` `FieldSchema` refuses `rows` BY NAME + (`unrecognized_keys`) on all four of textarea/markdown/html/richtext, so it is + an objectui render hint that must not be written into authored object + metadata. The four inert editor keys (`toolbar`/`preview`/`minHeight`/ + `maxHeight`) stay deliberately undeclared and are pinned so. + - `SelectOptionMetadata.description` — secondary option text `LookupField` + searches on authored static options and emits from `recordToOption`. NOT a + spec key either: `@objectstack/spec` `SelectOptionSchema` is strict over + exactly `{label, value, color, default, visibleWhen}` and refuses + `description` BY NAME, and `FieldSchema` routes `options` through that schema, + so the key must never reach authored object metadata. + - `RichTextField` and `TextAreaField` (`@object-ui/fields`) now read their + metadata through the declared types instead of `field as any` (the spec-face + `maxLength` dual-read in `TextAreaField` stays as a documented structural + read). Behaviour unchanged; `rows` and option `description` are now legal to + author in an objectui **annotated literal** — never in an object document sent + to the platform. + + Both spec attributions above were corrected in place before release + (objectui#7537): as first written this changeset claimed each key was "aligned + with" a `@objectstack/spec` schema member that does not exist. Re-measured on + `@objectstack/spec@17.2.0`, each refusal is paired with a control that accepts + the same payload minus the key. Same correction as objectui#7014 / PR #7510 made + to the published JSDoc; the package bumps and the declared behaviour are + unchanged. +- 5d3a2d1: The capability picker localizes `manage_sharing` (objectui#6285). Before this, "Manage + Sharing" was the one platform capability in `sys_permission_set`'s picker that rendered in + English in every locale, beside seven siblings that translated — a user-visible missing + translation, in all ten packs at once. + + The cause was an unchecked copy. `CURATED_CAPABILITY_LABELS` in + `CapabilityMultiSelectField.tsx` listed seven capability names under a doc comment claiming + it mirrored `@objectstack/spec/security`'s `PLATFORM_CAPABILITIES`; the spec grew an eighth + member and the list did not follow, so `manage_sharing` fell through to the English label + the `sys_capability` registry serves. Nothing could catch it: the i18n gate reads that list + as this key family's vocabulary and checks the members it names — all seven had keys — and + no instrument compared the vocabulary to the array it was named after. + + `capability.label.manage_sharing` is now authored in all ten packs and in the field widgets' + provider-less defaults map, the list carries the member, and the prose claim is replaced by + a check: `CapabilityMultiSelectField.specParity-6285.test.tsx` imports `PLATFORM_CAPABILITIES` + and fails on any difference in either direction, reading the declaration through the i18n + gate's own source reader so what it pins is exactly what that gate consumes. `labelFor` also + gains a `defaultValue`, so a capability that arrives in a future spec bump before its + translation is authored degrades to the registry's English label rather than rendering a raw + i18n key at the user. +- dddb942: Delete the dead `metadata-admin/previews/object-fields-bridge.ts` module, and the three + prose references that still described it as wired. + + The module exported `bridgeFromDraft`, `commitToDraft` and `FieldsBridgeResult` and had + **zero importers** — re-measured on the merged base, not inherited from the filing. Nothing + in the repository could reach it either: `@object-ui/app-shell`'s `exports` map declares + only `.` and `./styles.css`, so the file was not addressable as a deep import even from + outside the workspace. + + Removing it is not the whole change. Three comments — in `types/src/designer.ts`, `types`' + `designer-field-types.test.ts` (twice) and `fields`' `richtext-cell-renderer-5452.test.tsx` + — cited the bridge as a live corroborating source. Left behind, they would have swapped + dead code for false documentation: three in-repo pointers telling a future reader that this + bridge mediates between the framework field record and `FieldDesigner`, and nothing telling + them it is unreachable. The two that named it as the consumer deriving an editable-subset + check from `DESIGNER_FIELD_TYPES` now name `MetadataFieldsPage`, which does exactly that + with the same idiom and the same `objectui#3017` anchor. The third cited the bridge's + `richtext` → `html` mapping as one of three corroborations that `richtext` stores HTML; the + other two (the showcase seed and the field-type decision tree) are live and carry the point + on their own, so that clause is dropped rather than repointed. + + No behaviour changes: nothing imported the module, so there is nothing to migrate. +- 98188c2: `LocationField` no longer discards a location's `altitude` / `accuracy` when the user + retypes the coordinate pair (objectui#6664). + + The widget edits the pair as one comma-separated text box and rebuilt its emission as a + fresh `{ lat, lng }` from the parsed text, so the two OPTIONAL keys `@objectstack/spec` + declares alongside them — `LocationValue` is `{ lat, lng, altitude?, accuracy? }` — were + gone the moment anyone edited the coordinates. Nothing warned; they simply were not in + the object handed to `onChange`. Both keys are registered on the platform's authorable + surface (`authorable-surface.base.json`), so a customer may author them even though the + platform itself produces neither today — measured in both repos. + + The drop **predates** objectui#6272: before that flip the widget emitted + `{ latitude, longitude }` and discarded the rest identically. What #6272 changed is only + that the *declared* value type is now the spec's, so the type claimed four keys while the + write path handled two. This closes that gap; it is not a regression #6272 introduced. + + The carry is a key-by-key pick of exactly those two keys out of a value that is already a + valid `LocationValue` — deliberately **not** a spread of the incoming value, which would + carry a stored record's retired `latitude` / `longitude` spelling straight back into the + emitted object and undo #6272's rename. A negative control pins that. Each key is taken + only when it is a usable number, because the spec's `z.number()` rejects `NaN`, `Infinity` + and a numeric string alike; leaving such a value behind narrows the emission rather than + widening what the widget accepts. +- f46bd39: `LocationField` no longer emits a coordinate pair the platform's own validator + refuses (objectui#6714). + + `@objectstack/spec`'s `LocationValueSchema` constrains the coordinate **range** + (`lat` −90..90, `lng` −180..180), but the widget's guard tested only that each + coordinate was a finite number. Typing `999, 999` therefore emitted + `{ lat: 999, lng: 999 }` — a value `valueSchemaFor({ type: 'location' })` + rejects with `too_big` at both keys. That is the producer direction of the + contract-first failure class (AGENTS.md #0.1): a renderer writing what the + contract rejects. It was open to every user who edits a location field, since + typing the coordinates is this field's only interaction. + + **Measured before choosing the disposition**, as triage required: nothing + downstream rejects or repairs the value. Driving a real `ObjectForm` with a + `type: 'location'` field and typing `999, 999` called `dataSource.create` once + with `place: { lat: 999, lng: 999 }` verbatim, `aria-invalid="false"` on the + control and no error text anywhere. `sanitizeFormData` filters keys and never + inspects a value, and `buildValidationRules` has no `location` branch. So the + out-of-range pair reached storage silently, and the widget is the only place a + refusal can work. + + The fix therefore **refuses the emission**, extending the rule this widget + already applies to text that isn't a coordinate pair from *format* to *range*: + the typed pair is simply not written and the prior value stands. No new UI and + no new mechanism — the same `// If invalid, don't update the value` branch. + + The bounds are **not** restated in the widget. A hand-copied `-90..90` would be + a second contract free to drift from the spec, so the emission is put to + `LocationValueSchema` itself. Two consequences of asking the schema rather than + testing two bounds by hand: the check covers the WHOLE emitted object, so the + `altitude`/`accuracy` carried across an edit (objectui#6664) are held to the + contract too; and `Infinity` is refused as well, which the finiteness gate let + through (`parseFloat('Infinity')` is `Infinity`, and `!isNaN(Infinity)` is + `true`). + + Reading is deliberately unchanged: a record that already holds an out-of-range + pair still renders in the box, so the person who can correct it can still see + it. objectui#6272's empty render was for a value whose *shape* this widget + cannot read; this shape is readable, it is only not writable. +- b98352a: `LocationField` no longer invents a coordinate out of text that is only partly a + number (objectui#6715). + + Each half of the typed pair was read with a bare `parseFloat`, which stops at the + first character it cannot read and returns what it got. So `"12abc, 34"` emitted + `{ lat: 12, lng: 34 }` — a coordinate nobody typed. + + **Why nothing downstream could catch it, and why that makes this different from + objectui#6714.** Every one of those truncations is a pair + `valueSchemaFor({ type: 'location' })` ACCEPTS: well-formed, in range, and wrong. + #6714's `999, 999` was at least a value the contract refuses, so something + downstream could in principle have objected; here the platform validator cannot + be the oracle at all. Measured on `b76ca6764` by driving a real `ObjectForm` + (create mode, a `type: 'location'` field, a fake `DataSource`) and submitting: + + ``` + typed "12abc, 34" create({ place: {"lat":12,"lng":34} }) aria-invalid=false + typed "1.2.3, 4" create({ place: {"lat":1.2,"lng":4} }) aria-invalid=false + typed "12deg, 34" create({ place: {"lat":12,"lng":34} }) aria-invalid=false + typed "0x10, 34" create({ place: {"lat":0,"lng":34} }) aria-invalid=false + typed "12.5 N, 34 E" create({ place: {"lat":12.5,"lng":34} }) aria-invalid=false + ``` + + The last two show the size of the class. `0x10` truncates to `0` — objectui#6272's + `|| 0` in the Gulf of Guinea, arriving through a different door — and + `"12.5 N, 34 E"` drops the hemisphere, so a `12.5 S` paste would have been stored + as `+12.5`, on the wrong side of the equator, with nothing said. + + **The fix** parses each half as a strict whole-string number, applying + objectui#6272's precedent: a field that renders a plausible wrong place is worse + than one that renders nothing. The test is `parseFloat`'s OWN grammar, anchored — + not a stricter notion of a number invented in the widget — so every form that is + a number today still is: negatives, a leading `+`, surrounding whitespace, + exponent forms (`3.027e1`), and a bare decimal point on either side (`.5`, `30.`). + + The refusal is **announced**, through the machinery objectui#6716 landed rather + than a new one, and it names the half it could not read: *Not saved: latitude + "12abc" is not a number. Enter plain decimals (example: 30.2741, 120.1551).* A + third silent refusal would have re-opened the defect #6716 had just closed. + + Two boundaries drawn deliberately: + + - Text with **no** number at the front (`abc`, `NaN`, `here, there`) keeps the + pre-existing format sentence. "No number at all" and "a number with text after + it" are different mistakes and get different advice. + - `Infinity` carries no residue — `parseFloat` reads the whole word — so it is + still refused by objectui#6714's **range** arm, not by the new one. + + ⛔ Degree/hemisphere notation (`12°N, 34°E`) is **not** parsed. It stays refused, + per the maintainer ruling of 2026-08-29: the paste route is unmeasured, and it + becomes its own feature card if real demand arrives. +- b76ca67: `LocationField` says WHY it refused an edit, instead of refusing in silence + (objectui#6716). + + The widget refuses to emit for input it cannot accept, and used to say nothing + when it did. Two refusals shared that silence: text that is not a + comma-separated pair (pre-existing), and a pair outside the spec's coordinate + range (objectui#6714). In both, `onChange` was never called, so the typed text + vanished with `aria-invalid` reading `"false"` throughout — a screen reader was + told the control was fine right after it had rejected the entry. + + - Both arms now render a short reason and set `aria-invalid` on the control. The + range message is built from `LocationValueSchema`'s own issues, never from a + hand-copied `-90..90`, so it cannot drift from the spec. + - The box now HOLDS the refused text, so the message has something to point at + and the entry can be corrected in place. Measured first without it: with the + value derived straight from the stored one, React restores the control in the + same tick, so typing a valid coordinate one character at a time left the box + empty, stored nothing, and lit a refusal on the final keystroke too. + - Refusal is unchanged: a coordinate the platform validator rejects is still + never emitted, and never stored. The published objectui#3222 `error` slot keeps + its single author (the form renderer); the widget's own state is separate, as + `ObjectField`'s `parseError` already is. +- b392674: Field widgets say WHY they refused an edit in the reader's language + (objectui#6755, maintainer ruling 2026-08-29). + + Three sentences a person has to read to recover from a refusal were string + literals in the widgets, inside a package whose locale channel 11 of its 55 + widgets already use: `ObjectField`'s `Invalid JSON`, and `LocationField`'s + format and range refusals (objectui#6716 / #6714). So a zh / ja / ar user who + mistyped a coordinate or a JSON blob was told why in English, in a form whose + labels, gate hints and validation copy were all translated. + + - All three now read from `useFieldTranslation` / `FIELD_DEFAULTS` under + `fields.object.invalidJson`, `fields.location.refusedFormat` and + `fields.location.refusedRange`, with entries in all ten locale packs — bound + from now on by `check:i18n-drift`. + - The `en` values are byte-identical to the literals they replace, so English + and provider-less rendering are unchanged, and the refusal pins of + objectui#6716 / #6715 and `plugin-form`'s two refusal suites are untouched. + - `fields.location.refusedRange` keys the FRAME only: the interpolated + `{{detail}}` is `LocationValueSchema`'s own complaint, because the widget must + not restate the spec's bounds (a hand-copied range is a second contract). + - Not in scope, and recorded rather than folded in: `LocationField`'s third + refusal sentence — the residue arm objectui#6715 added after the ruling was + written — is still a literal. objectui#6888 carries it. +- 8579e34: `CurrencyField` and `TagsField` now compose a host-supplied `onBlur` instead of + overriding it (objectui#6802). + + `onBlur` is a DECLARED DOM pass-through key — named in `FieldWidgetDomProps` + and in `SDUI_DOM_PASS_THROUGH_KEYS`, and forwarded by `toDomProps` — but both + widgets wrote their own `onBlur={…}` AFTER the `{...toDomProps(props)}` spread, + so the host's handler was overwritten and never reached the control. Each now + resolves `toDomProps(props)` into `domProps` and calls `domProps.onBlur?.(e)` + at the end of its own handler, the idiom the other four widgets of this package + already use. + + ⚠️ This is a REAL behaviour change, not the no-op the finding was filed as. The + form renderer hosts every field through react-hook-form's `Controller` and + spreads the controller field — `{ name, value, onChange, onBlur, ref, disabled }` + — into the widget's props, so the overridden handler was the one that marks a + field touched and runs its validation. Concretely: on a form declaring + `validationMode: 'onBlur'` or `'onTouched'`, currency and tags fields were + silently opted out of blur-mode validation while every sibling field type kept + it. They now behave like the rest. + + Currency keeps emitting its rounded value before handing the event on, so a + blur-mode validator reads the parsed amount rather than the raw text; tags + still commits the typed draft first, so the validator reads the committed list. +- d57db5d: `NumberField` now reads the published `error` validation slot, so a number + field marked invalid is announced to assistive tech by the widget itself + (objectui#6803, closing an objectui#3222 gap). + + The widget destructured `{ value, onChange, field, readonly, ...props }` with + no `error`, so the slot landed in the open tail and `toDomProps` — a whitelist + — dropped it. It wrote `aria-invalid` only while its own bad-input refusal was + active, which meant that on any host that does not hand a value down itself, + an invalid number field carried no `aria-invalid` at all. + + `error` is now wired and the conditional spread becomes the ordinary + `aria-invalid={!!error || !!refusal}` the sibling number widgets already use. + Both halves ship together on purpose: reading `error` is what makes an + unconditional attribute safe to write, and leaving the attribute conditional + would have kept the wiring invisible. Un-conditionalising WITHOUT reading + `error` is the regression this pairing forbids — it would stamp `"false"` over + the correct value `FormControl`'s Radix Slot hands down. +- 320374d: Key `LocationField`'s THIRD refusal sentence — the residue arm — into the locale + packs (objectui#6888). + + Typing a half that is only PARTLY a number (`12abc, 34`) is refused by + `LocationField` with its own sentence, added by objectui#6715. objectui#6755 had + ruled two weeks earlier that a widget's own refusal sentence goes through + `useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences — but it was + written on 2026-08-29 14:53 and this arm landed after, so it stayed a hard-coded + English literal while its two siblings were keyed. + + **The consequence was worse than one more English string.** All three refusal arms + render through the SAME `<p>` and the same `refusalError` state, so after #6755 + landed that one line spoke the reader's language when the format or range arm fired + and English when the residue arm did — objectui#4028's shape ("four Chinese labels + around one English one") compressed into a single sentence position, which reads to + a user as a bug rather than as a missing translation. + + **Arity is answered explicitly, not defaulted.** Unlike the other two sentences, this + one had grammatical number: `verb` was `is not a number` / `are not numbers`, chosen + in TypeScript. Handing a pack an English verb form through a `{{hole}}` gives it a + fragment it cannot inflect around — Arabic has a DUAL, and two halves is exactly that + case. So the verb is not a hole. It lives inside two SIBLING keys picked at the call + site, `fields.location.refusedResidue` and `fields.location.refusedResidueOne`, + following this repo's own plural convention rather than i18next's `_one`/`_other` + suffixes — the same shape `RecordPickerDialog` already uses in this very defaults map + (`lookup.recordCount` / `lookup.recordCountOne`), and for the reason `ReactionPicker` + states in source: zh/ja/ko have no separate singular form, would legitimately omit a + `_one` half, and `all-locales-key-parity` reads that as a missing key. The `ar` pack + now uses its dual (`ليسا رقمين`), which the old implementation could not have produced. + + The English conjunction `' and '` and the coordinate NOUNS go the same way: each pack + writes its own conjunction inside the two-half value, and `latitude` / `longitude` + become `fields.location.latitude` / `fields.location.longitude`, keyed once each and + interpolated into both arities so no locale holds two spellings of the same word. The + only holes carrying untranslated data are `{{text}}` / `{{otherText}}` — the + characters the person actually typed. + + **No behaviour moves.** The English values are byte-identical to the literal they + replace in both arities, verified by `check:i18n-drift` (0 en values changed, 4 added) + and by objectui#6715's own `LocationField.strictNumeric.test.tsx` and `plugin-form`'s + `ObjectForm.locationResidue.test.tsx` passing untouched. Provider-less rendering is + unchanged, the refusal itself is unchanged, and the four new keys are bound from here + on by `check:i18n-keys` and `all-locales-key-parity` like their three siblings. +- b458300: `FieldEditWidget` now DELIVERS the DOM pass-through block it DECLARES + (objectui#6909). + + Its props are `FieldWidgetComponentProps` — the controlled-input keys + intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-` + family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`, + `onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type + error. The body then destructured five keys and rendered the widget with those, + so `autoFocus` was the ONLY survivor of the whole block and everything else was + silently dropped. That is this package's own first-class defect class, named in + `widgets/toDomProps.ts`: a key that type-checks, reads as supported, and + silently never reaches the element (objectui#3290's `aria-required`, + objectui#3222's validation slot). + + Not a widening, and not a contract change. The keys were already declared, and + each widget still re-filters through its own `toDomProps` before anything + reaches a DOM element — what any widget accepts or rejects is unchanged. The + factory was simply the one link in the chain nothing bound to the declaration: + `toDomProps` binds the WIDGET contract to its whitelist with compile-time + assertions in both directions, and the factory sat above them, bound to + neither. + + The fix hands the widget `toDomProps(props)` — this package's own executor — + rather than a second key list written out in the factory. That reuse is the + guard: `toDomProps.ts`'s direction-2 assertion already makes + `keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to + violate, so a key added to the declared DOM block now reaches the widget + through this factory automatically. One mechanism, one judge — a private list + here would have been free to drift, which is how the factory came to deliver + one key out of seven. + + The forwarded set is a deliberate superset of `FieldWidgetDomProps`: it also + carries `className` and `disabled`, declared on the controlled-input block and + forwarded by the same executor for the reason stated there — withholding them + makes it a silent styling- and interactivity-dropper. The semantic props + (`field`, `value`, `onChange`, `readonly`, and `compact` for the relational + pickers) stay explicit and are applied after the spread, so a host cannot + displace them. + + **No host in this repo changes behaviour.** Measured on all three call sites + before the fix: `ObjectGrid.renderCellEditor` passes `{ field, value, onChange }`, + `InlineFieldInput` passes those plus `autoFocus` (the key that already worked), + and `RequiredFieldsDialog` passes those plus `readonly`. None passes a dropped + key, so this is a plain repair rather than a live regression — but + `RequiredFieldsDialog` had already worked *around* the drop, wrapping each + control in a `label` because "`FieldEditWidget` … takes no `id` to associate + with". It does now. + + Also corrects a comment in `@object-ui/components`' `data-table.tsx` that this + change falsifies. It justified the injected editor's document-level + `pointerdown` listener partly with "`FieldEditWidget` forwards `autoFocus` and + nothing else out of the DOM block, so a host handler could not reach the + control through it even if one were passed" — no longer true. The listener is + still load-bearing for the other half of that reason, which is untouched: the + `renderCellEditor` context object has nowhere to put an `onBlur` in the first + place. Comment only; no behaviour change in that package. +- 39d69ad: Stop shipping `dist/__tests__/numberInputBrowserReadings.d.ts` in the published tarball + (objectui#6943). `packages/fields/tsconfig.json` now excludes the tooling DIRECTORIES + (`__tests__`, `__mocks__`, `__benchmarks__`), not just the `*.test.*` NAME. + + `numberInputBrowserReadings.ts` holds the measured Chromium/happy-dom readings the number + widget suites share. It is deliberately not a `*.test.ts` — it carries no assertions — so + the name-only exclude list did not catch it, and it was emitted into `dist` and published + while its 79 neighbours in the same directory were kept out. That made + `check:published-dist` red on `main`, and because the same script is the first link in + `changeset:publish`, it also failed the publish command at its first step. + + This is the third instance of the same name-versus-directory mismatch (objectui#4006 here, + objectui#4836 in plugin-grid / plugin-view / plugin-designer), so the exclude table is now + the directory convention itself rather than a list of names to extend. + + Which program had to be fixed was measured rather than assumed, because this package's + build is `tsc && vite build` and the `tsc` leg inherits the root's `noEmit`: run alone the + `tsc` leg exited 0 and wrote zero files, while `vite build` alone produced the whole + 81-file output including the offending declaration. vite-plugin-dts is the emitting + program, and it builds its declaration program from this package's `tsconfig.json`, so + that is where the exclude belongs. + + No type coverage moves with the change and no API surface moves: `numberInputBrowserReadings.ts` + is the only file the directory patterns newly remove from the build program, and the + `tsconfig.test.json` chained off `type-check` already reads it as a transitive input of the + three suites that import it. The name patterns stay, because 52 `*.test.ts(x)` files in this + package sit outside any `__tests__/` directory. +- 639114c: Carry `manage_org_presentation`, the ninth platform capability (objectui#7122). + + `@objectstack/spec` 17.3.0 declares a ninth member of `PLATFORM_CAPABILITIES` + and the capability picker's curated set carried eight, so + `CapabilityMultiSelectField` fell back to the `sys_capability` registry's + English label for it in every locale — the exact defect objectui#6285 filed + when `manage_sharing` did the same thing. + + The label is the spec artifact's own (`Manage Organization Presentation`), read + off the installed build rather than invented, and it is authored everywhere the + widget's docblock requires of any edit to that list: `useFieldTranslation.ts` + and all ten locale packs. Each non-English string is composed from that pack's + own established sibling vocabulary (`manage_org_users`, + `manage_platform_settings`) rather than machine-translated; a native review pass + is welcome on the nine, and nothing about the capability's behaviour depends on + the wording. + + The parity pin is unchanged and still fails on ANY difference in either + direction, which is what made this visible before it reached a screen. +- e8e4c4d: The last five inline edit widgets read the delivered `error` slot, so a failed + required `text` / `boolean` / `date` / `datetime` / `time` control finally + reports `aria-invalid` (objectui#7126). + + objectui#7008 made `FieldEditWidget` DELIVER the declared `error` key to + whichever widget it resolves. Of the 27 distinct components in `EDIT_WIDGETS`, + 21 read it; five did not — `TextField`, `BooleanField` (serving both `boolean` + and `toggle`), `DateField`, `DateTimeField` and `TimeField` — so for their field + types the delivery was inert and the attribute was still never set. + + `text` being in that set is what made this a live defect rather than tidiness. + It is the most common field type in any object, so it is the likeliest thing a + kanban column makes required: `RequiredFieldsDialog` computed the failure, drew + the red "Required" hint, handed the state to the control, and the control said + nothing to assistive tech. The grid's inline cell editor and the detail page's + inline edit (`InlineFieldInput`) compose the same seam. + + Each of the five now computes `aria-invalid={!!error}` **after** its DOM + pass-through spread — one existing idiom, the objectui#3222 discipline the other + 21 already share, so a valid field says an explicit `"false"` rather than staying + mute. Two judgements worth stating: + + - **The FORM path was never broken and is unchanged.** `<FormControl>` is a + Radix `Slot` whose `aria-invalid` reached each control through the props + spread; the form also produces `error`, so the widget's own computation now + agrees with the value it replaces. The gap was every host WITHOUT that Slot. + - **`BooleanField` is the one composite here, and the mark goes on the + control.** Its Radix `Checkbox` / `Switch` renders a real + `button[role=checkbox]` / `button[role=switch]`; the wrapping flex `div` is + deliberately not the target, because a wrapper mark satisfies a subtree query + while telling a screen-reader user nothing (objectui#5223). The three + date/time widgets each render one native input, so the browser's picker raises + no second-element question. + + This buys the MARKING only. The objectui#3222 slot drives `aria-invalid` and + renders no text: the visible message stays with the host, and nothing that was + invisible becomes visible. +- 48c19bd: Render a dataset measure over a date field as a date (objectui#7178, maintainer + ruling 2026-09-02, director summon #8 — option A). + + `formatMeasure` opened with `if (typeof v !== 'number') return String(v)`, + placed **before** `format` was ever read. So a `min` / `max` measure over a date + or datetime field printed its stored value verbatim — a 24-character ISO string + in the KPI tile's `text-2xl font-semibold`, wrapping to two lines — and the + `format` that `DatasetMeasureSchema` accepts was unreachable for those values. + A date-shaped value now routes to the date display path before that + short-circuit, so all four dataset-bound surfaces are served at once: the metric + tile, chart values, dataset table cells, and the metadata-admin dataset preview. + + `min` / `max` over a date stays a legal measure; nothing in `@objectstack/spec` + narrows. `PivotTable` takes a `number` outright and is unchanged. + + **No second date formatter was written.** `formatDate`, `formatDateTime`, + `formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`' + barrel down into `@object-ui/core` (`utils/date-display.ts`), which is the same + remedy objectui#4576 applied to `formatDisplayNumber` and for the same reason: + `core` is the React-free engine and could not import from a React package, so + the alternative was a parallel date convention in `dataset-format.ts` — exactly + the drift that once had a list cell rendering `1.234,5 %` beside a dashboard + measure's `1.234,5%`. `@object-ui/fields` re-exports all four names unchanged, + so no consumer's import path or behaviour changes, and a reference-identity test + pins that the cell renderer and the measure formatter call the same function. + + **What `format` can say for a date measure, measured rather than assumed.** The + shared date path takes a named STYLE, not a date pattern: `'short'` and + `'relative'` are honoured — the same words `DateCellRenderer` honours from + `field.format` — while a pattern such as `'YYYY-MM-DD'` renders the locale + default. That limit is unchanged by this release (`plugin-dashboard`'s + `recordFields` already routed a date-shaped `format` into the same style slot) + and is now pinned by a test instead of being silent. + + **Numeric measures are byte-identical.** 33,696 argument forms + (value × format × currency × percentScale × locale) were compared against a + verbatim copy of the pre-fix function: the only values that moved were the four + ISO-shaped, parseable ones. Numbers, numeric strings (`'1751612400000'`, + `'2026'`), the nullish em dash, arbitrary prose and non-strings all render + exactly as before. +- a865c73: Grid field widget: announce a form-level validation failure to assistive tech. + + A required `grid` submitted while still empty rendered its "is required" message + but marked nothing — every row was a ghost row, and ghost rows were skipped by + the widget's per-cell validity channel. A sighted user saw the red message; a + screen-reader user was told nothing at all. + + The host failure now drives the per-cell channel the widget already owns: when + the `error` slot is set on an empty grid, the ghost entry row's required cells + flag, and the mark sits on each cell's own control rather than on the `td` + wrapper (a `td` is not focusable, and assistive tech reads validity from the + control). Populated grids are unaffected — they already marked their own empty + required cells inline. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- e4e9557: A multi-value lookup cell no longer grows its row without bound: `LookupCellRenderer` + now shows at most 3 chips and collapses the rest into one muted `+N` chip, the same + cap `UserCellRenderer` has always applied to its avatar stack in the very same file. + + Previously the array branch rendered EVERY referenced record as its own chip inside a + `flex-wrap` container. In a grid column that wraps to one chip per line, so a cell + referencing a large set — a production 排班计划 row referencing 60+ work objects — grew + a single row to several screens of height and blew the page layout apart. The same + uncapped rendering reached every surface that resolves through `getCellRenderer('lookup')`: + grid, related lists, gallery, kanban, report and dashboard tables, and the record detail + sections. + + The collapsed names stay reachable: the `+N` chip's `title` lists the display names of + the hidden references (resolved through the same option/label/record-name path as the + visible chips), and the record's own detail view remains the place to see the full set. + The first 3 chips keep their per-record links (#4336) and their resolution order — + nothing changes for cells with 3 or fewer references. +- 7a28e1e: A lookup's inline dropdown renders its columns through the same cell renderer the browse-all picker uses, so one `lookup_columns` declaration cannot produce two answers. + + A form's lookup field offers two ways to pick a related record, and both read + the same declaration: the inline dropdown under the field, and the + "browse all records" picker behind it. The picker resolved every cell through + the type-aware cell renderer. The dropdown did not — it printed + `record[descriptionField]` verbatim into the option subtitle and concatenated + `label: String(rawValue)` into the row's `title` attribute. Measured on the + same declaration, on a real 17.1.0 deployment: + + ``` + column inline dropdown (before) browse-all picker + lookup T5MsMCuwP4t_yUHq (bare FK id) the related record's name + date 2026-08-20T00:00:00.000Z (ISO) a formatted date + select pending (enum code) the authored option label + ``` + + Both surfaces now call one shared module — `widgets/lookupColumnDisplay.tsx`, + which owns column normalisation, the field-descriptor enrichment from the + referenced object's schema, and the render itself. The picker's own + `renderCellContent` and `columnFieldDescriptors` are now thin calls into it, so + there is a single renderer left to drift from. The dropdown's extra columns are + rendered into the option row itself; the row's `title` keeps the full option + label, which is what a truncated label needs, instead of a raw-value dump. + + No query changed and no contract widened. `lookupColumns` entries stay bare + field names — no dot paths, no populate/expand semantics — because neither + surface's request carries populate to begin with: the picker resolves a + foreign-key id to a name client-side, in the lookup cell renderer, and the + dropdown now inherits exactly that. An unresolved reference therefore renders + what the picker renders for it, and keeps its column: a slot is dropped only + when the record holds no value for the field, decided on the raw value and + never on what the renderer makes of it, so an unresolved id can never degrade + into a silently empty column. +- b6e83be: **Bug — a `code`/`text` value whose text is JSON rendered as the literal `[Object]`.** `coerceToSafeValue` classified strings by SHAPE: any string starting `{`/`[` and ending `}`/`]` was `JSON.parse`d and the result run through the reference-label extraction (`name || label || externalId || id || _id || '[Object]'`), which answers the placeholder for an object carrying none of those keys. Every text-like cell reaches that helper — `text`, `textarea`, `code`, `time`, `auto_number` and `qrcode` all register to `TextCellRenderer` — so a stored `{"ok": true}` displayed as `[Object]`, and `[1, 2, 3]` in a text field displayed as `1, 2, 3`. + + A string is now returned verbatim, whatever its shape. The reference case the parse was written for (an unresolved external-id reference arriving as `'{"externalId":"…"}'`) belongs to reference-TYPED columns and is already handled there: `LookupCellRenderer` carries its own JSON-string branch, which resolves the label through the referenced object's schema and links to the record — neither of which the type-blind helper could do. The behaviour is scoped to the column type that owns it, not dropped. Object and array VALUES still coerce, so React error #310 stays fixed. +- 4bb940b: A readonly `markdown` / `html` / `richtext` form field now renders its content + FORMATTED instead of showing the user its markup source (objectui#5498). + + `RichTextField`'s readonly early return rendered `{value}` as a React text child, + so a readonly field of any of those three types displayed the stored markup as + literal characters — a markdown field's asterisks and hashes, a richtext field's + tags. The `prose` classes on that wrapper were the tell: they style rendered rich + content, and there was none to style. Every other read surface — grid, kanban + card, gallery, related list, dashboard record panel and the record detail page's + read mode — dispatches through `getCellRenderer` and rendered the same stored + bytes formatted, so one field disagreed with itself depending on which surface it + was read on. + + The readonly branch now renders through the same components `getCellRenderer` + resolves: `markdown` through the GFM renderer, `html` and `richtext` through the + sanitizing HTML renderer. The two renderers moved out of the package barrel into + `widgets/richTextDisplay.tsx` so the widget can reach them without importing the + barrel back, and both sides now read one shared type-to-renderer table rather + than two that can drift apart. + + The editor header's format label is fixed with it: it was computed as + `field.format || 'markdown'`, and `format` is declared on `date` / `datetime` / + `time` / `phone` / `auto_number` and on no rich-content type — so it read + `undefined` for every real field and labelled an `html` field "Format: markdown". + The label is now derived from the field type's display pipeline, so it names the + syntax the value is actually stored in. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- f1690d4: A populated `richtext` field no longer renders as a blank cell (objectui#5452). + + `richtext` stores HTML — the spec documents the type as "Formatted content with + HTML/WYSIWYG", the showcase seed's own specimen is `<p>Rich <strong>text</strong></p>`, + and this repo's designer bridge already maps `richtext` onto its `html` type. The + display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose + sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops + raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the + cell body came out empty — with no error, no fallback and no console warning, so a + populated field read as an empty field and anyone auditing data through a grid + concluded the records were blank. Measured on the same stored bytes, a neighbouring + `html`-typed column rendered them correctly, which is what ruled out "the value never + arrived". + + `richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` + (script/style/iframe/object/embed blocks, inline event handlers and `javascript:` + URLs removed) and keeps everything a rich-text editor legitimately emits — headings, + paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at + once: the grid, the kanban card, the gallery, the related list, the dashboard record + panel and the record detail page all resolve their read-mode cells through this same + `getCellRenderer`. + + The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" + one type by moving every `markdown` cell's trust boundary, so `markdown` still drops + raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [a1c41c5] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/providers@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/fields/package.json b/packages/fields/package.json index 8177869d7c..8f69a77c33 100644 --- a/packages/fields/package.json +++ b/packages/fields/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/fields", - "version": "17.6.0", + "version": "17.7.0", "description": "Field renderers and registry for Object UI", "license": "MIT", "type": "module", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 5fa542ac97..f173cf537d 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,1794 @@ # @object-ui/i18n +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- d3995fe: Retire 25 confirmed-dead locale keys from all ten packs — 250 translated strings + with no reader anywhere in the repo (objectui#4730's key-level trim round; + `calendar.agenda` closes objectui#5783). + + Every key was confirmed individually, not swept from a tool's output. The + inventory comes from `scripts/check-i18n-dead-keys.mjs`, which stays report-only + by design, and each candidate then had to clear the objectui#4658 evidence + standard on its own: zero `t()` call sites, zero textual footprint anywhere + outside the packs, and a read of its plausible consumer confirming no i18n + wiring reaches it. Five namespaces held nothing but retired leaves and went with + them — `map`, `cellRender`, `rowAction`, `recordDetail`, and `home.stats`. + + The retirements fall into three shapes: + + - **Superseded twin vocabularies.** `cellRender.*` and `rowAction.*` duplicated + a `grid.*` vocabulary that won. `RowActionMenu.tsx` is fully i18n-wired and + reads `grid.openMenu` / `grid.edit` / `grid.delete`; `ObjectGrid.tsx` reads + `grid.empty` / `grid.yes` / `grid.no` / `grid.systemFields`. The twins had no + reader on either side. + - **Labels that outlived their control.** `calendar.agenda` labelled a view mode + objectui#5740 retired from `CalendarViewMode` (now `'month' | 'week' | 'day'`). + `home.quickActions.createApp*`, `layout.systemNav.createApp`, + `actionDialog.defaultActionTitle` / `.ok` and `grid.bulk.selectPlaceholder` + sit in namespaces whose consumers are live and wired but demonstrably read + other siblings. + - **Surfaces that left the product.** `map.*` is the strongest form: + `@object-ui/plugin-map` declares no `@object-ui/i18n` dependency and contains + no `t()` call at all, so it cannot consume a locale string. `home.stats.*` and + `recordDetail.viewersTooltip` name surfaces nothing renders. + + `packages/i18n/src/__tests__/dead-key-batch-retired-4730.test.ts` pins the + retirement, following the convention of the five retirement pins already in that + directory. It is load-bearing rather than decorative: every i18n gate in this + repo runs call site to key, so a dead key coming **back** into the packs is + invisible to all of them, and this pin is the only thing watching that direction. + + **Deliberately NOT deleted, and pinned as live.** Seven `console.*` bootstrap + strings that this same sweep reported CONFIRMED-dead are in fact **live**, and + were pulled back out of the batch. `LoadingScreen.tsx` is bootstrap-critical UI + that must render before i18n loads — precisely when the server is unreachable — + so it deliberately does not call `t()`. It imports the packs directly and reads + them as plain object properties (`strings.loadingSteps.connecting`). That + consumer is invisible to both legs of the sweep: there is no call for the AST + pass to classify, and the full dotted key is never spelled in source because the + namespace segment is bound to a local variable. The new pin asserts those keys + stay, so the next sweep round cannot repeat the mistake. +- b362c1b: Setup gains a **Packaged automation** page — the operational surface for the flows an + installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other + framework-contributed Setup surface is: the page registers the component-registry ref + `automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it + at `/apps/<app>/component/automation/packaged`. No bespoke route is added — a second way + in would be a URL the app metadata does not know about. + + Per packaged flow the page does exactly two things: + + - **on/off for this scope** — reads the activation state the engine reports + (`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation` + ledger) and flips it through `POST /api/v1/automation/<name>/toggle`; + - **clone** — `POST /api/v1/automation/<name>/clone` with a mandatory new machine name and + label (§7.1). The carried-over definition is never offered as editable form fields; the + copy is edited in Studio like any other flow. + + Authoring stays in Studio. The list is scoped to packaged flows by the server's own + three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the + `_packageId`-only shortcut, which classifies a tenant overlay bound to a package as + packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an + install-wide switch. + + **Server refusals reach the operator verbatim** — no client-side softening or rewording. + Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose + message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard + (409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a + list nothing on the client could reconstruct), and the §7.1 clone name conflict (409). + + ⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no + base-moved notice, no link from a clone back to its source. Cloned-without-disabled and + disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including + the case where a response carries a `clonedFrom` key anyway — the platform does not track + that lineage, so a page that displayed it would be displaying something it invented. + + `@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real + translations in all nine other packs, matching each pack's existing vocabulary for "flow" + and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket, + fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and + each one's quotation convention. The group deliberately carries no drift or ancestry + wording, and no server refusal text: those arrive as server-authored prose and are + rendered verbatim. +- 9602dc8: Setup › **Packaged automation** gains a packaged **actions** section beside its flows one — + the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch + doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated: + 「动作 可能是需要开关的,因为有的 action 我不想启用。」 + + Per packaged action the section does exactly **one** thing: **on/off for this scope**. That + is all the `sys_metadata_activation` ledger knows about an action, and the section claims + nothing more. + + - **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic + data API list the object itself sanctions for exactly this + (`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer + 'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows + everything armed. + - **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded + `actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with + the object-embedded declaration winning a `<object>:<action>` key clash. Listing only the + first source would leave an administrator with no off-switch for a packaged standalone + action. + - **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly + the one key its body declares (`{ enabled }`); `global` is the object segment for an + object-less action, the same spelling both dispatch doors take. + + **⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3 + charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered + until real pull appears. A clone control here would advertise machinery that does not exist — + which is also why the server's §5 refusal for actions recommends the platform operator and an + ordinary sibling action where the flow refusal recommends a clone. + + **⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no + base-moved notice. The platform tracks no such lineage, so a surface showing it would be + showing something it had to invent. The absence is pinned against a response that smuggles + `clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire. + + Server refusals reach the operator **verbatim** — no client-side softening, no retry loop. + Three shapes are reachable in tests, each transcribed character-for-character from the + runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the + posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`, + naming the objects a machine name collides across — a list nothing on the client could + reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`). + + One further refusal is the section's own and it points the same way: a `hasMore` on the + ledger read is treated as a **load failure** rather than rendered. A dropped row reads as + "active", so a partial ledger would show a switched-off action as armed — the one direction + this section must not fail in. + + The flows section is unchanged in behaviour. It gains a heading beside the new one, and the + page subtitle now says "Flows and actions" — that string moved in all ten packs together. + Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real + translations; the two toggle-failure keys are artifact-neutral by wording and are reused + rather than duplicated. +- 969ba84: Renderers for the `app:launcher` and `nav:menu` page blocks (objectui#6661). + Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two + `PageComponentType` members that are purely metadata-driven, so nothing had to + ship before their renderers could. Phase 2 (`global:search` / + `global:notifications`) landed in objectui#6757 and set the pattern this + follows. + + A page that declared either member drew a dashed box. The two symptoms were not + the same, which is worth recording because it decides what "fixed" looks like + for each: + + - `nav:menu` is in `PALETTE_PLACEHOLDER_BLOCKS`, registered eagerly, so it drew + the literal "Component Placeholder" scaffold in every host. + - `app:launcher` is only in `PROTOCOL_COMPONENTS`, registered when a host opts + in via `registerPlaceholders()` — which just `apps/console` does. So it drew + the scaffold in the console and `SchemaRenderer`'s red OBJUI-001 "Unknown + component type" panel everywhere else. + + Neither block adds a data layer — each mounts plumbing that was already live, + and neither issues a request or touches an adapter: + + - `app:launcher` reads the metadata app registry (`useMetadata().apps`, which + `MetadataProvider` fetches eagerly) through the shared `filterActiveApps` + predicate, and draws it with `HomeAppsStrip` — the console's own launcher + grid — so an authored launcher and the Home launcher cannot drift into two + looks for one thing. + - `nav:menu` reads the active app's navigation tree from that same registry and + renders it as page content, taking every derived fact from `@object-ui/layout`: + hrefs from `resolveHref`, labels from `resolveNavItemLabel`, the active row + from `resolveActiveNavItem`, and the item-level guards (`visible`, + `requiredPermissions`, `requiresObject` / `requiresService`) in the order + `NavigationItemRenderer` applies them, wired to the same console providers + `AppSidebar` wires them to. `action` items dispatch through + `useNavActionDispatch`, so framework#4509's "renders but dead-clicks" shape is + not reintroduced. + + `nav:menu` does not mount `NavigationRenderer` itself: that renders through + `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's + `SidebarProvider`, and a page block has to render standalone. `@object-ui/layout` + therefore exports `resolveNavItemLabel`, which was module-private — an additive + export with no behaviour change, so the sidebar and an authored menu cannot show + one nav entry under two names. + + Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an empty + shape for each, and both use `skipFallback: true` so neither claims the bare + `launcher` / `menu` keys. This does not change the Studio page palette — + `app:launcher` remains recorded there as a shell singleton, which is a palette + decision independent of whether a declared type renders. + + Three new strings — the launcher's and the menu's accessible names, and the + menu's empty state — are declared under `console.nav` in `en.ts` and its nine + sibling packs. An inline `defaultValue` alone is not a fix: it renders English + at one call site and leaves the string untranslatable everywhere + (objectui#3517). +- 220c18d: Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated + once for the surface (objectui#7063). + + Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that + renders a bare row-placeholder on an empty result is the PLATFORM's defect and + must be fixed uniformly — apps must not compensate widget by widget + (objectstack#13848). The measured scenario is a fresh flagship-demo install: + eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads + as "the dashboard failed to load" even though the widget, its declaration and + its (not yet produced) data are all legitimate. + + - New `WidgetEmptyState` is the seam the three dashboard surfaces now share. + There was no shared placeholder to fix: `DatasetWidget` wrote + `dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote + `dashboard.noDataAvailable` — three renders, two strings, no common code. + - The default now reads as a STATE, not a failure: `role="status"` (the empty + branches previously carried no role at all, while the failure branches beside + them are `role="alert"`), muted treatment with an inbox glyph rather than a + warning triangle, and a title plus an explanation where the placeholder was a + single terse fragment. + - It names WHAT is empty with zero authored copy — the widget's data source, + which is the half the reader cannot already see (the tile's title is rendered + by the card header directly above). That is `widget.dataset` on the dataset + path and `schema.objectName` on the object-bound table/pivot; `PivotTable` + takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable` + forwards. + - Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel` + added to `en` and all nine sibling packs. No inline `defaultValue` and no + interpolation — the source renders as a labelled value, so no separator is + concatenated in code and every pack spells its own punctuation. + + No new authoring obligation and no new spec key. Note that the `emptyState` + override the card assumes for this surface does not exist: `emptyState` is a + LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no + such key — so there is nothing here for an author to override, and adding one + would be a contract question rather than a rider. +- 8952395: A grouped grid now says, where the group counts are, that it grouped a **page** + (objectui#7189). + + `useGroupedData` buckets the rows the browser already holds and computes every + per-group aggregate from that same array, so both the set of groups and every + number in a group header are properties of the fetched page, not of the query. + That is a correct implementation of client-side grouping and is **unchanged** + here — what was missing is any statement that client-side grouping is what you + are looking at. Measured on a 186-record store distributed 86 / 61 / 31 / 7 / 1 + across five business units with a 100-row page: with contiguous rows the grid + rendered **two** group headers (`86`, `14`) and three units were absent from + the screen entirely; with interleaved rows all five resolved but every count + was a page slice (31 / 31 / 30 / 1 / 7). Nothing on screen said either. + + The paging footer is not a statement about what was grouped, and it + demonstrably did not prevent the wrong reading — a wrong number invites a + second look, an absent row invites none. So the disclosure goes where the + authoritative-looking number is: + + - a short `Partial` marker beside **every** group count, at every nesting + depth, carrying the full sentence as its `title` and its accessible name; + - one line directly above the group list, inside the grouped region rather + than in the footer: *"Grouped over the first 100 of 186 records. Group counts + are page-scoped, and a group whose records all fall beyond the loaded rows is + missing here."* + + The trigger is the strongest thing the component can actually know, and the + wording never outruns it. With a real match total to compare against + (`resolvedTotalMatching` — the one derived value the pager and both bulk-bar + sites already read, reached either from the grid's own fetch or from a host's + `rowCount`) it states the fact with both numbers. With no total but a window + that came back full it may only say *"more may match"* — the same inference + `plugin-list`'s own footer draws when no total is known. Rows handed in inline + are not a page and are never marked, and **a grouped grid whose result set fits + in one page shows nothing at all**: the marker is conditional, which is what + makes it worth reading. + + Server-side grouping — the durable fix — is deliberately NOT part of this. It + is an API-surface decision still open on objectui#5560, and nothing here builds + toward it or changes the fetch. + + `@object-ui/i18n` carries the three new `grid.grouping.*` strings across all + ten locale packs; `GroupRow` gains two optional props (`partialLabel`, + `partialTitle`) and is otherwise unchanged. No metadata schema key was added: + the condition is derived from data the grid already has. +- a4514e8: Retire `useObjectLabel().viewDescription()` and the `_views.<view>.description` + catalog convention it resolved (objectui#7219, maintainer ruling 2026-09-02, + option B — enforce-or-remove). + + **Breaking for translation bundles, deliberately — and this text is the notice.** + Out-of-repo translation bundles that authored + `<ns>.objects.<objectName>._views.<viewName>.description` cannot be seen from + this repo, so there is no census to point at and no migration script to run: + that key now resolves nowhere, and an entry left under it is simply ignored. + Nothing throws, and nothing else on that node changes. + + **What replaces it.** A list view's description has exactly ONE channel: the + `I18nLabel` value authored on the view entry itself — a string, or an inline + locale map: + + ```ts + listViews: { + by_unit: { + label: 'By business unit', + description: { en: 'Open work only.', 'zh-CN': '仅未完成的工作。' }, + }, + } + ``` + + `ObjectView` relays that value to the renderer and `plugin-list`'s `ListView` + resolves it against the display locale (objectui#7199, shipped before this + change), so the authored channel already works end to end. **Migration:** move + the sentence out of the translation bundle and onto the view entry as a locale + map. + + **Why removed rather than wired in.** The member was declared and resolved but + had zero callers and zero in-repo bundle usage — an entry authored under the + catalog key reached no screen. Wiring it in would have put two vocabularies on + one concept (`I18nLabel` on the entry, and the catalog key) and required a + precedence rule between them, which is the ambiguity rather than the fix. + + The two sibling members on the same node are **unaffected**: `viewLabel` and + `viewEmptyState` still resolve `_views.<view>.label` and + `_views.<view>.emptyState.{title,message}`, and the shared `viewSuffixes` key + builder they use is unchanged — only the `'description'` tail is gone. Pin tests + in `@object-ui/i18n` and `@object-ui/app-shell` were retargeted onto those two + survivors plus a case that authors the catalog `description` and asserts the + authored value is what a consumer resolves. +- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts + (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer + ruling B, 2026-09-02). + + A dataset-bound chart's aggregate axis / legend title read the analytics + service's hard-coded English `Count` on a zh console whose category labels were + already Chinese. The renderer was passing `fields[].label` through verbatim — + correctly, for an author-declared measure (objectui#4106) — and had no way to + tell the server's built-in default apart from an author's label. + + The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural + discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | + 'count_distinct'`, populated only on the server-side built-in defaults + (objectstack#14492). This change is the consumer side of that contract: + + - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — + `ChartResultField` plus the optional `builtinAggregate` carrier + (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than + on it because the spec this release is built against does not carry the key + yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / + `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries + the locale strings in (core stays React-free and i18n-free — the same + division as `nullCategoryLabel`). A field carrying a recognised + discriminator resolves through that map; every other field keeps its wire + `label` verbatim — never by matching the label's text or the field's name + (the rejected option A). + - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings + through the existing `report.aggregate.*` keys (zh already carried 计数 / + 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). + - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), + `plugin-report` (`DatasetReportRenderer`): pass the resolved map to + `buildChartSeries`. + + Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same + chart titled `计数`; an `en` session still reads `Count`; an author-labelled + measure (`Tasks`) and a measure literally named `count` without the + discriminator are byte-for-byte unchanged. Until the upstream field is + populated the wire carries no discriminator and every chart renders exactly as + before. +- 20cb8db: `ObjectTimeline` refuses an undeclared date axis instead of inventing one + (objectui#7459). + + Steps ① and ② of the three-step sequence the maintainer ruled on objectui#7070 + (2026-09-01, 总监批 #28). House posture, on record with that ruling: + 日期轴永不虚构 — a date axis is never fabricated. + + `ObjectTimeline` resolved its date axis through five declared bindings and then + closed the chain with a sixth rung that was a bare literal field name nobody + has ever declared. A name therefore ALWAYS resolved: for a view that declared + no axis, every record read a key its object does not carry, every event landed + in the "No date" bucket, and the screen read as a timeline that had been built + and simply had nothing in it. + + Two changes, shipped together because neither is observable alone: + + - **The renderer now refuses.** An object-bound timeline with no declared date + axis renders a diagnostic naming every binding it accepts — + `timeline.startDateField`, `timeline.dateField`, `mapping.date`, and the two + deprecated flat spellings — instead of a chart. The twin of `ObjectGantt`'s + "Gantt configuration required" screen, in the shape objectui#7070 settled. + - **The invented sixth rung is gone**, which is the only thing that makes the + refusal reachable. Added while the floor stood, it would have been dead code; + retired without the refusal, it would have produced exactly the silent + "No date" outcome the ruling rejects. + + **What changes for an author.** A view that declares a date axis is completely + unaffected — all five declared spellings resolve exactly as before, and a + timeline authored from literal `items` is never refused, since its items carry + their own dates and no field name is read for them. A view that declared no + axis anywhere, and was rendering an empty-looking timeline, now says so. + + ⚠️ Both premises were RE-MEASURED on the current tree before anything was + edited, rather than taken from the card: the renderer had no absent-axis + refusal (against a live control term that fires in the same file), and the + floor was still present and still spelled as reported. The pairing itself is + pinned — the refusal cases go red the moment the floor returns, including one + whose records carry a column that happens to be named `date`, where a returned + floor renders a convincing timeline rather than an empty one. + + Refusal is distinguished from "renders an empty timeline" by asserting the + canvas is ABSENT, not merely event-free. The component's success surface is now + named (`data-testid="timeline-canvas"`) so that distinction can be measured; + every other terminal state of the component already named itself. + + Step ③ of the ruling — the `'created_at'` floors on the two plugin faces — + stays on objectui#7070 and is deliberately NOT in this change. +- 91f9276: **The maker's edit-mode starter offers sample data, not an automation v1 cannot + build (objectui#7709).** Bound to an existing app (`?package=`), the maker's + empty state offered four starters: add a field, add an object, add a dashboard, + and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and + notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the + measured behaviour on the sibling chips was not a refusal but a silent degrade + into a view — so the product recommended an automation and would have handed + back a page. + + Rewording it was not available: asking for a field, a view or a dashboard + duplicates one of the three chips beside it. The fourth chip is now + `addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all + ten packs and in the call-site `defaultValue` fallback, which is a second copy + of the same string. The three surviving chips all add STRUCTURE; what an app + that already has objects most often lacks is DATA, and `seed` is on v1's + authoring whitelist. A note beside the keys in every pack and at the call site + says this chip's automation wording comes back when ADR-0112 v2 re-adds flows + and actions, and the retired sentence for each pack is kept in the guard suite + so v2 has it verbatim. +- a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered" + (objectui#7741). + + `ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the + import wizard hides its saved-mapping selector on an empty list. So "the server served + zero mappings" and "the server refused, or broke" produced the identical UI on every + deployment — the feature simply absent — with a `console.warn` as the only + discriminator, in the browser console, with nothing pointing at it. That silence did not + merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter + (objectstack#14026 was filed, routed and worked by two seats against a wizard that had + been correct since `@object-ui/data-objectstack@17.1.0`). + + **The empty-list return is unchanged.** `listImportMappings` still answers + `Promise<any[]>` and still never throws, on every arm including the loud ones — this is + a channel added ALONGSIDE that contract, not a change to it. + + - **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe + channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a + metadata read failed in a way that is NOT the supported "this deployment does not + serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the + object, whether the server `refused` this caller or the answer was `unreadable`, and + the server's own ADR-0112 code, HTTP status and message. + - **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported + so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code` + first, the status only where no code was declared — and never "is the result an empty + array", which is what both conditions produce and so can never tell them apart. + - **The older-server case stays quiet.** A deployment that does not serve the `mapping` + kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list + door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no + event. That is a real, supported deployment shape and it must not become a visible + fault. + - **The console now says so.** `AdapterProvider` subscribes to the new channel and + renders a warning toast naming the object, the remedy and the server's own words, so a + user without devtools open can tell "there are none" from "we could not find out". + Three new `console.importMappings*` keys ship in all ten locale packs. + + This applies framework #13906 decision 1 option A — *a thing that could not be READ is + not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not + a new principle. +- 0758bd8: `ObjectChart` refuses an object-bound chart that declares no category axis (objectui#8168). + + An object-bound chart that named no field to group by used to be composed anyway. + `runAggregate` passed `schema.aggregate` to `ds.aggregate(objectName, { field, + function, groupBy, filter })` with no guard on `groupBy`, and the `ds.find` leg + handed the same bag to `aggregateRecords`, which buckets every record on + `record[groupBy] ?? 'Unknown'`. So one path asked a driver to group by `undefined` + and the other collapsed the whole object into a single `'Unknown'` bar — and which + of those a reader saw was decided by the data source, not by the renderer. The only + loud states this component had were a fetch `error` (`chart-error`) and a generic + "No data yet"; neither is a statement about an absent binding. + + `ObjectCalendar`, `ObjectGantt` and `ObjectTimeline` each already refuse a view that + declares no axis. This is the fourth, following `ObjectTimeline`'s shape + (objectui#7459): a `role="alert"` box, `data-testid="chart-missing-category-axis"`, + naming the bindings the author can declare — `aggregate.groupBy`, `xAxisKey`, + `xAxis.field` — rendered from the resolver's own vocabulary so the message cannot + drift from what the resolver reads. + + **Breaking, deliberately — and this repo ships breaking as `minor`.** A chart that + previously rendered an `'Unknown'`-bucketed bar (or whatever the driver did with + `groupBy: undefined`) now renders the refusal instead. That is the intent: the + picture it drew was not a picture of the data. + + It keys on the CATEGORY alone. A measure may legitimately be absent — `count` takes + no field — so refusing on an absent measure would refuse `count` grouped by a + declared category, a chart that renders correctly. Four shapes are deliberately + untouched: an ADR-0021 `dataset` chart (which may declare no dimension), a chart + carrying authored `data` or a `bind` scope (no field name is read to fetch those + rows), a spec-shape `xAxis: { field }` with no `xAxisKey` (resolved through + `normalizeChartSchema`, this package's one translation of the author-facing shape), + and every schema the five in-repo producers compose today — all five floor their own + category, so none of them can reach the refusal. + + This does **not** retire the six `'name'` / `'value'` floors at the three relay faces; + that is the remainder of objectui#7547 and is mechanical only once this screen exists. + + New key `chart.unconfigured.noCategoryAxis` in all ten locale packs. +- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id. + + A record waiting on a position rendered its approver as `positi…ager` — the + engine reference `position:sales_manager`, 22 characters, past the identity + formatter's 14-character truncation arm and middle-truncated to fit its chip. The + step names beside it were human prose; the one line answering *who is holding + this record* was an internal identifier, and not even a complete one. The same + reference reached the admin-override confirm dialog un-truncated, so a paragraph + of plain governance prose ended `— position:sales_manager` (objectui#5414). + + Both surfaces now resolve the reference before rendering, in three tiers, most + authoritative first. The server's own `pending_approver_names` wins whenever it + answers, and a backend that resolves its own slate costs the record page no extra + request. Otherwise the console reads the directory row the spec's approver + binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, + for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no + adapter and no row, the machine name still prettifies into prose rather than + truncating. The raw reference stays on hover, which is where an internal + identifier belongs. + + An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` + is actionable where `positi…ager` is not, and it is the motivating rescue case + for the admin-override path. Staffing is deliberately tri-state — a + `sys_user_position` read the viewer is not permitted to make leaves the seat's + staffing UNKNOWN and says nothing, because "I could not look" is a different + claim from "nobody holds it" and only one of them is safe to print on a + governance surface. + + Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` + and `approvalsInbox.approverNameSeparator`. The separator is a translated + punctuation key rather than `Intl.ListFormat`, which was measured on this tree + joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with + no separator, reading as one person's name. + + The directory-backed kinds and their value columns are read from + `@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new + approver type is covered the day the spec publishes it. Id-valued kinds + (`user` / `team` / `department`) keep the existing middle-truncation: a row id + has no prose to recover, and that arm is objectui#3461's answer, not this card's + defect. +- c40f3b8: A screen flow's resume result reaches the user — on both outcomes (objectui#5417). + + A dogfood walkthrough reported that a refused `resume` and a successful one + "render identically: the dialog closes and the page is unchanged", leaving no + gesture that distinguishes "created" from "rejected". Re-measured against `main` + before any change, one half of that was already fixed — `interpretFlowResponse` + reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its + prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors + after the version the report was measured on. There was no interpreter bug and + no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were + real, and they are what changed: + + - **A terminal failure no longer closes the dialog.** The reason it closed is + unchanged and is not reversed: on a `FLOW_FAILED` the engine has already + consumed the suspension, so a resubmit can only reach "No suspended run" and + must not be offered. Closing was one way to withhold that dead retry and the + expensive one — the user had just typed a form they could no longer see, and + the engine's sentence names a value that left the screen with it. The dialog + now stays open with the submit affordance withdrawn: the flat footer swaps + Submit for Close, and an `object-form` step drops its Save (which also stops a + second click from duplicating the record it had already persisted). + - **The refusal has a second, non-expiring carrier.** The toast stays — it is + viewport-fixed, so it still reaches a user scrolled past a tall step's header + — and an inline destructive `Alert` (`role="alert"`) now holds the same + sentence inside the dialog, beside the values that produced it. A retryable + refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, + and its banner clears as soon as the user starts editing. + - **A successful run invalidates what the flow WROTE, not just what the user is + looking at.** Both hosts answered `onComplete` with + `notifyDataChanged({ objectName: <this page's object> })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a + first-class disabled state, and the load-failure hint describes the control plane + the runtime was actually pointed at (objectui#5504). + + `apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory + default, so a stock self-hosted stack has no marketplace at all. The Console still + recommended one: Home led with "Start with a template" and "Browse App + Marketplace", and the click landed on a red **Failed to load marketplace / Not + found** card whose hint claimed this runtime "points at the public ObjectStack + cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for + exactly the deployment reading them — the operator had not left the default, and + the advice pointed back at the template that told them to set `off`. "Marketplace + disabled by configuration" is a configuration conclusion, not a load failure. + + - `isMarketplaceEnabled()` (`runtime-config`) reads the server's own + `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the + serving app's route table (objectstack#8356). It is never inferred from the shape + of a failed request: a control plane that is merely DOWN leaves the flag `true`, + so an outage still renders as an outage. Unknown fails OPEN. + - The marketplace page renders an informational "App Marketplace is turned off" + state — muted, not `destructive` — and issues no request it knows will 404. + - Home's "Start with a template" cover greys out with a visible localized reason, + and the "Browse App Marketplace" shortcut is withheld, exactly as they already + are for the `manage_metadata` capability gate. + - `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the + configured control plane) and `failedHintSameOrigin`. The "points at the public + cloud by default" sentence is gone: it was rendered unconditionally, including on + every runtime whose operator had overridden `OS_CLOUD_URL`. + + All ten locale packs carry the new keys. +- f157423: Studio workbench and AI tool cards speak the author's language (objectui#7254) + + - The Interfaces breadcrumb, canvas caption and navigation rail show the + metadata label plus a translated kind; the internal `type · name` pair moves + to the tooltip. An unlabelled nav leaf now falls back to its object name + instead of rendering an empty row. + - The Studio top-bar package switcher reads the package's human name from + either position the packages endpoint serves it in, instead of degrading a + registry-shaped entry to its reverse-domain id. + - The dashboard property panel is localized: the spec's authoring form is + overlaid through the platform's own `metadataForms.<type>` convention, so + section headings, field labels, hints and the `header` composite's sub-fields + render in Chinese (developer vocabulary such as "Tailwind units" is replaced + with something an author can act on, not transliterated). + - AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty + platform-provided tools, ten locale packs), the header status badge is + localized, and the plan count strip is a real plural family instead of an + English `+ "s"` concatenation. + - The tool card's header badge and its body badge now come from one producer: + a proposal that has been confirmed, built or published no longer keeps a + header reading "Awaiting Approval". +- 7a90afd: Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. + + Creating an object through Studio collected exactly two things — display name and + identifier — and saved a draft that declared no `sharingModel`. The draft saved + happily, the form designer worked, and the object was then refused at 发布 → + 全部发布 by `security-owd-unset`: a required decision the surface never asked + for, delivered by failing, as English ADR prose in a toast that then vanished on + a timer. The one actionable word in it named a control three clicks away that + nothing routed to. + + The publish gate is correct and is unchanged — an org-wide default has to be an + authored decision, not an accident. What changes is when the console asks and + when it answers: + + - **The create dialog asks.** A third field collects the baseline, pre-selected + to `private` and glossed with the Settings tab's own strings, so a new object + is publishable by construction. `buildObjectSkeleton` now takes the value as a + required parameter — a future create path cannot omit the baseline without + failing to type-check. `controlled_by_parent` is deliberately not offered at + creation: it derives access from a master relation a brand-new object does not + have yet, so offering it would trade one publish refusal for another. + - **The review sheet reports it.** The pending-changes panel now runs the + framework's own `validateSecurityPosture` over the pending object drafts and + names any blocking finding, with its fix-it hint, next to the Publish button. + It mirrors the producer's rule rather than re-deriving it, and it reports + without blocking — the server door stays the authority. + - **The Settings tab stops calling an unset baseline safe.** It described unset + as "defaults to Private", which answers what the runtime does and not whether + the object can ship. It now reads as the publish-blocking problem it is, + styled like the external-wider warning beside it. +- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. + + The dashboard widget config panel carried a private `resolveLabel` documented as + resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference + form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline + per-locale map `I18nLabelSchema` actually admits has neither limb, so + `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy + of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, + `MetricWidget` and `MetricCard`. + + This was not a display bug. The resolved value seeds the panel's editable draft, + so a widget whose stored title was a map opened with an **empty** Title field and + the next save wrote `''` over the author's map — on the ordinary path, not an + exotic one: open the widget, change anything, save. + + Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: + + - **Reading** goes through `pickLocalized(value, language)`, so the panel shows + the active locale like every sibling surface post-objectui#4032. + - **Writing** replaces only the active locale's entry and carries every other + locale across. A title the author never touched round-trips the stored object + itself through an unrelated config edit; an edited one merges into the entry + that was displayed. The live-update callback (`onFieldChange`) forwards the + merged map for the same reason — hosts feed it back into the widget the panel + re-opens from, so a bare string there dropped the map before a save ever ran. + + `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side + inverse of `pickLocalized`, so the rule is stated once instead of re-derived per + panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, + region-qualified sibling — and deliberately stops there: the `default` / `en` / + first-value limbs are display fallbacks that hand back *another* locale's string, + and writing to one would let an author editing in `fr` overwrite English. With no + entry for the active locale the edit adds one. The pairing + `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a + write that lands where the read does not look is how a "saved" string disappears. + + A full multi-locale editing UI remains out of scope (objectui#4163). + +### Patch Changes + +- b06e374: The Spanish pack renders `Done` as `Listo` at every one of the four sites that say it + (objectui#3880, triage adjudication 2026-08-09). `grid.bulk.done` — the footer button that + dismisses the bulk-action result dialog — read `Hecho` while `common.done`, `view.done` and + `form.fullscreen.done` all read `Listo`, so the same English word rendered two ways in + Spanish across dialogs a user meets in one session. + + Adjudicated a typo rather than a deliberate contextual split, on three checks. All four + keys hold the byte-identical `en` value `Done`, and all four call sites are the same + control: a dialog-footer button whose click finishes or dismisses the surface + (`BulkActionDialog` `onClose(result)`, `ManageViewsDialog` `onOpenChange(false)`, + `fullscreen-editor` `commitFullscreen`, `InviteMemberDialog`'s invitation-created footer). + The nine other packs each render all four identically (de `Fertig`, fr `Terminé`, pt + `Concluído`, ru `Готово`, ja `完了`, ko `완료`, zh `完成`, ar `تم`), so no other translation + pass had found a context worth splitting on. And the neighbouring `Hecho`/`Deshecho` + pairing that could have justified it does not hold: `grid.bulk.undo` is the verb `Deshacer`, + and `Deshecho: ` is `undonePrefix`, a result-line status rather than a button. + + `Hecho` moved to the 3:1 majority `Listo`, which is the value objectui#3546 slice seven had + already chosen for `common.done`. `packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` + pinned the old outlier as a recorded example of deliberate divergence; that pin now asserts + the four as one value instead, and its note keeps the history plus the `Pending`/zh row, + which remains a genuine deliberate split. + + No `en` value changes, so no other pack is asked to follow. This is the value half of + objectui#3880 only — the card's 281/164 shared-string census stays on the card as + documentation, and is explicitly not a gate: 164 of those groups diverge legitimately. +- 594704f: The `console.objectView.*` config-panel vocabulary is retired — 116 keys removed from each + of the ten packs, 1160 translated strings that nothing read (objectui#4730, maintainer + ruling 2026-08-19). + + The namespace held 209 keys per pack. 116 of them labelled a view-configuration settings + panel that does not exist: appearance and density toggles, accessibility attributes, + conditional-formatting rules, row-action and inline-edit switches, quick-filter builders, + an advanced-settings tier. `packages/app-shell/src/views/ViewConfigPanel.tsx` — the panel + they were written for — was migrated off the legacy `buildViewConfigSchema` engine onto + `ViewVariantInspector`, a spec-driven inspector whose field labels come from + `@objectstack/spec` metadata rather than from this namespace. The panel was replaced; the + keys were not cleaned up with it. + + Removed under objectui#4658's three-legged evidence standard, re-measured on this branch's + merge base rather than inherited from the card: zero `t()`/`tt()` call sites, zero textual + occurrence of the dotted key anywhere in the repo outside the packs that define it, and a + consumer spot-check confirming no i18n wiring. The 93 live keys stay — the create-view + dialog fields, the view-type catalogue, `new`/`save`/`cancel`, the object-not-found copy, + plus the 38 keys whose spelling still appears somewhere the AST pass cannot see, which are + out of scope here. + + Four of the retired keys name `ListViewSchema` properties that are still active — + `rowActions`, `inlineEdit`, `hiddenFields`, `filterableFields`. They are retired anyway, by + the ruling's own words: a live schema property is not a consumer of a locale string; only a + labelled UI control is. If such a panel is ever specified, its keys are re-authored + alongside it. + + `packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts` pins the removal + by name. Every i18n gate in this repo runs call site → key, so none of them can see a dead + key come back: the parity gate is fully satisfied by 116 dead keys present in all ten packs, + and the reverse sweep that found them (`scripts/check-i18n-dead-keys.mjs`) is report-only by + design. The pin is the only thing that would notice. +- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's + ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a + list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user + dragging a column or cycling density was re-styling that view for the entire organization. Nothing + in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, + v18) and is deliberately not built here — which is precisely why the write has to be gated rather + than narrowed: there is no second, private store for it to fall back to. + + `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability + set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` + (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the + `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — + before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. + It is on the write rather than on the toolbar button on purpose: withholding the affordance would + leave the method still accepting the call from anything else holding the adapter, whereas a gate on + the write is inherited by every caller, present and future. + + `manage_metadata` is not a newly minted name. It is the capability this repo already treats as + metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself + refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very + same ADR-0005 metadata door, so this applies the authority the server is already applying instead of + inventing a parallel one. + + **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating + ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a + client-side refusal on missing data cannot protect anything and can only break a permitted user. A + *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new + `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. + + The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure + into `console.error`, which for a debounced toggle whose UI has already moved would have left the + operator looking at a density they did not get; a denied write now raises a toast. And the "View + settings" popover — where density and field visibility are actually changed — now states the scope + before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who + uses this view."*, translated in all ten packs. +- 95f8704: `record:path` now announces each stage's state, not just its label (WCAG 2.2 SC 1.4.1) + + The lifecycle path distinguished travelled, upcoming and lost-terminal stages with + colour plus a `✓`/`✗` glyph, and both glyphs are `aria-hidden` decoration. + `aria-current="step"` marked the current stage and nothing else, so a screen-reader + user heard a run of identically-announced items — and a rejected stage announced + exactly like an ordinary stage the record had not reached yet. + + Each stage now carries an accessible name composing its (already picklist-localized) + label with its state, from five new `detail.pathStage*` keys translated in all ten + locale packs. The glyphs stay decorative and the readout's `role="listitem"` / + `aria-current` semantics are unchanged. + + The name is composed into `aria-label` rather than visually-hidden text because + `listitem` takes its name from the author only: text placed inside a stage computes + to an empty accessible name, so the visually-hidden shape would have looked right in + the markup and delivered nothing to the accessibility tree. +- 8d3a529: `record:path` finishes localizing and de-colouring its accessible names — the two residues + objectui#5916 named and deliberately left behind (objectui#5956, objectui#5957). + + **The list's own label was English on a localized surface, and the other one named nothing.** + Both the desktop and the mobile `role="list"` row did + `aria-label={schema.aria?.label || 'Record path'}`, so a zh/ja/ar session heard `Record path` + for the list while every stage inside it announced in the session locale — one control + speaking two languages at once. The fallback is now `detail.pathLabel`, translated in all ten + packs; the `schema.aria.label` author override still wins ahead of it. + + The lost-terminal alt group was a different defect wearing the same clothes: its + `aria-label="Alternative terminal stages"` sat on a bare `div`, which has the `generic` role, + and browsers expose no accessible name on a generic element. That string reached nobody — + inert, not merely untranslated — so translating it would have shipped copy to ten packs that + no user can hear. It is removed rather than given a role that takes a name, on three + measurements: nothing is lost (it was never announced), it would be redundant (every stage + inside already announces `closed lost` in the session locale after objectui#5916, in the one + place `role="list"` can carry it), and it would fork the two rows (the mobile row renders one + flat list with no alt group, so a named group would make one control expose two structures by + viewport). + + **An unreached goal terminus was distinguished by hue alone.** `railClass` paints it + `bg-emerald-500/30` where a plain upcoming stage gets `bg-muted` — the renderer's own note + calls this "a faint emerald so the goal is legible" — while both announced the identical + `{{stage}}, upcoming`. Two stages ahead of the record painted differently and read the same: + the WCAG 2.2 SC 1.4.1 class objectui#5916 closed, on the one distinction it left behind, and + reachable without authors opting in because `classify()` finds `won` through the `WON_TOKENS` + heuristic as well as an explicit `terminal: 'won'`. New key `detail.pathStageWonUpcoming` + (`{{stage}}, goal stage, not reached`), translated in all ten packs. + + Scoped to the UNREACHED goal, which is a measurement of the stylesheet rather than a + preference: a reached goal terminus paints `bg-primary` when current and `bg-emerald-500` when + completed, byte-identical to any other current or completed stage. Naming it apart would hand + a screen reader a distinction the screen does not make — the mirror image of the defect — so + it is one new key, not a pair, and a test pins that decision so it cannot drift into a fourth + state unnoticed. + + Both new keys also land in `DETAIL_DEFAULT_TRANSLATIONS`, which + `defaults-maps-mirror-en-pack` compares against the `en` pack key by key, so neither can fork + between a provider-mounted console and a provider-less embed. No existing `en` value changes, + so no pack is asked to follow an edit. +- 5ac2e2c: The merged `Loading…` group now reads one way per language (objectui#5972). + + objectui#3878 converged every pack on the typographic ellipsis, and in doing so **merged** + the ASCII `Loading...` group into the U+2026 `Loading…` group. Nobody re-measured the + wording afterwards. Re-derived on today's tree by flattening all ten packs and comparing + exact values, the group is 10 keys — `lookup.loading`, `common.loading`, + `fields.recipient.loading`, `grid.import.historyLoading`, `grid.bulk.loading`, + `detail.loading`, `report.loading`, `dashboard.loading`, `auth.device.loading`, + `approvalsInbox.loadingMore` — and `de` rendered them four ways, `ko` two and `ar` two, + while en/zh/ja/fr/es/pt/ru were already unanimous. + + Three packs move, translation copy only — no key is added or removed, no `en` value + changes, and every value keeps its U+2026: + + - **de** → `Wird geladen…` on `detail.loading`, `report.loading` (were `Laden…`) and + `approvalsInbox.loadingMore` (was `Lädt…`). The passive is both the group majority and + the pack's dominant register for in-flight states generally. + - **ko** → `로딩 중…` on `fields.recipient.loading`, `grid.bulk.loading`, + `grid.import.historyLoading` and `approvalsInbox.loadingMore` (were `불러오는 중…`). + Majority, and it matches the pack's own pattern: `불러오는 중` is what `ko` uses when the + string names the thing being loaded, the bare form is `로딩 중`. + - **ar** → `جارٍ التحميل…` on `common.loading` and `detail.loading` (were `جاري التحميل…`). + This one is an orthography normalization rather than a wording choice: `جارٍ` is the + indefinite منقوص participle with tanwīn on the rāʾ, `جاري` the yāʾ-retaining form. + + `de` `auth.device.loading` deliberately stays `Lade…`. It is the one member whose outlier + spelling is coherent with its own screen: `DeviceAuthPage` renders that namespace's three + in-flight states together and `de` writes all three in the same first-person voice + (`Genehmige…`, `Ablehne…`, `Lade…`), the other two being outside this group. Converging it + alone would manufacture a fresh same-screen inconsistency, so it is reported as a fork and + pinned as a named exemption instead. + + `packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts` gains the per-language + uniformity pin beside the glyph rule that created the group. The pin derives the group + from `en` and asserts its membership and per-language value counts **before** asserting + uniformity, so it cannot pass by matching nothing. +- b08b7eb: All ten locale packs now define `home.recentApps.itemType.report` and + `home.recentApps.itemType.metadata` (objectui#6023). `RecentItem['type']` is a six-member + union and `useTrackRouteAsRecent` writes both of these at runtime — `metadata` on any + `/metadata/<type>/<name>` route visit, `report` on any `/report/<name>` visit — but the + packs defined only four of the six, so the Recently Accessed and Starred cards labelled + those two items from the call sites' inline `defaultValue` instead of from the pack. + + Ten packs missing the same member is full parity, so no pack-vs-pack gate could see it, + and the prefix rule only ever asked whether `home.recentApps.itemType` resolved, which it + did. What made it quiet rather than loud is the `defaultValue`: English readers saw a + plausible `Report` / `Metadata` (lowercase in the rail) rather than a raw key, and the + other nine locales saw those English words — objectui#3517's mechanism for hiding a + missing key for months. + + The nine translations were taken from each pack's own existing rendering of the same word + rather than composed: the singular `Report` that `appDesigner.navReport`, + `appDesigner.navTypeReport` and `search.badgeReport` already carry (ar `تقرير`, de + `Bericht`, es `Informe`, fr `Rapport`, ja `レポート`, ko `보고서`, pt `Relatório`, ru + `Отчёт`, zh `报表`), and the `Metadata` that `layout.metadata.label` already carries (ar + `البيانات الوصفية`, de `Metadaten`, es `Metadatos`, fr `Métadonnées`, ja `メタデータ`, ko + `메타데이터`, pt `Metadados`, ru `Метаданные`, zh `元数据`). No new vocabulary was invented + for any locale. + + The two matching entries in `scripts/i18n-call-site-key-baseline.json`'s `missingMembers` + are cleared, since that list is a ratchet: a baselined entry whose defect is gone fails the + build too. The union itself is untouched — narrowing `RecentItem['type']` is the other + resolution the gate accepts, and it would have been a lie about data both call sites + demonstrably write. +- 5fa06c4: The zh pack's `console.breadcrumb.reports` renders 报表, the noun the rest of the pack + already uses for the report feature (objectui#6166). + + **The authority for this edit is the maintainer ruling of 2026-08-25, not an occurrence + count.** The card was filed explicitly as a native-speaker call and explicitly forbade + resolving it by normalising to the majority: 报表 (a tabular/data report) and 报告 (a + written/narrative report) are not interchangeable, so a pack that spells one key + differently from its siblings is evidence of a majority and never, on its own, evidence of + a mistake. The maintainer ruled that no deliberate narrative-report distinction was + intended here and that the breadcrumb names the same report feature the rest of the pack + calls 报表. + + The render context corroborates the ruling and closes the confidence gap triage recorded + when it declined to decide this itself. `console.breadcrumb.reports` labels the + `routeType === 'report'` **list** route in app-shell's `AppHeader` — a structural sibling + of the `dashboards`, `pages` and `system` segments beside it — and drilling through it + appends a metadata report definition, the same feature named by + `console.commandPalette.reports`, `console.nav.navReport`, `search.typeReports`, + `search.badgeReport` and `search.reportNotFound`. Nothing narrative renders beneath it. + + A comment at the key records that this was **ruled** rather than counted, and states the + 报表/报告 distinction it was ruled against, so the next reader measuring pack consistency + neither re-files it nor quietly restores 报告 after reading the render context and + disagreeing. That comment is half of the deliverable; the value change alone would leave + the decision unrecorded, which is the failure mode the card was most concerned about. + + **Scope: one key, one pack.** This is not a licence to normalise vocabulary across the ten + packs — the card names that hazard explicitly and says it would need its own ruling, and + this ruling grants nothing beyond the single key it names. No `en` value changes, so no + other pack is asked to follow, and no other pack was touched. +- 5d3a2d1: The capability picker localizes `manage_sharing` (objectui#6285). Before this, "Manage + Sharing" was the one platform capability in `sys_permission_set`'s picker that rendered in + English in every locale, beside seven siblings that translated — a user-visible missing + translation, in all ten packs at once. + + The cause was an unchecked copy. `CURATED_CAPABILITY_LABELS` in + `CapabilityMultiSelectField.tsx` listed seven capability names under a doc comment claiming + it mirrored `@objectstack/spec/security`'s `PLATFORM_CAPABILITIES`; the spec grew an eighth + member and the list did not follow, so `manage_sharing` fell through to the English label + the `sys_capability` registry serves. Nothing could catch it: the i18n gate reads that list + as this key family's vocabulary and checks the members it names — all seven had keys — and + no instrument compared the vocabulary to the array it was named after. + + `capability.label.manage_sharing` is now authored in all ten packs and in the field widgets' + provider-less defaults map, the list carries the member, and the prose claim is replaced by + a check: `CapabilityMultiSelectField.specParity-6285.test.tsx` imports `PLATFORM_CAPABILITIES` + and fails on any difference in either direction, reading the declaration through the i18n + gate's own source reader so what it pins is exactly what that gate consumes. `labelFor` also + gains a `defaultValue`, so a capability that arrives in a future spec bump before its + translation is authored degrades to the registry's English label rather than rendering a raw + i18n key at the user. +- 00c665e: `appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten + locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310). + + objectui#6043 retired the Field Designer's formula-expression textarea, which was the + key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label: + t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in + eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in + `packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner > + fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. + + Removed under objectui#4658's evidence standard, re-measured on this branch rather than + inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could + reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its + sole textual occurrence anywhere in the repo was the defaults-map row this change removes + with it — so the key goes from NEEDS-REVIEW to no footprint at all. + + The map and all ten packs move in one commit, which is what keeps + `defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack + lacks, and `all-locales-key-parity` fails a pack left behind. + + Not touched: `designer.field.formula` (`'Formula (CEL)'`) in + `packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging + to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula + expressions. + + `packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the + removal by name, following the four prior retirements (objectui#4145, objectui#4392, + objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can + see a dead key come BACK into the packs: the reverse sweep that found this one is report-only + by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and + `check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted — + reviving the row in all ten packs turns exactly that one case red, naming each pack, while the + parity gate and the defaults-map mirror stay green. +- fff9645: `ar` spells the منقوص participle one way — `جارٍ`, pack-wide (objectui#6610). + + The pack wrote the active participle of جرى two ways at once: `جارٍ` on 92 + values and `جاري` on 8. One word, two spellings, differing by a single code + point — U+064D (tanwīn kasr, the yāʾ dropped) against U+064A (the yāʾ kept) — + and same-screen visible: `LoadingScreen` renders `console.initializing` as the + heading over the three `console.loadingSteps.*` and `console.actions.retrying` + on the retry button, four values that all said `جاري`, inside a console that had + already said `جارٍ` on `dashboard.loading`, `list.loading`, + `detail.loadingAttachments` and `console.ai.*`. + + The 92:8 majority is not why the 8 moved. An اسم منقوص declines three ways, so + "the 8 are right in their own context" was a live reading and was falsified + rather than out-voted: all eight opened their string as a fronted indefinite + predicate over a delayed subject — nominative, indefinite, not annexed — and + five had a word-for-word twin already spelled `جارٍ`, `grid.refreshing` + (`جاري التحديث…`) against `list.refreshing` (`جارٍ التحديث…`) being the exact + minimal pair. Meanwhile the three values where the yāʾ is grammatically required + (accusative `جاريًا` after لا يزال / ما زال, definite `الجارية`) are correct and + are left alone. + + Eight values converged; no key added or removed and no `en` value touched. + `ar-participle-orthography-6610.test.ts` makes it a pack-wide invariant rather + than an eight-key edit — a ninth value arriving with the yāʾ now fails by key + name — and pins the three legitimate yāʾ occurrences so a later sweep cannot + flatten a correct distinction into a real grammatical error. +- 9c3b7ce: The German pack's `auth.device.*` in-flight states move to the passive register + (objectui#6611, triage ruling 2026-08-27, option 2): `approving` `Genehmige…` → + `Wird genehmigt…`, `denying` `Ablehne…` → `Wird abgelehnt…`, `loading` `Lade…` → + `Wird geladen…`. + + Graded a bug, not a taste call: `Ablehne…` was not a grammatical German form. + `ablehnen` is a separable-prefix verb, so the first-person singular is *ich lehne + ab* — the pack shipped broken German on `DeviceAuthPage.tsx` to users today. + Re-derived the ruling's premise against the tree before touching anything: the + `de` pack's dominant in-flight register is measurably the passive — 40 of 61 + `en`-bare-gerund keys render `Wird …`, against 21 that don't (a Unicode-aware, + position-aware scan, positive-controlled on `common.loading` → `Wird geladen…`), + close to triage's "roughly 37" and the same conclusion either way. `loading` was + the sole member of objectui#5972's merged `Loading…` group carved out by name in + `LOADING_GROUP_FORKS` (`packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts`), + because converging it alone while `approving`/`denying` stayed first-person would + have manufactured a new same-screen inconsistency. Moving the whole namespace to + the passive removes that fork rather than maintaining it, so the exemption row is + deleted and the pin now asserts zero forks. + + No key added or removed, no `en` value moves, no other locale touched. +- dbd5194: The shell's `sys_activity.type` reading stops calling every unrecognised type an + update (objectui#6730). + + `mapActivityRows` in `hooks/sharedUserFeeds.ts` — the feed behind the AppHeader + bell's Activity tab, Home's activity card and the exported `ActivityFeed` panel + — carried the third hand-written reading of that column in this repo, and it + bucketed every value outside `created` / `deleted` / `commented` / `mentioned` + as `update`. That is not a missing decision; it is a wrong one stated out loud: + a `scheduled` meeting, a `login`, a nightly `system` rollup and an author's + `contract_countersigned` all rendered as "somebody updated this record". + + - New `layout/activityItemType.ts` holds the whole reading — the table, the + generic bucket, the `"NOW()"` timestamp fallback and the row constructor that + applies all three — DOM-free, so what a row becomes is assertable directly. + - `ActivityItem['type']` gains a fifth kind, `system`: the generic bucket, with + its own icon, label and notification toggle. Following + `UNMAPPED_ACTIVITY_FEED_TYPE`'s precedent, an unrecognised value renders + through it and is named once on `console.warn` rather than being dropped — + `sys_activity.type` is author-extensible (objectstack#11507 direction 4), so + an unmapped value is real activity nobody has ruled on, not a mistake. + - The built-ins that had no honest presentation among the four existing kinds — + `system`, `completed`, `scheduled`, `login`, `logout` — now land in that + bucket instead of claiming `update`. `assigned` and `shared` stay `update`: + both write to the record. + + ⛔ The two readings of this column are deliberately NOT converged. + `activityRowToFeedItem` builds a `FeedItem`, and the vocabularies cross: + `FeedItem` collapses create/update/delete into one `field_change` and drops + `commented` / `mentioned` outright, so routing this surface through it would + cost the bell every comment row and every create/delete distinction. What is + shared is a pin, not an import — the new suite reads plugin-detail's real table + (a devDependency; no runtime edge) and fails when the declared vocabulary grows + an entry this side has not read, or when the two readings stop disagreeing in + the three measured ways. +- b392674: Field widgets say WHY they refused an edit in the reader's language + (objectui#6755, maintainer ruling 2026-08-29). + + Three sentences a person has to read to recover from a refusal were string + literals in the widgets, inside a package whose locale channel 11 of its 55 + widgets already use: `ObjectField`'s `Invalid JSON`, and `LocationField`'s + format and range refusals (objectui#6716 / #6714). So a zh / ja / ar user who + mistyped a coordinate or a JSON blob was told why in English, in a form whose + labels, gate hints and validation copy were all translated. + + - All three now read from `useFieldTranslation` / `FIELD_DEFAULTS` under + `fields.object.invalidJson`, `fields.location.refusedFormat` and + `fields.location.refusedRange`, with entries in all ten locale packs — bound + from now on by `check:i18n-drift`. + - The `en` values are byte-identical to the literals they replace, so English + and provider-less rendering are unchanged, and the refusal pins of + objectui#6716 / #6715 and `plugin-form`'s two refusal suites are untouched. + - `fields.location.refusedRange` keys the FRAME only: the interpolated + `{{detail}}` is `LocationValueSchema`'s own complaint, because the widget must + not restate the spec's bounds (a hand-copied range is a second contract). + - Not in scope, and recorded rather than folded in: `LocationField`'s third + refusal sentence — the residue arm objectui#6715 added after the ruling was + written — is still a literal. objectui#6888 carries it. +- 4f3a1e2: fix(plugin-timeline): refuse an unusable gantt date range with a diagnostic that names the offending value + + A gantt whose date range cannot be used now renders a `role="alert"` diagnostic + naming the value that made it unusable, instead of crashing or drawing a chart + that is confidently wrong. Two input classes, which failed in opposite + directions: + + - A date that does not parse threw `RangeError: Invalid time value` mid-render — + the same crash site objectui#6750 guarded for the empty list, on a different + input. This covers a malformed value (`startDate: 'not-a-date'`), an absent + one, and an unparseable `minDate` / `maxDate` pinned on the schema. + - An inverted pinned range (`minDate` after `maxDate`) drew a bar at + `left: 157.9%; width: -4.3%` under a header row with zero cells, with no + error and no diagnostic. + + Valid gantts, the empty-list sentinel from objectui#6750 and the degenerate + `minDate === maxDate` axis are unchanged. +- 320374d: Key `LocationField`'s THIRD refusal sentence — the residue arm — into the locale + packs (objectui#6888). + + Typing a half that is only PARTLY a number (`12abc, 34`) is refused by + `LocationField` with its own sentence, added by objectui#6715. objectui#6755 had + ruled two weeks earlier that a widget's own refusal sentence goes through + `useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences — but it was + written on 2026-08-29 14:53 and this arm landed after, so it stayed a hard-coded + English literal while its two siblings were keyed. + + **The consequence was worse than one more English string.** All three refusal arms + render through the SAME `<p>` and the same `refusalError` state, so after #6755 + landed that one line spoke the reader's language when the format or range arm fired + and English when the residue arm did — objectui#4028's shape ("four Chinese labels + around one English one") compressed into a single sentence position, which reads to + a user as a bug rather than as a missing translation. + + **Arity is answered explicitly, not defaulted.** Unlike the other two sentences, this + one had grammatical number: `verb` was `is not a number` / `are not numbers`, chosen + in TypeScript. Handing a pack an English verb form through a `{{hole}}` gives it a + fragment it cannot inflect around — Arabic has a DUAL, and two halves is exactly that + case. So the verb is not a hole. It lives inside two SIBLING keys picked at the call + site, `fields.location.refusedResidue` and `fields.location.refusedResidueOne`, + following this repo's own plural convention rather than i18next's `_one`/`_other` + suffixes — the same shape `RecordPickerDialog` already uses in this very defaults map + (`lookup.recordCount` / `lookup.recordCountOne`), and for the reason `ReactionPicker` + states in source: zh/ja/ko have no separate singular form, would legitimately omit a + `_one` half, and `all-locales-key-parity` reads that as a missing key. The `ar` pack + now uses its dual (`ليسا رقمين`), which the old implementation could not have produced. + + The English conjunction `' and '` and the coordinate NOUNS go the same way: each pack + writes its own conjunction inside the two-half value, and `latitude` / `longitude` + become `fields.location.latitude` / `fields.location.longitude`, keyed once each and + interpolated into both arities so no locale holds two spellings of the same word. The + only holes carrying untranslated data are `{{text}}` / `{{otherText}}` — the + characters the person actually typed. + + **No behaviour moves.** The English values are byte-identical to the literal they + replace in both arities, verified by `check:i18n-drift` (0 en values changed, 4 added) + and by objectui#6715's own `LocationField.strictNumeric.test.tsx` and `plugin-form`'s + `ObjectForm.locationResidue.test.tsx` passing untouched. Provider-less rendering is + unchanged, the refusal itself is unchanged, and the four new keys are bound from here + on by `check:i18n-keys` and `all-locales-key-parity` like their three siblings. +- 639114c: Carry `manage_org_presentation`, the ninth platform capability (objectui#7122). + + `@objectstack/spec` 17.3.0 declares a ninth member of `PLATFORM_CAPABILITIES` + and the capability picker's curated set carried eight, so + `CapabilityMultiSelectField` fell back to the `sys_capability` registry's + English label for it in every locale — the exact defect objectui#6285 filed + when `manage_sharing` did the same thing. + + The label is the spec artifact's own (`Manage Organization Presentation`), read + off the installed build rather than invented, and it is authored everywhere the + widget's docblock requires of any edit to that list: `useFieldTranslation.ts` + and all ten locale packs. Each non-English string is composed from that pack's + own established sibling vocabulary (`manage_org_users`, + `manage_platform_settings`) rather than machine-translated; a native review pass + is welcome on the nine, and nothing about the capability's behaviour depends on + the wording. + + The parity pin is unchanged and still fails on ANY difference in either + direction, which is what made this visible before it reached a screen. +- 1490691: `dashboard.noRows` and `dashboard.noDataAvailable` are retired — two rows removed from + each of the ten locale packs, 20 entries, zero readers (objectui#7125). + + objectui#7063 routed the three dashboard empty-state renders (`DatasetWidget`, + `ObjectDataTable`, `PivotTable`) through one shared `WidgetEmptyState`, which resolves its + own copy from the `dashboard.empty.*` family. The two keys the old per-widget placeholders + used outlived their call sites in `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. + + Removed under objectui#4658's evidence standard, re-measured on this branch rather than + inherited from the card: zero `t()`/`tt()` call sites for either fully qualified key, no + dynamic `dashboard.` head a substitution could resolve onto them, and every surviving + textual occurrence in `packages/` is a comment recording the consolidation. `pnpm + check:i18n-keys` stays green across the deletion with the `en` pack at 2,962 keys (2,964 + before) and every in-scope call-site key still resolving. + + Not touched: `table.noRows` (`'No rows to display'`) and `engine.form.noRows` + (`packages/app-shell/src/views/metadata-admin/i18n.ts`, read at `widgets.tsx`) — two + different, same-named keys in different namespaces. Nor the comments in + `WidgetEmptyState.tsx`, `DatasetWidget.tsx`, `ObjectDataTable.tsx` and `PivotTable.tsx` + that record WHY three widgets with three strings became one shared empty state; the packs' + own comment keeps that rationale and now names the retirement instead of a row that is + gone. + + `packages/i18n/src/__tests__/dashboard-emptyState-keys-retired-7125.test.ts` pins the removal + by name, following the five prior retirements (objectui#4145, objectui#4392, objectui#4730, + objectui#5504, objectui#6310). Every i18n gate here runs call site → key, so none of them can + see a dead key come BACK into the packs: the reverse sweep that found these is report-only by + design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and + `check:i18n-drift` reported the deletion as `2 removed — those are all-locales-key-parity's`. +- d8ec8d6: `ActivityTimeline` speaks the session locale — the other 18 literals + (objectui#7149). + + objectui#7142 gave this component its first `t()` call (the empty-state title) + and filed the sweep that found the rest. Until now a zh activity tab read + `"Activity(0)暂无活动记录"`: one translated string in a component that was + otherwise entirely English. + + All 18 now resolve from the ten packs, in three groups: + + - **Relative timestamps and the card title** (`just now`, `{{count}}m/h/d ago`, + `Activity`) — these render on *every* activity tab. All five were a pure + lookup swap: the `en` pack value was already byte-identical to the literal, + and the sibling `RecordActivityTimeline` already used the same keys. + - **The `formatFieldChange` sentences** — assembled in code, so they needed new + keys *with* interpolation holes rather than a lookup. Same reachability as the + timestamps: they render for any entry whose optional `description` is absent. + The quotes live inside each pack's value, so every locale punctuates its own + way (de `„…“`, zh `“…”`, ja `「…」`, fr/ru `«…»`). + - **The six filter chips and the chip group's accessible name** — reachable only + through the published export, since no host in this repo passes `filterable`. + + Ten new `detail.*` keys across all ten packs (no inline `defaultValue` — + objectui#3517), mirrored byte-for-byte into `DETAIL_DEFAULT_TRANSLATIONS` so a + provider-less host still reads English rather than a raw key. + + One deliberate English copy change: the chip group's `aria-label` was + `"Activity type filter"` and now resolves `detail.filterActivity` + (`"Filter activity"`) — the key `RecordActivityTimeline` already uses for the + accessible name of its own activity filter, so one control does not carry two + names across two components. + + Also drops the unused `Filter` import (a pre-existing eslint warning). +- 866cd1d: `RecordComments` and `PointInTimeRestore` resolve their copy from the locale + packs instead of hardcoded English (objectui#7163). + + Both files carried their own `formatTimestamp` whose relative-time branches + returned English literals, so a zh/ja/ar session read `5m ago` next to + otherwise translated chrome — the defect objectui#7142/#7149 fixed one file + over in `ActivityTimeline`. + + - **`RecordComments`** was already wired to the packs (11 `t('detail.…')` + references), so this is a pure lookup swap onto `detail.justNow` / + `minutesAgo` / `hoursAgo` / `daysAgo` — keys already present in all ten packs, + each `en` value byte-identical to the literal it replaces. **No new key, no + copy change.** + - **`PointInTimeRestore`** used no translation hook at all, so it is swept + WHOLE rather than having only its timestamps converted: card title, empty + state, field-count line, preview panel, snapshot heading, restore + confirmation and all three buttons. Wiring one string into an otherwise + untranslated component is what shipped objectui#7142's visibly half-done zh + card; that is not repeated here. + + Ten new `detail.*` keys land in all ten packs and in + `DETAIL_DEFAULT_TRANSLATIONS`: `revisionHistory`, `noRevisions`, + `revisionFieldsChanged`, `revisionFieldsChangedOne`, `revisionPreview`, + `revisionSnapshot`, `restoreConfirm`, `restoring`, `confirmRestore`, + `restoreToPoint`. `detail.cancel`, `detail.activityEmptyValue` and + `detail.emptyValue` are **reused**, not forked. + + The restore confirmation becomes one key with a `{{when}}` hole rather than a + sentence assembled around a JSX expression, and the field-count line uses the + repo's two-key plural convention selected by a static ternary over two literal + keys — never `t(KEYS[n])`, which objectui#7149 measured as invisible to the + i18n scanners. + + One deliberate copy change: the snapshot panel's null placeholder was an EN + DASH (`–`, U+2013) written inline and now resolves `detail.emptyValue`, which + is an EM DASH (`—`, U+2014) in all ten packs — the glyph the rest of the detail + package already uses for an empty value. +- 20c04b2: A gantt timeline whose rows are malformed now refuses to draw, naming the row, + instead of crashing the render (objectui#7164, maintainer ruling A+). + + `TimelineRenderer`'s gantt branch used to read the authored rows twice — once + defensively in `findUnusableGanttDate`, once bare in `calculateDateRange` — and + every input in the gap threw a `TypeError` mid-render from ordinary JSON: + `items: [null]`, a row whose `items` is `5` / `true` / `{}` / an array-like + object, or `items` itself not an array. The three readers (the date scan, the + range computation and the render loop) now consume ONE verdict from + `classifyGanttRows`, and a malformed shape renders the existing `role="alert"` + refusal through a new diagnostic key, + `timeline.gantt.unusableRange.malformedRow` — "items[0] is null, which is not + a row shape" — never the `malformedDate` copy, which named the wrong fault. + The key lands in `en` and the nine sibling locale packs. + + `@object-ui/types` (minor — the accept set narrows): `TimelineSchema.items` no + longer declares `z.array(z.any())`. Every element must be an object, and a + gantt row's own `items`, when present, must be an array, so `validate` refuses + `items: [null]` and `items: [{ items: 5 }]` at authoring time — before they + reach a renderer. Feed items (`vertical` / `horizontal`) carry no `items` key + and parse exactly as before; every in-repo `type: 'timeline'` fixture parses + green on both sides of the change. Rows with no bars (`items: []`, a row + without `items`) stay the ordinary empty state and still draw. +- 01c9023: `AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173). + + The AI HITL approval inbox held its own relative-time helper returning hardcoded + English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English + relative times on every row. It is the fifth spelling of that helper in the repo, + and the file had **no translation wiring at all** — the unwired-component shape, + not the lookup-swap shape. + + It is therefore swept whole. objectui#7142 wired one string into an otherwise + untranslated component and shipped something visibly half-done, and objectui#7149 + is what finishing that afterwards cost; the triage ruling on this card (2026-09-01) + carried that forward as *sweep the file whole or leave it*. Everything the user can + read now resolves from the locale packs: the card heading and description, the three + tabs, the refresh button, all five status badges, the six column headings, the empty + state, the row and drawer buttons, all nine drawer field labels, the outcome banner + and the whole reject-reason dialog. + + **No new rows for the four relative-time branches.** `detail.justNow`, + `detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed, + translated, in all ten packs, and cross-package key borrowing is this repo's settled + convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`, + `ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`, + `RecordDetailView` and `apps/console` all resolve `detail.*` from outside + `plugin-detail`. One phrase on one kind of control should not get a second + translation that can drift from the first. + + The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an + `aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately + separate from `approvalsInbox`, which is the human approval-**process** inbox — a + different surface and a different feature, so no rows are shared with it. Four + generic verbs are reused rather than forked (`common.refresh`, `common.cancel`, + `common.loading`, `common.ok`). + + **⛔ The five relative-time helpers are not unified.** They differ in real behaviour + — `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d + against 60s/7d, different tails — so normalising them is a behaviour change wearing + a refactor's clothes and needs its own card. This inbox's arithmetic is untouched, + and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s + threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives + `1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date). + + Two assembled English sentences became single interpolated keys — the outcome banner + (`Approve for {{id}}: {{message}}`) and the drawer subtitle + (`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments + around a `<code>` element cannot express, so the two identifiers lose their monospace + styling. That is the deliberate cost of making those sentences translatable. + + Evidence: an `en`-only assertion cannot discriminate here, because each key's `en` + value is byte-identical to the literal it replaced. The suite asserts in **zh and + ar**, and the provider-less path separately, in its own file (`createI18n` installs + itself as react-i18next's module-level global, so a provider-less render in a file + that has already mounted a provider silently reads that pack instead of the defaults + map). No inline `defaultValue` anywhere (objectui#3517). + + Two consequences of the sweep, both landed here rather than left for CI to find: + + `packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its + `vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph + reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so + the frozen surface made that read `undefined` and the file died during COLLECTION — the + objectui#6849 shape, which does not look like a test failure. It now spreads + `importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of + the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed + this to be the only file whose graph reaches the package. + + The ten pack blocks are locale DATA, and locale data lands in the console's eager + `framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's + ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh + measurement (502,405 to 514,863). Attributed by three console builds: the merge parent + reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and + this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing + else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression + the gate must catch) rather than widened; most of the overage was pre-existing drift, with + the merge parent already at 510,192 of the 512,000 allowed. +- e8c553b: A scatter handed more than one series now refuses instead of drawing a false picture. + + Scatter binds one measure: `series[0].dataKey` is the y axis, and every series was + handed the same rows through that one axis. A second series therefore added a + colour and a legend entry and nothing else — measured, two series over two rows + painted four symbols at two positions, each drawn twice, and the second measure's + values appeared nowhere on the plot. The data was valid and the picture was + confidently wrong, which no existing refusal could see. + + A `chartType: 'scatter'` with two or more `series` now renders the renderer's + refusal shell under `data-chart-error="scatter-multi-series"`, stating that a + scatter plots one measure, naming the fix (keep exactly one series) and listing + the series keys it was handed. A single-series scatter is unchanged. + + This refusal counts authored `series` only. `compareTo` on scatter is out of + its scope: objectui#7402 ruled (b) that scatter joins pie / donut / funnel in + excluding `compareTo` — `supportsCompareTo` and the dashboard widget path stop + synthesising a comparison series for it, so no `…__comparison` overlay is ever + built for a scatter and this guard is never reached by a compare-to document. + That exclusion ships as a separate change; until it lands, a `compareTo` + document still reaches the renderer as two series and refuses here today. + + No multi-measure projection is built (maintainer ruling, 2026-09-02): nothing + in-repo authors a two-series scatter, so that capability waits for a real caller. + The refusal copy is `chart.scatterOneMeasure` in all ten locale packs. +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- 3399704: Fix `AiUsageIndicator` to recognize the free plan's new `resetKind: 'weekly'` and its + `resetsAt` (objectui#7371, consumer of cloud PR #1852's rolling 7-day AI quota window). + + Before this change a `weekly` meter fell through to the component's unrecognized-kind + path and rendered no reset line at all — not a crash, but silently wrong information + next to a live progress ring. The indicator now shows "Resets in N days" (or "Resets in + N hours" once inside the final day, e.g. `console.ai.usage.resetsWeeklyHours`), computed + from the endpoint's `resetsAt`, in both languages via `@object-ui/i18n` + (`console.ai.usage.resetsWeeklyDays` / `resetsWeeklyHours`, real i18next plural families + with a base key so every locale pack resolves correctly, all ten packs translated). D5 is + preserved — no token count is ever rendered, only the days/hours until reset. + + Contract-first: `resetsAt` is read verbatim from the endpoint, never re-derived or + guessed client-side. A `weekly` meter with `resetsAt: null` (nothing counted yet in the + window) and any `resetKind` this build does not recognize both render no reset line — + fail-soft, not a crash or stale copy. + + `AiUsageResetKind` (`packages/app-shell/src/hooks/useAiUsage.ts`) gains the `'weekly'` + member; `resetsAt` was already `string | null` and needed no shape change. +- 2a5bf45: fix(i18n): `createI18n`'s `resources` deep-merges, so overriding one key of a namespace keeps the rest + + A language pack's top-level keys ARE the namespace groups (`common`, `calendar`, + `list`, ...), each a nested object, but `createI18n` merged `resources` over the + built-in packs one level deep. Supplying a partial group therefore **replaced** + it rather than merging into it: + + ```ts + createI18n({ defaultLanguage: 'en', resources: { en: { calendar: { today: 'Heute' } } } }); + ``` + + left `calendar.today` set and dropped `month`, `week`, `day`, `allDay`, + `newEvent`, `moreEvents` and `unscheduled` from the instance. `t('calendar.allDay')` + then returned the bare key, and `calendar.allDay` reached the DOM as literal + text — silently, with no error and no warning. + + The merge now recurses, so a partial group override touches only the keys it + names. Packs nest up to four levels below the group + (`console.ai.empty.build.title`), so it recurses rather than adding one fixed + extra level. Two in-repo call sites were affected by this and are repaired by + the change: `packages/plugin-gantt/demo` supplied 53 of the 80 `zh.gantt.*` keys + and silently lost the other 27 (its own comment says the demo "is never + half-translated" — it was), and the `skills/objectui/guides/i18n.md` setup + example, the documented way to use this API, dropped 46 of the 48 `common.*` + keys in both `en` and `zh`. + + **Arrays are replaced, not concatenated** — stated because it is a decision, not + a library default. No built-in pack carries an array value today (every leaf in + all ten packs is a string), so nothing observable rides on it; the rule decides + what a future array means. An author who writes an array is naming the whole + list, so replacement is the only rule that lets them shorten or reorder one and + the only one that stays idempotent when the merge runs again. This is + deliberately narrower than i18next's own `deepExtend`, which the provider's + async `addResourceBundle` path uses: that recurses into arrays index-wise and + would leave a longer base array's tail behind, which is the same silent-hybrid + shape this fix removes. + + No caller depended on the old replacement semantics: a census of every + `resources` literal in the repo (47 parsed sites, plus the dynamic ones resolved + by hand) found no site that supplied a partial group in order to clear the rest. +- bc5870c: fix(plugin-calendar): a record with no date is no longer placed on today + + `ObjectCalendar` mapped a record whose declared `startDateField` carried no + value to `new Date()` — the current moment — so it rendered on today's cell as + an ordinary event, indistinguishable from a real one. The `isNaN` guard six + lines below could not catch it by construction: a no-argument `new Date()` is + always valid, so the absent-value case became a well-formed lie *before* the + check that would have caught it. + + The fabricating arm is deleted. Such records now leave the grid entirely and + appear in a collapsed "Unscheduled (N)" area below the calendar — a visible + count and an expandable list, with no invented date and no scheduling UI. The + `isNaN` filter keeps its original job for values that are present but + unparseable: absent and malformed stay two distinguishable outcomes. + `allDay: !endDate` now applies only to records that have a start, so a record + with no dates at all is unscheduled rather than silently all-day; a record with + a start and no end still renders all-day exactly as before. + + Adds `calendar.unscheduled` to all ten locale packs. +- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock + noticed it (objectui#5054). + + One abandoned device approval could be noticed by either of two clocks, and the + Cloud Connection panel had a different answer for each. When the panel's own + `expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — + translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered + HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` + threw a bare `Error` carrying only that sentence, and the catch rendered it + verbatim. Same user, same failure, two languages, decided by which clock got + there first — visible on a zh console as the same abandoned approval reading + Chinese or English depending on whether the tab sat open past `expires_in`. + + `getJson` now carries the envelope's `declaredCode` and `code` across its throw, + and a single closed map turns the two RFC 8628 outcomes a user can actually cause + into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, + `access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten + locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream + spelling there — `code` is `DEVICE_CODE_FAILED` for both. + + Every other code is unchanged: `invalid_grant`, and anything upstream invents + next, still render the wire `message`, which stays the single source of truth for + failures this console has no copy for. No API, export or resolver was widened. +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. +- de570cc: fix(i18n): `useObjectTranslation`'s provider-less `t` now interpolates its inline `defaultValue` + + With no `I18nProvider` mounted, react-i18next hands back its not-ready `t`, which + returns `options.defaultValue` **verbatim** — so an inline default written + `'Deleted {{count}} rows'` reached the user with the braces intact. 68 inline + defaults across 24 files rendered through that path on any host that embeds an + ObjectUI component without a provider, which is the configuration + `createSafeTranslation` exists for. + + `useObjectTranslation` now runs its not-ready result through the same one + interpolator `createSafeTranslation`'s `fallbackT` uses, so both provider-less + renderers fill exactly the `{{name}}` spelling the copy is already gated to. The + ready path is untouched: with a provider, i18next's own `t` is returned by + reference and nothing is interpolated twice. Pre-interpolated template-literal + defaults (`` `Deleted ${n} rows` ``) stay correct — they have no holes left to + fill — so no call site changes. +- 1e66879: `console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings — + now exists in all ten locale packs. It previously existed only in `en` and `zh`, so + `ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and + `all-locales-key-parity` failed on `main` (objectui#5705). + + The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was + titled for the locale packs but reached only `zh`, so eight packs × five keys stayed + missing and the parity assertion — which carries no allowlist — was red on `main` and + on every PR whose diff touched source. Source-free diffs skip the shard that runs it, + which is why the breakage survived several merges. + + Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`, + `ru` puts the number last (`…: {{count}}`) as it already does for the sibling + `home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}` + because that value is a runtime message rather than a single token — both choices + carry an in-pack note. Terminology is taken from each pack's existing publish-bar + vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read + alike. + + Both interpolations survive verbatim in every pack — `{{count}}` in `count` and + `{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated + packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a + different node and is untouched. +- c5200f0: Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. +- fd8dace: Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save + + objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. + + The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door. + + `MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones. + + One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb. + + **BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group. + + Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) still discards per-draft advisories server-side — objectstack#9343, open and unruled — and nothing here compensates for that from the client side. A test pins the absence, so a later traversal of a batch-shaped `published[]` cannot be added without turning it red. +- 38a9568: `useObjectLabel` now keeps a stable identity when no i18next instance is bound, + so the memoization it advertises holds on the no-provider path too + (objectui#5564). + + react-i18next's `useTranslation` builds its return value out of a fresh `{}` on + every render when it has nothing to bind to (`const finalI18n = i18n || {}`, + which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived + with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`, + so the memo never held: measured 4 distinct returned objects across 4 renders + with no instance, against 1 with one. That is the wrong way round — the memo + exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in + heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case + as the one it exists to serve. + + Both memo dependencies are now pinned to module-level constants while no + instance is bound. The substitution is unobservable rather than merely + convenient: every `t()` call in the module sits inside a + `for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]` + under exactly the same "is there a usable instance" predicate — so while the + substitution is in effect, the closures cannot read either value. When an + instance appears the dependencies become the live values again, so a provider + mounting after first render recomputes the object exactly once and resolves + real translations from then on. + + No API change: no new exports, no signature changes, and the returned surface is + identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside + `useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the + memo's own docstring names. +- b2437a7: `setLocalized`'s published docblock states the single-locale write rule that is + actually in force, instead of deferring the multi-locale-authoring question to a + closed card (objectui#5591). + + The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163 + closed as completed on 2026-08-15 with that product question still unanswered, so the + parenthetical pointed at nothing — and it read as though the question had been settled + somewhere a reader could go and check. This is the failure mode objectui#5428 + demonstrated is not harmless: there, a dangling deferral of exactly this shape let an + expired justification sit unread for a release cycle at two surfaces. + + The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in + force (`setLocalized` reaches only the entry for the locale the author is in), keep the + open product question open **in place**, and record why there is deliberately no tracker + reference — so the next reader cannot restore one. Re-pointing is how the class + regenerates, because the next card closes too. The same wording form already landed in + `plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`. + + Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with + `pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`. + + Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per + package rather than assumed, and the two packages this change touches differ: + + - `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches + the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is + `composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count: + `dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js` + `06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so + it publishes something. + - `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between + declarations and a test docblock, neither attached to an exported declaration. + `dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild + (`93252e8cdf5a6faa…` both sides). The only artifact that moved is + `dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above + the declarations; no declaration text changed. Nothing user-visible publishes from that + package, so it is not named here. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- Updated dependencies [64dae8e] +- Updated dependencies [9801765] +- Updated dependencies [053fdc8] +- Updated dependencies [490d9a9] +- Updated dependencies [2c3cd1b] +- Updated dependencies [546ddf7] +- Updated dependencies [44d075b] +- Updated dependencies [a26b9e4] +- Updated dependencies [8f1d995] +- Updated dependencies [3c2b6f7] +- Updated dependencies [5961030] +- Updated dependencies [299102e] +- Updated dependencies [831be72] +- Updated dependencies [d0889e2] +- Updated dependencies [4d5f9b4] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [045d20b] +- Updated dependencies [9e37d9b] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [47547d0] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [e62c44e] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [52cac38] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [81c0bc4] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [e719ebd] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [105f3c5] +- Updated dependencies [af3861f] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [6c6cee7] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] + - @object-ui/core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index da0d8d7ba5..eac88e9d5b 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/i18n", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/layout/CHANGELOG.md b/packages/layout/CHANGELOG.md index 728f273d41..5ea61c09af 100644 --- a/packages/layout/CHANGELOG.md +++ b/packages/layout/CHANGELOG.md @@ -1,5 +1,549 @@ # @object-ui/layout +## 17.7.0 + +### Minor Changes + +- 969ba84: Renderers for the `app:launcher` and `nav:menu` page blocks (objectui#6661). + Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two + `PageComponentType` members that are purely metadata-driven, so nothing had to + ship before their renderers could. Phase 2 (`global:search` / + `global:notifications`) landed in objectui#6757 and set the pattern this + follows. + + A page that declared either member drew a dashed box. The two symptoms were not + the same, which is worth recording because it decides what "fixed" looks like + for each: + + - `nav:menu` is in `PALETTE_PLACEHOLDER_BLOCKS`, registered eagerly, so it drew + the literal "Component Placeholder" scaffold in every host. + - `app:launcher` is only in `PROTOCOL_COMPONENTS`, registered when a host opts + in via `registerPlaceholders()` — which just `apps/console` does. So it drew + the scaffold in the console and `SchemaRenderer`'s red OBJUI-001 "Unknown + component type" panel everywhere else. + + Neither block adds a data layer — each mounts plumbing that was already live, + and neither issues a request or touches an adapter: + + - `app:launcher` reads the metadata app registry (`useMetadata().apps`, which + `MetadataProvider` fetches eagerly) through the shared `filterActiveApps` + predicate, and draws it with `HomeAppsStrip` — the console's own launcher + grid — so an authored launcher and the Home launcher cannot drift into two + looks for one thing. + - `nav:menu` reads the active app's navigation tree from that same registry and + renders it as page content, taking every derived fact from `@object-ui/layout`: + hrefs from `resolveHref`, labels from `resolveNavItemLabel`, the active row + from `resolveActiveNavItem`, and the item-level guards (`visible`, + `requiredPermissions`, `requiresObject` / `requiresService`) in the order + `NavigationItemRenderer` applies them, wired to the same console providers + `AppSidebar` wires them to. `action` items dispatch through + `useNavActionDispatch`, so framework#4509's "renders but dead-clicks" shape is + not reintroduced. + + `nav:menu` does not mount `NavigationRenderer` itself: that renders through + `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's + `SidebarProvider`, and a page block has to render standalone. `@object-ui/layout` + therefore exports `resolveNavItemLabel`, which was module-private — an additive + export with no behaviour change, so the sidebar and an authored menu cannot show + one nav entry under two names. + + Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an empty + shape for each, and both use `skipFallback: true` so neither claims the bare + `launcher` / `menu` keys. This does not change the Studio page palette — + `app:launcher` remains recorded there as a shell singleton, which is a palette + decision independent of whether a declared type renders. + + Three new strings — the launcher's and the menu's accessible names, and the + menu's empty state — are declared under `console.nav` in `en.ts` and its nine + sibling packs. An inline `defaultValue` alone is not a fix: it renders English + at one call site and leaves the string untranslatable everywhere + (objectui#3517). +- e62c44e: Re-home the breakpoint layout vocabulary and delete the two dead responsive + implementations (objectui#7580, maintainer ruling 2026-09-04, option A). + + **Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole + `ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, + `BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the + stated ground that the four types "had no other authorable carrier". That ground is + measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component + whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by + `resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in + `@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and + when a renderer implements it" — is already met here, so the two types a renderer reads + are re-homed rather than retired. + + What survives, under the same names and the same members: + + - `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead + of re-exported from the spec. **No consumer change**: same name, same six members, same + export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. + - `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), + verbatim from the retired `$strict` schema: six optional column counts, no index + signature. `responsive-grid`'s `columns` input and its resolver are unchanged. + + What is removed: + + - `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had + no read point in the package; it was published only because the retired + `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` + needed the type. With the schema gone there is no order vocabulary for it to be the type + of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. + - `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and + `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with + `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / + `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired + `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). + - `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once + the two implementations above went, dropped rather than re-declared locally, the same + disposition the retired i18n names in that file already carry. + + No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, + `ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and + `responsive-grid` itself — are untouched. + + **Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` + objectui#7580: the retirement is merged upstream and unreleased, so this must land first. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- fe76ece: The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. + + `vite-plugin-dts` emits one declaration file per source file, and TypeScript + copies a module specifier into the declaration verbatim. `export * from './ui'` + therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in + `@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted + trees. Node16/NodeNext resolution does not extension-search a relative + specifier, so the compiler could follow none of the hops and every symbol they + carried read as absent from the package: + + ``` + error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. + ``` + + Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls + in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from + `layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. + On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. + + The emitted `.js` never had the defect — rolldown resolves the same specifier + away — which is why `pnpm check:esm-specifiers`, whose verdict is about + specifier-preserving `.js` builds, correctly never scanned either package. The + fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, + shared by both `vite.config.ts` files), not in the sources: the same source line + produces a clean `.js` and a broken `.d.ts`, so no source edit can express the + difference. The rewriter resolves each specifier against the source tree the + output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — + throws on anything it cannot resolve, and after the build re-parses the emitted + declarations to assert every relative specifier both carries an extension and + names a file the build really emitted. + + `packages/fields` takes the `nodenext` pin as a result — the same two lines + `packages/react` has carried since objectui#4538 — so the property is enforced by + the compiler on the consumer side rather than by review. `packages/app-shell` + does not: it type-checks clean without the pin and still shows 23 errors with it, + none of them from these two packages. That residue is filed separately. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/layout/package.json b/packages/layout/package.json index e4a160cb43..30b79f4beb 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/layout", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 4161e7dc50..7432a26687 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,5 +1,397 @@ # @object-ui/mobile +## 17.7.0 + +### Minor Changes + +- 2d7304d: Retire the `MobileOverrides` type and its `mobileOverrides` mount point (objectui#4919, + maintainer ruling 2026-08-19, ADR-0049 enforce-or-remove). + + `MobileOverrides` published a six-key mobile override surface — `layout`, `columns`, + `useBottomSheet`, `fullScreen`, `touchTarget` and a three-value `navigation` vocabulary + (`'bottom-tabs' | 'hamburger' | 'drawer'`) — from `@object-ui/types` and, re-exported, + from `@object-ui/mobile`. Nothing read any of it. Measured on current `main`: the type had + exactly four mentions repo-wide — its own declaration, the single + `MobileComponentConfig.mobileOverrides` mount point, and the two barrel re-exports — and + the lower-case property name (the spelling a renderer would actually read) appeared only + in that declaration. No renderer, hook or adapter resolved it, and a sweep of the example + apps and the `objectstack` sibling checkout found zero authors. The three `navigation` + values were three spellings of the same no-op. + + The declared surface is removed rather than narrowed. The #3985 lineage's rule is "narrow + to the implemented values"; here the implemented set is empty, so that rule terminates in + deletion — a config that type-checks, builds and silently does nothing is the + declare-without-enforce shape the platform doctrine forbids. + + Removal rather than a `?: never` tombstone follows this package's own discriminator. A + tombstone exists to steer authors to a named live replacement — `crud.ts` `confirm` → + `confirmText` (objectui#4314), `data-display.ts` `hoverable` / `striped` → `data-table` + (objectui#5474) — or to keep a key loud that the docs had actively taught as working. + Neither applies: there is no replacement key to steer to, no documentation ever described + the surface, and there is no successor spelling. That is the same zero-pull, no-successor + shape as the retired `AccordionItem.icon` (objectui#4652) and `ToggleGroupItem.icon` + (objectui#4632), both of which were removed outright rather than tombstoned. + + **Breaking for TypeScript authors of `MobileOverrides` / `mobileOverrides` only** (marked + `minor` per this repo's version-alignment rule, which reserves `major` for following + `@objectstack` across a major — see AGENTS.md's 版本号策略, and the identical + classification used for `AccordionItem.icon`). Runtime behaviour is unchanged: an authored + `mobileOverrides` did nothing before and does nothing now. What changes is that the + contract no longer claims otherwise, so the mistake surfaces at authoring time — importing + the type is now a "has no exported member" error, and authoring the key on a + `MobileComponentConfig` object literal is an excess-property error, instead of a silent + no-op that type-checks and builds. + + If real mobile-override renderer work is ever wanted it re-enters deliberately, as designed + product surface on its own card, with the renderer landing in the same change as the + declaration — not by resurrecting this declaration. +- 90665e0: **Removes a published export.** Retire the `MobileComponentConfig` type + (objectui#5942, ADR-0049 enforce-or-remove). The name is deleted from + `@object-ui/types` and from `@object-ui/mobile`, which re-exported it — after + this release `import type { MobileComponentConfig }` from either package is a + compile error, not a deprecation warning. + + `MobileComponentConfig` published a four-key "mobile component schema + extension" — `responsive`, `gestures`, `pullToRefresh` and `infiniteScroll` — + and nothing read it. Re-measured on current `main` before anything was deleted: + the type had exactly four code mentions repo-wide — its own declaration, one + doc-comment cross-reference, and the two barrel re-exports. It had **no mount + point at all**: no type mounted it as a property, nothing extended it, and no + renderer, hook or adapter annotated, cast to or imported it. A sweep of the + example apps and the `objectstack` sibling checkout found zero authors. Every + read-shape probe returned zero against a control lit in the same run. + + That makes it stricter than the usual case: not merely a surface whose values + were unimplemented, but a container with no path by which any authored value + could reach a renderer. objectui#4919 removed its last member + (`mobileOverrides`), which is what left the container itself inert. + + Removed outright rather than kept as a `?: never` tombstone, on this package's + own discriminator: a tombstone steers authors to a named live replacement key + (`crud.ts` `confirm` to `confirmText`; `data-display.ts` `hoverable`/`striped` + to `data-table`), or keeps loud a key the docs taught as working. Neither + applies — the whole interface goes, so there is no surviving object to hang a + `never` key on, and no documentation ever described it + (`skills/objectui/guides/mobile.md` teaches the hooks, never this type). Same + zero-pull, no-successor shape as `MobileOverrides` (objectui#4919) and + `AccordionItem.icon` / `ToggleGroupItem.icon`. + + ## Upgrading + + **No behaviour changes and there is nothing to migrate at runtime.** An object + authored against this type did nothing before and does nothing now; what + changes is that the contract no longer claims otherwise, so the mistake + surfaces at authoring time instead of silently type-checking. + + - **You imported the type only** (the only thing that was possible — nothing + accepted it as a value): delete the import. If you kept a local config object + annotated with it, drop the annotation; the object was never passed anywhere + that read it. + - **You actually wanted the behaviour:** it exists, and it is not being + retired. It lives in `@object-ui/mobile` as React hooks, which is where the + working code always was — `useResponsive` / `ResponsiveContainer` for + `responsive`, `useGesture` for `gestures`, `usePullToRefresh` for + `pullToRefresh`. `infiniteScroll` has no hook; it was never implemented in + any form. See `skills/objectui/guides/mobile.md`. + - **You want a declarative mobile config surface:** that re-enters deliberately + as designed product surface on its own card, with the renderer that reads it + landing in the same change as the declaration — not by restoring this + declaration. + + **Do not follow the compiler's suggestion.** TypeScript reports the removal from + `@object-ui/types` as TS2724 and appends `Did you mean 'ComponentConfig'?`. That + is a lexical near-match, not a migration target: `ComponentConfig` is the + renderer **registration** record (`{ type: string; component: T }`, extending + `ComponentMeta`) and has nothing to do with mobile configuration. The import + from `@object-ui/mobile` gets a plain TS2305 with no suggestion at all. + + Marked `minor`, not `major`, per this repo's version-alignment rule, which + reserves `major` for following `@objectstack` across a major (AGENTS.md + 版本号策略) — the same classification objectui#4919's identically breaking type + removal used. **Breaking for TypeScript consumers of the name only.** + + Follow-up, deliberately not widened into this change: `MobileResponsiveConfig` + and `GestureConfig` were consumed only by this container and are now + zero-consumer published types themselves. Filed as objectui#7519 for triage. +- 51eb515: **Removes two published exports.** Retire the `MobileResponsiveConfig` and + `GestureConfig` types (objectui#7519, ADR-0049 enforce-or-remove). Both names + are deleted from `@object-ui/types` and from `@object-ui/mobile`, which + re-exported them — after this release `import type { MobileResponsiveConfig }` + or `import type { GestureConfig }` from either package is a compile error, not a + deprecation warning. + + Each had exactly one consumer: the `responsive` and `gestures` members of + `MobileComponentConfig`, which objectui#5942 retired. Re-measured on current + `main` before anything was deleted, each was a declaration plus the two barrel + re-exports and nothing else — no type mounted either, nothing extended, + annotated, cast to or imported them outside the barrels, and the example apps + and the `objectstack` sibling checkout had zero authors. A value written against + either could not reach a renderer or a handler by any path. That is the same + declared-surface-with-no-consumption-path shape as `MobileComponentConfig` + itself and `MobileOverrides` (objectui#4919) before it, one level down. + + Removed outright rather than kept as `?: never` tombstones, measured against + this package's two-prong discriminator (a tombstone steers authors to a named + live replacement key, or keeps loud a key the docs taught as working). Prong 1: + neither has a replacement key — the behaviour they named lives in hooks, and + `SpecGestureConfig` is a different contract, not a successor. Prong 2: the only + release-note lines naming either are the objectstack#4115 rename-ledger rows + and, for `GestureConfig`, the objectui#3363 reclaim note; none taught a + renderer or dispatcher reading them, and no member carried a published + `@default` (contrast `triggerIcon`, tombstoned by objectui#7654 on exactly that + evidence). Structurally there is also no silent-strip hazard for a tombstone to + guard: whole interfaces go, nothing ever parsed them, and the mobile module has + never had a `zod/` twin to host a `retirementTombstone()`. The compiler was the + only channel these names ever had, and the refusal now lives there. + + ## Upgrading + + **No behaviour changes and there is nothing to migrate at runtime.** An object + authored against either type did nothing before and does nothing now; what + changes is that the contract no longer claims otherwise, so the mistake surfaces + at authoring time instead of silently type-checking. + + - **You imported a type only** (the only thing that was possible — nothing + accepted either as a value): delete the import. If you kept a local object + annotated with it, drop the annotation; it was never passed anywhere that read + it. + - **You wanted per-breakpoint layout:** it exists and is not being retired — + `useResponsive` / `ResponsiveContainer` / `useBreakpoint` in + `@object-ui/mobile`. `ResponsiveValue` and `BreakpointName` stay exported from + both packages. + - **You wanted to bind a gesture to a handler:** `useGesture` in + `@object-ui/mobile` takes `{ type: GestureType, onGesture, threshold?, + longPressDuration?, enabled? }`. `GestureType` and `GestureContext` stay + exported from both packages. + - **You want a declarative mobile config surface:** that re-enters deliberately + as designed product surface on its own card, with the renderer that reads it + landing in the same change as the declaration — not by restoring these + declarations. + + **Do not follow the compiler's suggestion for `GestureConfig`.** Measured against + the built declarations: `import type { GestureConfig }` from either package now + fails as TS2724 with `Did you mean 'SpecGestureConfig'?`. That is a lexical + near-match, not a migration target. `SpecGestureConfig` is the retired + `@objectstack/spec` `ui/touch` **tuning** record (`{ type, label, enabled, + swipe, pinch, longPress }`) that `useSpecGesture` reads; it has no `action` + member and does not bind a gesture to anything. `MobileResponsiveConfig` fails + as a plain TS2305 with no suggestion from either package. + + Marked `minor`, not `major`, per this repo's version-alignment rule (AGENTS.md + 版本号策略), which reserves `major` for following `@objectstack` across a major — + the same classification objectui#5942 and objectui#4919 used for identically + breaking type removals. **Breaking for TypeScript consumers of the two names + only.** The in-repo consumer count is zero; consumers outside this repository + that import either name from either package are not visible from here, which is + why this entry is graded on the published-surface change and not on that count. +- e62c44e: Re-home the breakpoint layout vocabulary and delete the two dead responsive + implementations (objectui#7580, maintainer ruling 2026-09-04, option A). + + **Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole + `ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, + `BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the + stated ground that the four types "had no other authorable carrier". That ground is + measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component + whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by + `resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in + `@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and + when a renderer implements it" — is already met here, so the two types a renderer reads + are re-homed rather than retired. + + What survives, under the same names and the same members: + + - `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead + of re-exported from the spec. **No consumer change**: same name, same six members, same + export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. + - `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), + verbatim from the retired `$strict` schema: six optional column counts, no index + signature. `responsive-grid`'s `columns` input and its resolver are unchanged. + + What is removed: + + - `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had + no read point in the package; it was published only because the retired + `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` + needed the type. With the schema gone there is no order vocabulary for it to be the type + of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. + - `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and + `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with + `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / + `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired + `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). + - `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once + the two implementations above went, dropped rather than re-declared locally, the same + disposition the retired i18n names in that file already carry. + + No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, + `ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and + `responsive-grid` itself — are untouched. + + **Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` + objectui#7580: the retirement is merged upstream and unreleased, so this must land first. + +### Patch Changes + +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [d2fb6ef] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [d6ceb8d] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [6bca0e4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 779c8fa368..385f592a03 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/mobile", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Mobile optimization for Object UI with responsive components, PWA support, and touch gesture handling.", diff --git a/packages/permissions/CHANGELOG.md b/packages/permissions/CHANGELOG.md index 70e748b38a..30256e9f73 100644 --- a/packages/permissions/CHANGELOG.md +++ b/packages/permissions/CHANGELOG.md @@ -1,5 +1,247 @@ # @object-ui/permissions +## 17.7.0 + +### Minor Changes + +- 3c9fca3: Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. + +### Patch Changes + +- 45ac2cb: `usePermissions()` now returns an identity React cannot discard (objectui#6724). + + The hook cached its return in a `useMemo` keyed on `[ctx]`, and both branches build a + fresh object — an object literal when no provider is mounted, a spread of `ctx` when one + is. `useMemo` carries no semantic guarantee: React may throw the cache away and recompute + even when `[ctx]` compares equal, and that hands the caller a new identity while every + permission it carries is unchanged. + + That matters because consumers name this value in dependency arrays — 13 arrays across 6 + files: `ListView`'s data-fetch effect (`perms`), `DetailView`'s `gatedSchema`, + `ObjectForm`, `ModalForm`, `ObjectGrid`, `RelatedList`. A discard alone re-ran the fetch + effect and re-issued `dataSource.find` with nothing an author or a caller controls having + changed. Same family as objectui#6018 / #5976 / #6591 / #6592 / #6697. + + The by-identity dependency at the consumers is the correct shape and stays: what they read + off this object is the verdict FUNCTIONS (`checkField(object, field, 'read')`, + `can(object, 'update')`) over an open set of field names, which flatten to no fixed list of + primitives the way objectui#6592's `dataConfig` members did. So the fix is at the hook, + where the identity can be made trustworthy: + + - the decoration becomes a plain function of `ctx` — the same context value always yields + the same object, because the mapping lives in a module-level `WeakMap` React has no say + over, keyed weakly so it dies with the provider's value. That is strictly stronger than + the memo it replaces: the identity is now stable across every component reading the same + provider, not just across one component's re-renders. It also costs no hook, so there is + no render-phase ref write and no state adjustment to reason about. + - the no-provider answer becomes one shared frozen module constant. Every member is a pure + constant function, so there was never anything per-instance to keep, and a single frozen + object cannot churn in any component for any reason. + + A new context value still produces a new identity, on purpose: that is a real permission + change and every consumer must see it. + + No permission value moves: the returned object still spreads `ctx` by identity and derives + `can`/`cannot` from `ctx.check`, and the documented no-provider fallbacks (`isLoaded: + false`, `userId: null`, `systemPermissions: undefined` with `hasCapabilities` fail-open — + objectui#5683 / #4656) answer exactly as before. + + Measured while fixing, and worth recording: on React 19.2.8 this repo has no reproduction — + 51 re-renders with no provider, 51 with one and 42 under `StrictMode` each returned ONE + identity, and there is no `Activity`/Offscreen subtree here. This closes a latent hazard, + not an observed re-fetch. The providers' own context-value memos are the remaining link in + the same chain (objectui#6813). +- 30266cf: Both permission providers now build their context value where React cannot discard it + + `PermissionProvider` built its context value in a `useMemo` over four + `useCallback`s, and `MePermissionsProvider` in a `useMemo` over six. Neither + carries a semantic guarantee: React is permitted to discard the cache and + recompute even when the dependency list compares equal, and every one of those + factories builds a fresh object. A discard would therefore hand + `PermCtx.Provider` a NEW context value with every permission it carries + unchanged — which moves the key `usePermissions()` caches on, and re-runs the + consumer chain that names it: `ListView`'s data-fetch effect (an extra + `dataSource.find`), `DetailView`'s gatedSchema, `ObjectForm`, `ModalForm`, + `ObjectGrid` and `RelatedList`. + + ⚠️ This is **hardening, not a repair**. Nothing misbehaves today: on this + repo's pinned React 19.2.8 the cache is not discarded spontaneously — 51 + re-renders with no provider, 51 with one and 42 under `StrictMode` each + returned one identity — and there is no `Activity`/Offscreen subtree here, + which is the documented case where React does throw memo caches away. What is + removed is the dependency on React continuing not to exercise a licence it + holds. + + Each cached member and each context value is now keyed on the identities of the + inputs it is derived from, in a module-level `WeakMap` React has no say over — + the same technique that made `usePermissions()`'s own return discard-proof one + link down the chain. The dependency sets are unchanged, so nothing churns more + often than it did, and a genuine permission change still publishes a new + context value to every consumer. Two providers given the same inputs now share + one context value, which is stricter than the per-instance memo it replaces. + + No published export changes, and the context carries exactly what it carried + before. +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [d2fb6ef] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [d6ceb8d] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [6bca0e4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/permissions/package.json b/packages/permissions/package.json index 0cf8d326da..77887fb437 100644 --- a/packages/permissions/package.json +++ b/packages/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/permissions", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "RBAC permission system for Object UI with object/field/row-level access control, permission guards, and hooks.", diff --git a/packages/plugin-ai/CHANGELOG.md b/packages/plugin-ai/CHANGELOG.md index 028227253e..cf6aa129ed 100644 --- a/packages/plugin-ai/CHANGELOG.md +++ b/packages/plugin-ai/CHANGELOG.md @@ -1,5 +1,405 @@ # @object-ui/plugin-ai +## 17.7.0 + +### Minor Changes + +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-ai/package.json b/packages/plugin-ai/package.json index 423e01c35d..df270b3e20 100644 --- a/packages/plugin-ai/package.json +++ b/packages/plugin-ai/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-ai", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/packages/plugin-calendar/CHANGELOG.md b/packages/plugin-calendar/CHANGELOG.md index 4289380882..7aaf0d45c1 100644 --- a/packages/plugin-calendar/CHANGELOG.md +++ b/packages/plugin-calendar/CHANGELOG.md @@ -1,5 +1,1181 @@ # @object-ui/plugin-calendar +## 17.7.0 + +### Minor Changes + +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- 7ef3867: Retire the `filter.calendar` configuration spelling on `object-calendar` (objectui#7711). + + **Breaking, deliberately.** `getCalendarConfig` no longer probes `schema.filter` for a + `calendar` key. A calendar whose configuration was written as + `filter: { calendar: { startDateField: … } }` no longer resolves a configuration at all + and now renders the component's existing "Calendar configuration required. Please + specify startDateField and titleField." refusal screen. Write the configuration under + the declared `calendar` container instead — the read for it already existed, directly + below the retired arm. + + `filter` is the query filter and nothing else. `@objectstack/spec`'s + `ComponentPropsMap['object-calendar']` declares `calendar` as the configuration + container and `filter` as the base query filter, and admits no `filter.calendar` + spelling; the renderer nevertheless read the config out of the filter FIRST, and the + comment on the canonical read below called *that* one the "backward compatibility" + branch — the contract inverted in a source comment. The arm and the comment are both + gone. No compatibility rung and no deprecation window, per AGENTS.md #0.1: a tolerant + fallback fossilizes the wrong convention into a second de-facto contract. + + **The bug this closes is not only the retired spelling.** One authored key was being + read twice with two incompatible meanings. `filter: { calendar: 'team' }` — a + legitimate condition on a field literally named `calendar` — was returned as the + `CalendarConfig` while the same object still went to `$filter` on the wire. `'team'` is + truthy, so the refusal screen did not fire either: `startDateField` destructured off a + string is `undefined`, every record fell into "unscheduled", and the grid rendered empty + while the author's declared `calendar` block sat unread. Such a filter now reaches + `$filter` untouched and the declared container is what configures the calendar. + + The calendar twin of the `filter.map` retirement in objectui#4034, with one measured + difference: there is no `Array.prototype.calendar`, so `'calendar' in schema.filter` was + never true for an array-shaped filter the way `'map' in schema.filter` was true for + every one of them. Only an object-shaped filter with an own `calendar` key ever reached + the retired arm, and the calendar answers the loss of a configuration with a named + refusal on screen rather than the map's silent fallback to default field names — so this + retirement carries no dev-time diagnostic rider. + + **Migration.** A census of this repo found **zero** authored `filter: { calendar: … }` + sites (both the object-literal and JSON spellings, with positive controls and a planted + fixture proving the search fires). + + Also in this change: the `calendarConfig` memo's dependency list drops `schema.filter`, + which the function no longer reads, and gains the three flat keys it does read but the + list never named — `startDateField`, `endDateField` and `allDayField`. Dropping `filter` + removed an accidental co-trigger that used to pick those up whenever a filter changed + alongside them. +- 0ead1f6: Declare the `filter` input on every `object-kanban` / `object-calendar` + registration (objectui#7712) — the html tier stops reporting `unknown-prop` on a + key the spec declares and both renderers read. + + `ObjectKanban.tsx` sends the authored key to the query as `$filter: schema.filter` + and `ObjectCalendar.tsx` does the same, and `@objectstack/spec`'s + `ComponentPropsMap` declares `filter` on both blocks (measured: `safeParse` + accepts it, and refuses an undeclared key by name on the same call). But none of + the four registrations that publish those two renderers listed `filter` in + `inputs`, and `sdui-parser`'s `validateTree` reports `unknown-prop` for every key + no `inputs` entry claims. So an author writing the one spelling that WORKS was + told it was unknown — objectui#6678's shape, where a correct write draws the same + diagnostic as a write that does nothing. That is worse than an inert key: it + actively punishes the correct behaviour, and the honest response to it is to + delete working metadata. + + ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has + live readers on both ends, so the registrations were the side that was wrong. The + declaration is `type: 'array'` on all four, matching the `filter` that + `object-grid`'s `GRID_QUERY_INPUTS` and `object-metric` already publish, and it is + writable as one shape only because objectui#7711 landed first and retired the + object-shaped `filter.calendar` spelling — `filter` is the query filter and + nothing else. + + Declared per key against the spec rather than derived from the + `ElementDataSourceMapping` sitting beside these registrations, even though that + mapping already asserts `filter` is a live query key. Measured, that derivation + would be wrong: the kanban mapping also carries `limit`, and the spec's strict + `ComponentPropsMap['object-kanban']` **rejects** `limit` by name, so emitting it + would publish a key the save gate refuses. + + ⚠️ Note for whoever meets this class next: `check:react-blocks-declaration-parity` + runs manifest → spec, one direction. A key the SPEC declares and the manifest + omits is structurally outside what that ratchet measures, so fixing these four + registrations does **not** make the next omission loud. Making that ratchet + bidirectional is its own card. +- 35e6366: Declare the `sort` input on both `object-calendar` registrations + (objectui#8171) — the html tier stops reporting `unknown-prop` on a key the spec + declares and `ObjectCalendar` already reads. + + objectui#7712's defect, one key over. `ObjectCalendar.tsx` lowers the authored + key onto its own query as `$orderby: convertSortToQueryParams(schema.sort)`, and + `@objectstack/spec`'s `ComponentPropsMap['object-calendar']` declares `sort` + (measured on 17.2.0: `safeParse({ objectName, sort })` returns `success: true`, + while the same strict schema on the same call refuses `bogusProp` by name — that + control is what makes the acceptance a verdict). But neither of the two + registrations that publish this renderer — `plugin-calendar:object-calendar` and + `view:calendar` — listed `sort` in `inputs`, and `sdui-parser`'s `validateTree` + reports `unknown-prop` for every key no `inputs` entry claims. So an author + writing the one spelling that WORKS was told it was unknown — objectui#6678's + shape, where a correct write draws the same diagnostic as a write that does + nothing. + + ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has + live readers on both ends, so the registrations were the side that was wrong. + Declared `type: 'array'`, matching the `filter` entry objectui#7712 put beside + it and the `sort` that `object-grid`'s `GRID_QUERY_INPUTS` publishes — measured, + that arm is the repo-wide convention for this key, all seven existing `sort` + declarations write it. + + ⛔ Not derived from the `ElementDataSourceMapping` sitting six lines above, + which already asserts `sort: true`. That structure maps query keys for + `ElementDataSourceGate`; it is not an authoring declaration, and objectui#7712 + measured why a mechanical derivation would be wrong — kanban's mapping also + carries `limit`, which the spec refuses by name. + + ⚠️ Carried forward from objectui#7712, because it stays true here: + `check:react-blocks-declaration-parity` runs manifest → spec, one direction. A + key the SPEC declares and the manifest omits is structurally outside what that + ratchet measures, so declaring this key does **not** make the next omission + loud. Making that ratchet bidirectional is objectui#8176. + + Rider carried by the same change, because objectui#8212 landed first: the + console's `registry-inputs-spec-parity` ledger is updated in step with the + declaration — the `object-calendar.sort` unpublished-key exemption is deleted + (its cover expires the moment the key is declared), the shrink-only objectui#8176 + backlog ceiling drops 16 to 15, and `sort` gets a `MEMBER_PINS` entry rather than + a member-pin exemption. `MEMBER_PIN_EXEMPTION_CEILING` is untouched at 62. + + ⚠️ That member pin is deliberately NOT the identity-forwarding shape the two + `filter` pins use. `ObjectCalendar.tsx` writes + `$orderby: convertSortToQueryParams(schema.sort)`, which builds a new + `field -> direction` map, so this key is lowered rather than passed through and + `toBe` on `$orderby` is false about it. The pin asserts what is READ inside a + member instead — `field`, `order`, an omitted `order` meaning ascending, and a + member with no usable `field` dropped rather than invented. + + No version bump is declared for `@object-ui/console`: its only edit here is that + test file's ledger data, which publishes nothing. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 3e853c9: Let a producer-marked refusal reach the drag-write surfaces (objectui#5902). + + The kanban card-move toast, the calendar drag-to-reschedule toast and the OCC + conflict dialog each substituted a generic string for a refusal the producer had + marked as user-facing (`userMessage`), so a user was told "Save failed" where the + application author had written a sentence addressed to them. All three now read + the marking through the shared `declaredUserMessage` reader, which covers both + places the adapter boundary parks it — the typed member on + `ConcurrentUpdateError` and the details bag on `DataApiValidationError`. + + Nothing unmarked changes: the reader answers `null` for it, so every existing + generic substitution — including the localized "not authorized" message that + keeps raw server diagnostics away from end users — still governs unmarked + refusals exactly as before. + + The two toasts substitute; the conflict dialog augments. Its description also + explains what the destructive "Overwrite" button does, which is affordance copy + that surface owns rather than a refusal message, so the marking leads and that + paragraph stays. +- 7975f2d: A standalone `object-calendar` bound to an object now queries WITH its `$expand`, so + lookup / master_detail / user / tree fields render the related record instead of a raw + foreign-key id (objectui#6453). + + `ObjectCalendar`'s fetch effect built its expand set from a ref assigned in the render body + (`objectSchemaRef.current = objectSchema`) and left `objectSchema` out of its dependency + list. That bought the effect exactly one run per mount and paid for it with the expansion, + permanently: on that one run the ref was still `null`, `buildExpandFields` saw no fields, + the query went out with no `$expand` at all, and nothing re-ran the effect when the schema + landed. Only the standalone calendar reached this path — one hosted by `ObjectView` or + `ListView` receives its rows as `data`, which objectui#6419 already covers. + + The ref is replaced by a settled-and-keyed resolution (`{ key, def } | null`) that GATES the + record query, the third member of the family after objectui#6271 (`ObjectKanban`) and + objectui#6419 (`ObjectView`). Measured on this component rather than inherited: gated, the + calendar issues one query carrying `$expand` in every latency profile; the alternative of + adding `objectSchema` to the dependency list issued two, and when the schema read was the + slower of the two it painted raw ids, reverted to the "Loading calendar..." placeholder, + then swapped — a three-step paint the correct rows do not arrive any later than. + + The gate is on the schema read having SETTLED, never on a truthy schema: an adapter that + exposes no `getObjectSchema`, and a read that throws, both settle with nothing and the + calendar still queries (unexpanded) rather than waiting forever. An inline `value` data set + is deliberately not gated — it issues no metadata read, so there would be no resolution to + wait for. +- f9653ae: Re-key the load-bearing fetch effects in `ObjectMap`, `ObjectCalendar` and + `ObjectGantt` onto the primitive fields they actually read off `dataConfig` + (`provider` / `object` / `items`) instead of the whole memoised `dataConfig` + object (objectui#6592, the deferred half of objectui#6270/PR #6591). + `ObjectTree` is a census member too but is deferred out of this change — see + the PR body — because its own fetch effects are the surface of PR #6696 + (objectui#6481), open at the same time. + + `useMemo` carries no semantic guarantee — React is permitted to discard a + memo cache and recompute even when its dependency array compares equal to + the previous render, and the local `getDataConfig(schema)` helper each of + these renderers carries builds a fresh `{ provider, object }` / + `{ provider, items }` wrapper object on every call. So a fetch effect keyed + on `dataConfig` itself was correct only for as long as that identity + happened to survive a discard: a recompute alone (no author or caller + action) was enough to re-run the effect and issue an extra + `dataSource.find` / `dataSource.getObjectSchema` call. Keying the effects + on the primitives instead makes a cache discard a no-op, restoring + `useMemo` to a pure optimisation. + + `ObjectGantt`'s `effectiveDataSource` memo deliberately keeps `dataConfig` + as a dependency (`resolveDataSource` needs the whole provider-shaped + value — the `api` provider's `read`/`write` request config cannot be + flattened to a fixed primitive list the way `object`/`value` can), so its + `reload()` fetch is decoupled from the redundant direct `dataConfig` + dependency but not from `effectiveDataSource`'s own; for the `object`/`value` + providers `resolveDataSource` returns its `fallback`/a fresh + `ValueDataSource` respectively rather than reading further into the config, + which is enough for the two fetch effects to observe no extra call under a + recomputed-but-equivalent `dataConfig` in the common case. + + No behaviour change for a schema whose `useMemo` caches survive normally; + the effects are unaffected by React discarding one. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 7c3df8f: The settled-schema convergence, and the gantt's duplicate query gated + (objectui#7225, maintainer ruling B, 2026-09-02). + + `useSettledSchema` was extracted and published in PR #6690 with exactly **one** + non-test adopter (`ObjectTree`, the component that had an actual defect — + objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and + `ObjectCalendar` kept their own hand copies of the same shape, so a published + export was owed compatibility forever **and** the duplication it was named for + stayed. All three now call the hook. + + The migration is a pure deduplication with no behaviour delta — the hook was + extracted *from* these three shapes, so each becomes a one-line call. + `ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the + recipe the hook's own doc comment prescribes for it by name: pass the data + source as `undefined` for a render that must not read metadata + (`hasInlineData ? undefined : dataSource`), so "inline value data set" is + expressed as "there is no source to read from" rather than as a second enable + flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and + what made the calendar's obstacle a non-obstacle: it was about the gate half. + + **One observable change:** `ObjectKanban`'s rejected definition read now logs + on `console.error` with a `[useSettledSchema]` prefix instead of + `console.warn`. Its test spy moves with it, and now asserts on the channel + rather than merely silencing it. + + **The gantt's duplicate query is gated** (ask 2 of the card; #6482's + undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s + dependency list, so every load issued two unbounded queries — the first with no + `$expand` at all. Measured on this component across three latency profiles, the + cost is not the mild "round trip bought and thrown away": when the metadata + read is the slower of the two, which is the common case on a cold + `MetadataCache`, the user sees the full three-step paint — raw foreign-key ids, + back to the loading placeholder, then the expanded rows. It now issues one + query, already expanded. + + Gating the gantt required its schema resolution to settle on EVERY exit + (objectui#7232): the hand-rolled effect returned without settling on + `!effectiveDataSource`, on `!resource` and in its `catch` — harmless while + nothing waited on it, and a chart that never loads once something does. + `useSettledSchema` settles on all three by construction, which is what makes + the gate safe; both exits are pinned. + + ⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's + separate ruling, in its own commit on the same branch. +- 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). + + objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope + (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This + closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, + `DetailView`, and `ObjectDataTable` (which builds its own whitelist in + `computeLookupExpand` rather than calling `buildExpandFields`). + + **Three of them pass no column list at all**, which makes them the sharp ones: + `buildExpandFields` reads an absent column list as "no column restriction" and falls + back to **every declared relation on the object**, denied ones included. So a standalone + calendar, a gantt, and every record page in the console asked the server to resolve the + object's full relation set by default rather than by configuration. + + **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column + list is already FLS-filtered field by field, which is exactly the route PR #7229 measured + as unsound: an emptied column list reads as "no column restriction", so a detail view + whose authored fields are all denied had its `$expand` **widened** from the relations it + asked for to every relation the object declares. The principal who may read least was + asking for the most. + + **Reproduced before it was fixed**, as a failing test per site. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip, and the + client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, + which contains only the object's declared reference-bearing fields, so the "`checkField` + answers false for an undeclared key" trap cannot be reached and derived / host-joined + columns are untouched. An unanswered permission policy filters nothing. Neither + `buildExpandFields` nor `computeLookupExpand` is changed. +- e75f4c9: `colorField` now means the same thing in the gantt, the calendar and the timeline + (objectui#7243). + + **The inversion this fixes.** `gantt.colorField` is documented as "field that drives the + bar color", and the renderer passed the stored value straight into the bar's + `backgroundColor`. Pointing the key at a select field therefore emitted + `backgroundColor: "open"` — not a colour, so the browser dropped the declaration and + every bar rendered identically. OMITTING the key was strictly better: the absent-key + branch derived a real colour per status. Declaring the documented key was worse than not + declaring it, with no error, warning or console message either way. + + The same key also meant three different things across the three lenses: the timeline + resolved the field's authored option `color`, the calendar hashed the raw value onto a + fixed palette, and the gantt emitted the raw value. An author colouring three views by + one field got three unrelated results, one of which was no colour at all. + + **The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's + resolver, lifted so all three call it: + + 1. the field's own option `color` for the record's value; + 2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`, + `#rrggbbaa`, `rgb(...)`, `hsl(...)`); + 3. else each renderer's own last rung, which is deliberately NOT shared — the gantt + derives a semantic-token hex (a bar must be painted), the calendar keeps its + theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its + default marker. + + **What changes for authors.** A gantt or calendar whose `colorField` points at a select + field with authored option colours now paints those colours. A gantt value that is + neither an option colour nor a colour literal now derives a colour instead of emitting + an invalid CSS value — including a palette NAME (`red`), which now resolves to that + palette's hex, the behaviour the key's own contract has always promised ("hex or + semantic name") and the one `borderColorField` already had. `gantt.borderColorField` + takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps + today's behaviour otherwise and deliberately gains no derivation rung, since the stroke + is opt-in and deriving one for every record would draw an alert on records that have + none. + + Calendars whose `colorField` points at a plain categorical field are unchanged: that + value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline + is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar + already accepted. +- b8fc1e2: fix(plugin-calendar): the all-day lane header reads the same string with or without an I18nProvider + + `CalendarView`'s `DEFAULT_TRANSLATIONS` table — the `defaults` map behind its + `createSafeTranslation` factory — spelled `calendar.allDay` as `all-day`, while + all ten locale packs carry the key and `en` spells it `All Day`. Since + `createSafeTranslation` serves that table only when no `I18nProvider` is + mounted, the same lane header rendered `all-day` in a standalone embed and + `All Day` inside the console. The table now matches the pack, so both paths + render one string. + + The table entry was the only one of the seven that disagreed with its `en` + value, and the packs predate it by three months — the drift was an oversight, + not a compact spelling chosen for the 56px gutter (both strings are seven + characters wide). + + Also removes the dead `t('calendar.allDay') === 'calendar.allDay'` ternary that + guarded the lane header: the factory's provider-less arm returns `defaults[key]` + before it could ever return the bare key, and with a provider the merged + resources always carry `calendar.allDay`, so its lowercase branch was + unreachable from either side. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- 2ce2612: A record-page URL now names the object the clicked rows actually came from, in + `ObjectTree` and `ObjectCalendar` (objectui#7638). + + `useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed, + and both components handed it the bare top-level `schema.objectName` while resolving + their own rows through the objectui#6939 record-source ladder (`data`, then + `staticData`, then `objectName`). objectui#6939 published `objectName` as that ladder's + THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one + record source — and a row fetched through `data.object` whose click built + `/{schema.objectName}/record/{id}` named a record that the URL's own object does not + contain. + + Two shapes change, both toward the object the rows came from: + + - a block carrying **both** bindings navigated to the top-level key and now navigates to + `data.object`; + - a **data-only** block had no name to build a URL from at all, so the hook took its + `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed + record URL. + + `ObjectCalendar` is where the divergence was plainest: on one click it resolved the + detail drawer through the ladder and the navigation URL through the top-level key. The + URL now reuses the very `schemaObjectName` that already keys the calendar's record query + and its `$expand` derivation, so query, drawer and URL agree by construction. + + **Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName` + tail for the off-contract `data: { provider: 'object' }` that carries no `object` + (`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName` + already carries, and the same conservatism objectui#7627 applied when it published the + shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an + `objectName`, and only what callers hand it has changed. + + The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is + why there were copies to convert at all — component authors copied the divergence out of + the documentation, correctly, as written — so it now points at + `resolveRecordSourceObjectName` and says explicitly that a caller with no data config + has nothing above rung three and should keep passing `schema.objectName`. + + `ObjectKanban` is deliberately **not** converted: it has no data config, no + `getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so + `schema.objectName` already IS its record source and its board, drawer and URL already + agree. +- 2d80456: `object-kanban` / `view:kanban` and `object-calendar` / `view:calendar` now DECLARE seven + spec-carried keys their renderers already honoured, so the html tier stops reporting working + metadata as `unknown-prop` (objectui#8201, the backlog objectui#8176 exposed). + + Board: `groupBy`, `cardTitle`, `titleField`, `swimlaneField`, `coverImageField`. + Calendar: `defaultView` (the spec's three-member enum) and `locale`. + + No renderer behaviour changes — every one of these keys was already read and already acted on; + what changes is that authoring tools can now discover them and the save gate agrees with the + validator. Both tags of each block now spread ONE shared `inputs` list, so the two published + surfaces cannot drift apart by hand-copy. +- 76ae729: `README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema` + that does not exist. Measured against the interface itself + (`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's + only required key besides `type` — was published as `events?`, so a reader + following the README omits it and TypeScript rejects the node; `defaultDate` was + `string` where the schema says `string | Date`; and `onDateClick` was listed as a + schema key when it is a `CalendarViewProps` **component** prop, sending readers + to a different package's surface for a key `calendar-view` does not have (the + schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys + with nothing saying it was a summary (objectui#5045). + + The block now carries the requiredness the schema declares, names itself a + partial summary of `CalendarViewSchema`, and adds the author-facing + `defaultView` / `view` / `views` / `editable` / `date`. It also states plainly + what the registered `calendar-view` renderer actually reads — it builds events + from the node's `data` array and drops an authored `events` key (objectui#4433) — + so the corrected requiredness does not itself become a new wrong instruction. + + This is a documentation fix to a file `plugin-calendar` publishes to npm, which + is why it carries a version: the npm landing page only picks up the correction + on a release. No behaviour, export, type, or `dist` byte changes. The pin test + added alongside it publishes nothing. +- bc5870c: fix(plugin-calendar): a record with no date is no longer placed on today + + `ObjectCalendar` mapped a record whose declared `startDateField` carried no + value to `new Date()` — the current moment — so it rendered on today's cell as + an ordinary event, indistinguishable from a real one. The `isNaN` guard six + lines below could not catch it by construction: a no-argument `new Date()` is + always valid, so the absent-value case became a well-formed lie *before* the + check that would have caught it. + + The fabricating arm is deleted. Such records now leave the grid entirely and + appear in a collapsed "Unscheduled (N)" area below the calendar — a visible + count and an expandable list, with no invented date and no scheduling UI. The + `isNaN` filter keeps its original job for values that are present but + unparseable: absent and malformed stay two distinguishable outcomes. + `allDay: !endDate` now applies only to records that have a start, so a record + with no dates at all is unscheduled rather than silently all-day; a record with + a start and no end still renders all-day exactly as before. + + Adds `calendar.unscheduled` to all ten locale packs. +- f47d94c: `@object-ui/plugin-calendar` now exports the `CalendarView` component's runtime event + type as **`CalendarViewEvent`**, and keeps `CalendarEvent` as a **`@deprecated` alias** + of it. **Non-breaking:** the alias is a working re-export denoting the same type, so + code importing `CalendarEvent` from this package keeps compiling unchanged — nothing is + removed and no behaviour changes. + + Why: `@object-ui/types` exports its own `CalendarEvent`, the AUTHORING event + (`id: string`, `start` / `end` accept ISO strings with `end` required, plus + `description`), while this package's was the runtime event (`id: string | number`, + `start: Date`, `end?: Date`). Neither is assignable to the other, and IDE auto-import + chose between the two identical names essentially at random — the wrong pick surfaced as + a remote `TS2322` about `Date` rather than as a wrong import, which is how this package's + own README example stayed uncompilable through an earlier import-path fix. The authoring + type keeps the canonical `CalendarEvent` name; the runtime type gets the self-describing + one (objectui#5044, following the `ObjectCalendarProps` -> `ObjectCalendarComponentProps` + rename in objectui#4650). + + Write `CalendarViewEvent` in new code. +- 065bba7: `CalendarViewSchema` (TS interface and zod mirror) converges on the registered + `calendar-view` renderer's measured read set (objectui#5667, maintainer ruling + option A — the renderer is authoritative). + + **Breaking for consumers of the published type** (deliberate; per-repo policy + breaking changes ship as `minor` — the fixed group's `major` tracks + `@objectstack`): + + - Nine inert keys are retired: `events` (the interface's only required key, + which the renderer deliberately drops — objectui#4433), `defaultView`, + `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + `onEventUpdate`, `onDateChange`. None had a read site on the authored-node + path and no measured app authors them (ADR-0049 enforce-or-remove). + - The type now declares what the renderer actually reads: `data`, `titleField`, + `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, + `currentDate`, `allowCreate`, `className`, plus the two host-only function + hatches it forwards (`onEventClick`, `onViewChange`). + - Practical radius, measured: `BaseSchema` carries an index signature and the + zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys + neither fail to compile nor get rejected at validation — they are simply no + longer declared, documented, or type-checked. The material accept change is + that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` + node without it now validates (previously the one key validation demanded + was the one key guaranteed to do nothing). + + Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README + and `content/docs/api/schema-reference.md` are repaired to the converged + surface in the same change, so no copy of the old contradiction survives. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [f66072d] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [1117414] +- Updated dependencies [6d63cd0] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [7c96c94] +- Updated dependencies [4da5109] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [63d54dd] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [b4393e5] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [d3005f7] +- Updated dependencies [1e7fe0a] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3777538] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [9409eb9] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [eeb6c2f] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [854cba3] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [6411def] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [b61d7d8] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [fe8f451] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [d06fba8] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [0b12a33] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [6c1b105] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [17fbbaf] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0d1e702] +- Updated dependencies [b03ba3a] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-calendar/package.json b/packages/plugin-calendar/package.json index 118b0d3358..5a73b1ce20 100644 --- a/packages/plugin-calendar/package.json +++ b/packages/plugin-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-calendar", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Calendar view plugins for Object UI - includes both ObjectQL-integrated and standalone calendar components", diff --git a/packages/plugin-charts/CHANGELOG.md b/packages/plugin-charts/CHANGELOG.md index f8807b8dad..6144ac1a01 100644 --- a/packages/plugin-charts/CHANGELOG.md +++ b/packages/plugin-charts/CHANGELOG.md @@ -1,5 +1,1131 @@ # @object-ui/plugin-charts +## 17.7.0 + +### Minor Changes + +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 0349555: A sankey with no positive flow says so, instead of rendering an empty div + (objectui#7140). + + `AdvancedChartImpl`'s sankey arm keeps only strictly positive measures, so a + chart handed **real rows** whose measure is all `0`, all `null`, all negative, + or unparseable built no links and returned a bare `<div>`. Measured in Chromium + against a populated control: the control drew 1 `<svg>` / 7 `<path>` / + 26 descendants; each of those four tiles rendered `descendantCount: 1`, + `svgCount: 0`, `textContent: ''`, and their screenshots hashed identical to one + another. No marks, no text, no `role` — a tile indistinguishable from a widget + that had crashed, which is the one distinction the file's other refusals exist + to make. + + It now renders through the `ChartRefusal` shell those refusals already use — + same box, same `role="status"`, and a new `data-chart-error="no-positive-flow"` + — reading *"This chart has no flow to draw: no row's `<measure>` is above + zero."* + + Two boundaries are deliberate and pinned: + + - **No rows at all is untouched.** That is the empty-result question, answered + upstream in `ObjectChart` where the query outcome is known; a sentence about + what the rows contain would be false about a dataset with no rows in it. + - **One positive row among zeros still draws.** The refusal fires on an empty + link set, never on a thin one. + + One code and one sentence for three causes (a genuinely all-zero flow, values a + flow cannot represent because they are negative, and measures `Number(…) || 0` + folds to zero): naming any single cause would be false for the other two, so + the copy names the predicate the filter actually applies, which is true for all + three. No recovery is promised. Every other chart family is byte-identical — + eight of the twelve tiles in the browser sweep hashed unchanged. +- 0dc2c93: `compareTo` on a `scatter` chart is no longer supported — scatter joins pie / donut / + funnel on the list of chart families that ignore it (objectui#7402, maintainer ruling + 2026-09-03). + + **This removes a published capability, deliberately.** Until now a `chartType: 'scatter'` + chart (and the dashboard widget types `scatter` and `bubble`, which both render as one) + with `compareTo` set synthesised a muted "previous period" overlay series. It drew the + wrong picture: a scatter binds ONE measure, and the renderer reads y through the single + `YAxis dataKey={series[0].dataKey}`, so the overlay was plotted on the PRIMARY series' y + — "previous period" painted exactly on top of "current" (objectui#7194). + + Enforce-or-remove: rather than keep drawing that, the capability is removed until it can + be drawn honestly. Drawing a real second measure on a scatter needs the multi-measure + projection recorded as option A of objectui#7194, which is not built (zero authored + callers). **If and when that projection lands, `compareTo` on a scatter returns with + it** — it is the same missing mechanism, one payment. + + What changes for authors: + + - A `compareTo` on a scatter is now IGNORED rather than drawn. The primary series still + renders exactly as before — nothing refuses, nothing goes blank, and no comparison + query is issued on the inline chart path. + - No `<measure>__comparison` (inline chart) / `<measure>__compare` (dashboard) series is + appended for a scatter, so a compare-to scatter document also never reaches the + two-or-more-series scatter refusal being added under objectui#7194. + - Charts that keep the overlay: line, area, bar, horizontal-bar, combo. Charts that + ignore `compareTo`: pie, donut, funnel and — as of this change — scatter (and the + `bubble` widget type that renders as a scatter). + + Reachability at the time of the change: **0** authored scatter/bubble instances in-repo + across both spellings (control `"type": "bar"` fires at 5 example files); incidence in + deployed tenant metadata is not measurable from this repo. +- 01c27c4: Fix: a chart series' `type` override (`ChartDataSeries.type`, objectui#6121) is now + honoured when the series array is written in the internal `dataKey` binding, not only + the `name` binding (objectui#7681) — the same sentence #2945 shipped for the other + dialect. + + `ChartRenderer`'s `isInternalShaped` fast path (introduced by #2945 to fix a different + bug — a `name`-shaped `series` shadowing the normalized `dataKey`-shaped one) took the + raw authored array untouched whenever every entry already carried `dataKey`, bypassing + `normalizeSeries` — the only place `type` is translated to the renderer-internal + `chartType`. So an author who wrote `series: [{ dataKey: 'revenue' }, { dataKey: + 'margin', type: 'line' }]` — both keys independently valid on `ChartDataSeriesSchema` — + got neither the override nor a combo chart, silently. + + `ChartRenderer` now always takes the series array through the one normalization layer + (objectui#2880 S1) instead of special-casing the `dataKey` shape around it. + + **Breaking on unmodified documents, deliberately — this changes rendered output.** A + chart authored with a `dataKey`-shaped series carrying a `type` override used to render + one family for every series; it now renders the mix the author actually described (the + card's own regression case: two bars become one bar and one line). + + **Two more effects on the delivered key set for a `dataKey`-shaped series array**, + undisclosed until now — `normalizeSeries` is a no-op on a well-formed entry, but these + two cases were never well-formed under the old fast path either: + + - An i18n `label` written as a `{ en, zh-CN, … }` record on a `dataKey`-shaped entry was + previously forwarded as that raw object; it is now resolved to a plain string (the + first string-valued limb), matching what a `name`-shaped series already got from + `normalizeChartSchema`. + - A `dataKey`-shaped entry whose `dataKey` does not resolve to a non-empty string (and + has no `name` to fall back to) was previously forwarded as-is; it is now dropped from + the delivered series array, matching what a `name`-shaped series with no usable key + already got. + + No existing well-formed internal caller (`DashboardRenderer`, `ObjectView`, the dataset + path) changes behaviour — every series entry those callers construct already carries a + non-empty string `dataKey` and a string `label`. +- 967e5d8: Chart `series[].opacity` and `series[].dashArray` are honoured on every series, + not only on a `variant: 'comparison'` one (objectui#7698). + + `@objectstack/spec` declares `ChartSeries.opacity` ("Override series opacity") + and `ChartSeries.dashArray` ("Override stroke dash pattern") as unconditional + per-series overrides, and `normalizeSeries` read both off every series. The + renderer then honoured them on a comparison overlay only, so an author who + wrote `{ name: 'cost', opacity: 0.6 }` or `{ name: 'cost', dashArray: '4 4' }` + on a primary series got a mark drawn exactly as if the key were absent. Fixed + in the renderer rather than by narrowing the published declaration to match: + the spec is the contract of record, and a renderer's partial implementation + does not get to dictate it (AGENTS.md #0.1). + + **Two gaps, not one.** The `variant` guard was the visible half — `comparisonStyle` + returned `null` for any other variant. The second half only showed on + `dashArray`: that helper already returned an AUTHORED dash for every family + (the `??` takes the left side whatever the kind), and the **Bar and Scatter + marks** then passed `fillOpacity` only, dropping `strokeDasharray` and + `strokeOpacity` on the floor — so an authored dash was lost on those two + families even on a comparison series. A fix aimed at the guard alone would have + left that untouched. `comparisonStyle` is now `seriesStyle`, and the Bar and + Scatter marks pass all three channels. + + **Comparison series are unaffected.** The authored branch already won over the + muted defaults, and those defaults stay gated on `variant: 'comparison'`: a + comparison series carrying neither key keeps its lower opacity and its `'4 4'` + line/area dash exactly as before. The two stroke defaults no mark ever consumed + (bar and scatter — neither is stroked by this renderer) are now spelled + `undefined`, so opening `strokeOpacity` on those marks does not hand them a + default they never had. + + Only a stroked mark can show a dash, so on a `bar` or `scatter` mark an + authored `dashArray` reaches the mark and paints nothing — the mark's geometry, + not a condition on the key. The `ChartDataSeries` mirror docs and the + plugin-charts reference, which stated the comparison-only condition as an + interim measure, are corrected in the same change. +- 0758bd8: `ObjectChart` refuses an object-bound chart that declares no category axis (objectui#8168). + + An object-bound chart that named no field to group by used to be composed anyway. + `runAggregate` passed `schema.aggregate` to `ds.aggregate(objectName, { field, + function, groupBy, filter })` with no guard on `groupBy`, and the `ds.find` leg + handed the same bag to `aggregateRecords`, which buckets every record on + `record[groupBy] ?? 'Unknown'`. So one path asked a driver to group by `undefined` + and the other collapsed the whole object into a single `'Unknown'` bar — and which + of those a reader saw was decided by the data source, not by the renderer. The only + loud states this component had were a fetch `error` (`chart-error`) and a generic + "No data yet"; neither is a statement about an absent binding. + + `ObjectCalendar`, `ObjectGantt` and `ObjectTimeline` each already refuse a view that + declares no axis. This is the fourth, following `ObjectTimeline`'s shape + (objectui#7459): a `role="alert"` box, `data-testid="chart-missing-category-axis"`, + naming the bindings the author can declare — `aggregate.groupBy`, `xAxisKey`, + `xAxis.field` — rendered from the resolver's own vocabulary so the message cannot + drift from what the resolver reads. + + **Breaking, deliberately — and this repo ships breaking as `minor`.** A chart that + previously rendered an `'Unknown'`-bucketed bar (or whatever the driver did with + `groupBy: undefined`) now renders the refusal instead. That is the intent: the + picture it drew was not a picture of the data. + + It keys on the CATEGORY alone. A measure may legitimately be absent — `count` takes + no field — so refusing on an absent measure would refuse `count` grouped by a + declared category, a chart that renders correctly. Four shapes are deliberately + untouched: an ADR-0021 `dataset` chart (which may declare no dimension), a chart + carrying authored `data` or a `bind` scope (no field name is read to fetch those + rows), a spec-shape `xAxis: { field }` with no `xAxisKey` (resolved through + `normalizeChartSchema`, this package's one translation of the author-facing shape), + and every schema the five in-repo producers compose today — all five floor their own + category, so none of them can reach the refusal. + + This does **not** retire the six `'name'` / `'value'` floors at the three relay faces; + that is the remainder of objectui#7547 and is mechanical only once this screen exists. + + New key `chart.unconfigured.noCategoryAxis` in all ten locale packs. +- 6f017e9: Dashboard chart widgets no longer render as a blank area when their height class + resolves to `auto`. + + `ChartContainer`'s min-size fallback was applied only to the wrapper `div`. + Recharts measures its own `width:100%;height:100%` size-detector element, and a + percentage height never resolves against an ancestor's `min-height`, so the + wrapper obediently grew to 280px while the measured element stayed at 0 — and + Recharts renders no children at all for a non-positive box. The result was a + widget card with its title over an empty chart area: no marks, no refusal, no + empty state, and permanent, because a box that never changes fires no resize. + The floor is now applied to the measured element as well, under the same + condition, so an author's explicit height still wins. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 40c4711: A sankey that drew only SOME of its rows now says how many (objectui#7148). + + The sankey arm keeps strictly positive measures + (`data.filter((r) => (Number(r?.[dataKey]) || 0) > 0)`), so a mixed dataset + drew a normal, healthy, confident chart of a fraction of itself and nothing + anywhere recorded that the other rows existed. Measured in Chromium across 27 + tiles: `[{New business: 40}, {Refunds: -25}, {Chargebacks: -12}]` rendered + `svg: 1`, `path: 3`, 18 descendants, no `role`, no text, and — against a live + console control that did fire on the same instrument — zero console output. + Its screenshot hashed byte-identical to five other datasets, one of which + genuinely had a single row. Six datasets, one image: a reader had no bit of + information separating a complete flow from a third of one. + + The discard itself stands — a flow has no negative width, so it is the only + thing that arm can do with those rows. What is added is a footnote under the + plot naming the ratio and the predicate the filter applies: + + > Showing 1 of 3 rows — 2 rows have no `amount` above zero, which a flow + > cannot draw. + + It names the predicate rather than a cause because `Number(…) || 0` folds + negatives, zeros, `null`, unparseable strings and a missing key into one + discard, and all five were measured reaching this branch beside a survivor; + naming any one of them is a sentence that is false for the other four. + + A complete flow is byte-for-byte unchanged and gains no wrapper element, and a + drawable sankey is never replaced by prose: the `no-positive-flow` refusal + still owns the case where NOTHING survives the filter, and the "one positive + among zeros still draws" boundary still draws — that fixture is itself a + thinned dataset, so it now draws *and* says so. +- e8c553b: A scatter handed more than one series now refuses instead of drawing a false picture. + + Scatter binds one measure: `series[0].dataKey` is the y axis, and every series was + handed the same rows through that one axis. A second series therefore added a + colour and a legend entry and nothing else — measured, two series over two rows + painted four symbols at two positions, each drawn twice, and the second measure's + values appeared nowhere on the plot. The data was valid and the picture was + confidently wrong, which no existing refusal could see. + + A `chartType: 'scatter'` with two or more `series` now renders the renderer's + refusal shell under `data-chart-error="scatter-multi-series"`, stating that a + scatter plots one measure, naming the fix (keep exactly one series) and listing + the series keys it was handed. A single-series scatter is unchanged. + + This refusal counts authored `series` only. `compareTo` on scatter is out of + its scope: objectui#7402 ruled (b) that scatter joins pie / donut / funnel in + excluding `compareTo` — `supportsCompareTo` and the dashboard widget path stop + synthesising a comparison series for it, so no `…__comparison` overlay is ever + built for a scatter and this guard is never reached by a compare-to document. + That exclusion ships as a separate change; until it lands, a `compareTo` + document still reaches the renderer as two series and refuses here today. + + No multi-measure projection is built (maintainer ruling, 2026-09-02): nothing + in-repo authors a two-series scatter, so that capability waits for a real caller. + The refusal copy is `chart.scatterOneMeasure` in all ten locale packs. +- bb459ea: Name the scatter legend's series, so its swatch stops reading as a stray data point + (objectui#7248). + + The Chart Gallery scatter ("Estimate vs Progress") appeared to draw a seventh point + below the x-axis, outside the plot area. It was not a point. `ChartLegendContent` + resolves a label as `config[nameKey || item.dataKey || 'value']`, and a `<Scatter>` + carries **no `dataKey`** — scatter's keys live on the XAxis/YAxis, not on the mark — so + the key collapsed to the literal string `'value'`, missed a config keyed by measure + name, and the legend entry rendered its colour swatch with no text beside it. An 8x8 + square in `--chart-1`, the same colour as the marks, sitting under the x-axis. + + Measured on the running showcase in real Chromium: the swatch sat at cy 341 against a + plot area ending at cy 295, on a y scale of 4.835 px per unit — y = -9.5, at x ≈ 45. + That is the "x≈40, y≈-10" the report described, to the pixel, and all six real marks + were inside the plot area at every viewport width swept from 1440 down to 480. + + **The y domain was not the defect and is unchanged.** Clamping it — the fix the report + asked for — would have created the bug it described: mixed-sign and all-negative + fixtures are pinned here drawing every mark, because recharts already extends the + domain to cover negative values. + + Two changes. The scatter now passes `nameKey` so its legend resolves the measure's + label, and `ChartLegendContent` falls back to the series `name` recharts itself put on + the legend item when the config lookup misses. The second closes the class rather than + this one instance: the swatch renders unconditionally, so a config miss must never + leave it anonymous. Charts whose config already resolves are unaffected — only a + currently-empty label changes. +- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts + (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer + ruling B, 2026-09-02). + + A dataset-bound chart's aggregate axis / legend title read the analytics + service's hard-coded English `Count` on a zh console whose category labels were + already Chinese. The renderer was passing `fields[].label` through verbatim — + correctly, for an author-declared measure (objectui#4106) — and had no way to + tell the server's built-in default apart from an author's label. + + The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural + discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | + 'count_distinct'`, populated only on the server-side built-in defaults + (objectstack#14492). This change is the consumer side of that contract: + + - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — + `ChartResultField` plus the optional `builtinAggregate` carrier + (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than + on it because the spec this release is built against does not carry the key + yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / + `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries + the locale strings in (core stays React-free and i18n-free — the same + division as `nullCategoryLabel`). A field carrying a recognised + discriminator resolves through that map; every other field keeps its wire + `label` verbatim — never by matching the label's text or the field's name + (the rejected option A). + - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings + through the existing `report.aggregate.*` keys (zh already carried 计数 / + 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). + - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), + `plugin-report` (`DatasetReportRenderer`): pass the resolved map to + `buildChartSeries`. + + Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same + chart titled `计数`; an `en` session still reads `Count`; an author-labelled + measure (`Tasks`) and a measure literally named `count` without the + discriminator are byte-for-byte unchanged. Until the upstream field is + populated the wire carries no discriminator and every chart renders exactly as + before. +- ed4a2f1: fix(plugin-charts): `pie-chart`, `donut-chart`, `radar-chart` and `scatter-chart` render as the family they name + + A schema written as `type: 'pie-chart'` (or `plugin-charts:pie-chart`, and likewise donut / radar / scatter) drew a **bar chart**. The four registrations declared their family as `defaultProps: { chartType: … }`, and nothing on the SDUI path has ever read a registration's `defaultProps` — so `ChartRenderer` resolved no family and `AdvancedChartImpl` fell to its `'bar'` default. Valid data, a confidently wrong picture, and no `data-chart-error` that could fire. + + `ChartRenderer` now derives the family from the schema's own `type`, through `normalizeChartSchema` — the package's single translation point, so the exported `normalizeChartSchema` answers what the runtime actually draws. An explicit `chartType` still wins, so `plugin-charts:chart` with `chartType: 'scatter'` is unchanged. + + The five inert `defaultProps: { chartType: … }` are removed with it rather than left beside a mechanism that works. Registration `defaultProps` remains unread on the SDUI path repo-wide; activating it generally is a separate, wider change and is not this one. + + ⚠️ `scatter-chart` now genuinely reaches the scatter arm, so a two-series `scatter-chart` now renders the `scatter-multi-series` refusal it was always supposed to. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- fc32921: Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing + (objectui#8266). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `field` — the normal way to author "how many records per status" — rendered an + empty chart. No error, no empty state: a plot frame with the category ticks drawn and + not one mark in it, which reads exactly like "this object has no rows yet". + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + built the series binding as `aggregate?.field || (options.yField || 'value')`, which for + a fieldless count resolves to `'value'`. The rows an object-bound fieldless count + returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned + since framework#3701. A `dataKey` naming a column no row carries plots nothing, and + neither of the renderer's two guards fires on it: the rows DO carry the category key, + and the series array is not empty. + + **Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to + `chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the value column an object-bound aggregate produces". Both relays now consult it, and + the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed + through the same function, so the two halves of the question cannot drift again. + + **What moves on screen.** A chart that was blank now draws. Charts that already drew are + unaffected: a field-bearing aggregate resolves to its raw field under both the old and + the new reading, and a chart with no `aggregate` at all keeps the author's `yField`. + One authored key changes meaning: a `yField` written on an object-bound chart that + ALSO declares an aggregate no longer wins over the aggregate's own column — it named a + record column that a grouped aggregate never returns, so it plotted nothing before. + + **Not fixed here, and out of scope.** The same widget with no `options.xField` is + refused by the category-axis guard naming `name`, a key the author never wrote (they + wrote `aggregate.groupBy`). That is the category half of the same relay gap and is + filed separately. +- 8f9d87a: Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being + refused for lacking a `name` column (objectui#8269). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `options.xField` rendered a refusal instead of a chart: + + > This chart cannot plot its category axis: no row has a `name` field. + + The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that + had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent + them to debug the wrong layer. + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + floored the category binding on a literal — `options.xField || 'name'` — and handed it to + the `object-chart` node without ever consulting the aggregate that decides it. An + object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no + row carried `name` and the category-axis guard (framework#4033) fired correctly on a + binding that was already wrong when it arrived. + + **Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to + `chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the category column an object-bound aggregate produces", and the published sibling of the + `chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now + consult it for the object-provider branch. + + **What moves on screen.** A widget that rendered a refusal now draws. Measured through + `ChartRenderer` at 480x320 over the rows a fieldless count returns + (`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from + `xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`, + 1 series and 2 marks with the category ticks drawn. + + **Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows + are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it + returns a single row with no category column), and the authored-literal-rows branch — the + `chart` node composed after the object-provider check fails — keeps its floor unchanged. + One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField` + written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the + aggregate's own column — it named a record column a grouped aggregate never returns, so it + produced the same refusal before. +- 894d103: `ObjectChart`'s wrapper div now carries `h-full`, keeping the height chain intact from a dashboard grid cell's declared height down to the element Recharts measures. Previously the chain died at the plain auto-height wrapper: `height: 100%` on the chart container computed to `auto`, Recharts measured a permanent zero, and only the `CHART_MIN_HEIGHT` floor (#5503) kept dashboard charts visible — at a fixed floor height instead of filling the cell (#5451). Under auto-height parents `h-full` resolves to `auto`, so non-dashboard hosts are unchanged. +- 5eddeeb: Pie, donut, funnel and treemap now say when rows carry no magnitude they can draw. + + These four families size a mark BY its measure, so a row whose value is zero, + negative, `null` or unparseable stays in the data and is given no area. Measured + in Chromium across 74 tiles: an all-zero pie put ZERO non-white pixels on the + page while its DOM carried 31 descendants and a real `svg`; a treemap handed + `40 / null`, `40 / 0` or `40 / -25 / -12` rendered one full-bleed leaf that was + byte-identical to a genuinely one-row treemap; and a funnel handed `40` beside a + `null` drew no segments at all and labelled the tile with the row that had no + value. + + When no row can be sized, these charts now render the file's refusal shell + (`no-positive-magnitude`) instead of a blank tile. When only some rows can be + sized, the chart still draws and carries a note counting the ones it could not. + All-positive charts, charts handed no rows at all, bar charts, and both sankey + answers are unchanged. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- d6fe1e1: Draw every categorical x-axis label on short axes + + A vertical bar chart in a dashboard-width widget dropped most of its x-axis + labels — three bars drew one label, five bars drew two — leaving the bars + unnamed, with no legend to fall back on because a single-series bar chart has + none. + + The x axis applied one tick policy to time and category alike (`preserveStartEnd` + with a 48px `minTickGap`), which is right for hundreds of dates and wrong for a + band axis, where a dropped tick is an identity the reader cannot recover rather + than a sample they can interpolate. It was also keyed to the viewport rather + than the widget, so a 200px chart inside an 800px console was treated as a wide + one. + + Bar, column, line, area and combo charts now draw every label on a categorical + x axis of five buckets or fewer — rotating, and ellipsising an over-long name + rather than clipping it. Longer axes keep the existing measured thinning, and + horizontal bars are unchanged. +- 6c5ee71: `ObjectChart` now depends on the `fieldOptionLabel` resolver directly instead of + holding it behind a ref, so a chart re-resolves its groupBy option labels when + the resolver genuinely changes (objectui#5587). + + The ref existed for a reason that no longer holds. `useSafeFieldLabel()` returned + a fresh object on every render outside an i18next provider, so a direct + dependency made `fetchData`'s `useCallback` identity fresh on every render, and + the effect that depends on `fetchData` refetched on every render — an unbounded + loop. `ObjectChart` worked around that locally with `fieldOptionLabelRef` plus a + `useEffect` keeping it current. `useObjectLabel`'s memo now holds with or without + an i18next instance bound (objectui#5564), so the resolver's identity is stable + on both paths and the indirection buys nothing. + + It did cost something, and that is the user-visible half: a ref-hidden dependency + meant `fetchData` did NOT re-run when the resolver changed. A chart mounted + before its `I18nProvider`, or rendered across a language switch, kept serving + groupBy labels resolved by the old resolver until some unrelated dependency + (object name, filter, aggregate) happened to move. It now refetches once on that + transition and shows labels in the active language. + + Pinned by `ObjectChart.fieldOptionLabelRefetch.test.tsx`, which counts fetches + across forced re-renders both outside and inside a provider. Reverting + `useObjectLabel.ts` to its pre-objectui#5564 state turns the no-provider case red + (2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so + the removal is pinned to the fix that unlocked it rather than to a comment. +- 93bbc20: Scatter now says when it cannot place a row, instead of drawing an empty axis. + + Scatter is the only two-measure positional chart in the renderer: `xAxisKey` feeds + a numeric X axis and `series[0]` a numeric Y axis, so a point exists only when + both are numbers. Measured in real Chromium, rows it could not place produced a + tile byte-identical to a scatter handed no rows at all, and six different + authoring failures shared one image. A chart with one placeable row among three + was 99.75% pixel-identical to a genuinely one-row scatter. + + Handed rows it cannot place any of, a scatter now renders the file's refusal + shell under `data-chart-error="no-plottable-points"`, naming both keys. When some + rows place and some do not it draws as before with a `data-chart-note="unplotted-points"` + footnote carrying the count. Charts whose rows all place are byte-identical to + before, and no wrapper element is added to them. + + The predicate is positional, not magnitude-based: zero and negative coordinates + are ordinary scatter data and keep drawing. +- dd35800: `ObjectChart` now renders a self-describing empty state when its query succeeds + and returns no rows, instead of falling through to a bare chart frame. + + The frame was measured in a browser rather than assumed: recharts derives its + ticks from the data, so with an empty result the bar and line families emit two + hairline axis rules and no `text` nodes at all, and pie/donut emit nothing — + there are no labelled axes to tell the reader what would have been plotted. + Beside the component's own red "Failed to load chart data" box, a blank tile + gives the reader nothing to distinguish a young chart from a broken one. + + The copy is the one `plugin-dashboard` already shows on the dataset-bound path + ("No data yet" / the load succeeded / the source name), so the same chart over + the same empty result no longer reads two different ways depending on which + widget drew it. Charts with inline authored data are unchanged — they ran no + query to report on. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-charts/package.json b/packages/plugin-charts/package.json index 8a6eabeddd..5cef13dfda 100644 --- a/packages/plugin-charts/package.json +++ b/packages/plugin-charts/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-charts", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Chart components plugin for Object UI, powered by Recharts", diff --git a/packages/plugin-chatbot/CHANGELOG.md b/packages/plugin-chatbot/CHANGELOG.md index 525453d4d6..8250006c18 100644 --- a/packages/plugin-chatbot/CHANGELOG.md +++ b/packages/plugin-chatbot/CHANGELOG.md @@ -1,5 +1,1133 @@ # @object-ui/plugin-chatbot +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- 52a43de: `ChatbotSchema` names the `chatbot` node's local-display and legacy + auto-response keys — a new, additive published surface (objectui#6169, the + #6172 family ruling: every component node has exactly one named, importable + authoring-face type). + + `ChatbotSchema` (`@object-ui/types`) now declares ten keys that previously + existed ONLY inside an anonymous inline intersection local to + `packages/plugin-chatbot/src/renderer.tsx`'s `chatbot` registration, invisible + to anything outside that one file: + + - `showTimestamp`, `userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, + `assistantAvatarFallback`, `maxHeight` — display fields. + - `autoResponse`, `autoResponseText`, `autoResponseDelay` — the local + auto-response (demo/playground) fields, already live via a real consumer + (`packages/app-shell/src/console/ai/AiChatPage.tsx`). + - `onSend?: (content: string, messages: ChatMessage[]) => void` — the + send-callback, now typed against the published `ChatMessage` shape rather + than the plugin's internal runtime message type. + + Each was read-site-censused before being declared (renderer.tsx and/or + `useObjectChat.ts` reads every one); none were dead, so none took the + ADR-0049 retirement route. `disabled` — also present in the original + intersection — is NOT redeclared: it is already `BaseSchema.disabled` + (`boolean | string`), read generically for every node type, and redeclaring + it here would have narrowed away the inherited expression-string case. + + **What an external consumer can now do that they could not before:** import + `ChatbotSchema` from `@object-ui/types` and get these ten keys with real, + checked types — previously any reference to them required either duplicating + the anonymous type by hand or falling back to `any`. The Zod mirror + (`@object-ui/types/zod`) gained the same ten keys in lockstep, so a `chatbot` + node parsed through it is now validated on these keys rather than silently + passed through unchecked (`BaseSchema`'s Zod mirror is `.passthrough()`). + + `packages/plugin-chatbot`'s `chatbot` registration (`renderer.tsx`) now types + its `schema` prop as `ChatbotSchema` directly, dropping the anonymous + intersection. No behavior change: `renderer.tsx:87`'s + `body: schema.requestBody` forwarding — the subject of the already-merged + #6193 — is untouched, and the render function reads the exact same keys it + already read. + + This is additive (new optional keys on an interface that already carried a + `[key: string]: any` index signature, and a new Zod-validated subset of + previously-passthrough keys), so it ships as `minor` even though it changes + published type surface: objectui's major is pinned to `@objectstack`'s + (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes + ship as `minor` with the break spelled out — there is no break here to spell + out, only a widening from anonymous-and-unchecked to named-and-validated. + + Out of scope, deliberately: the `chatbot-enhanced` and `chatbot-floating` + registrations' own anonymous intersections (different key sets, a decision + for a separate card in the same family), and the `surface` row on + `content/docs/plugins/plugin-chatbot.mdx`'s Properties table, which names a + key no registration in this package currently reads (filed separately). +- 7d8e546: `surface` becomes authorable on the `chatbot-enhanced` node, so the capability the docs + have been documenting is one an author can actually reach (objectui#6687, maintainer + ruling 2026-08-29). + + `content/docs/plugins/plugin-chatbot.mdx`'s `Properties` table listed `surface` + (`'card' | 'plain'`, "bordered panel or a frameless full-page workspace"), but the key had + **zero read points**: none of the three `ComponentRegistry.register('chatbot*', ...)` sites + in `renderer.tsx` forwarded it, and `ChatbotSchema` did not declare it. `surface` was real + only as a prop of the React component — `ChatbotEnhanced.tsx` defines `ChatbotSurface`, + defaults it to `'card'`, and branches six layout decisions off `isPlainSurface` — so it was + reachable by a hand-written React host and by nobody writing metadata. An author who wrote + `surface: 'plain'` got the `'card'` default, with no error and no signal. + + Measured on both declaration faces before the fix, each with a control that had to hit: + `schema.surface` appeared 0 times in `renderer.tsx` against `schema.placeholder` at 3 (one + per registration) and `schema.processVisibility` at 1; and `ChatbotSchema` + (`packages/types/src/complex.ts`) declared 34 keys, not this one. Two faces agreeing is + what made the zero a reading rather than a bad query. + + The ruling adopted **wiring it** over deleting the row — the row names a real, shipped + capability, and hiding it back inside the component would withdraw it from authors. It also + matches this page's two existing resolutions of the same defect class, neither of which + deleted a row: `requestBody` (objectui#6193) kept its row and documented the seam, and + `maxToolRoundtrips` (objectui#5605) kept its row, marked it inert, and warns once at runtime. + + - `chatbot-enhanced` declares `surface?: ChatbotSurface` on its inline schema-extension + type and forwards `schema.surface` to `<ChatbotEnhanced>`. The union is **imported** from + `ChatbotEnhanced.tsx` rather than re-spelled, so there is one contract rather than two + dialects that can drift (AGENTS.md #0.1). + - The key joins the registration's `inputs` (designer + autocomplete surface) with + `defaultValue: 'card'`, and deliberately **not** its `defaultProps` — mirroring + `processVisibility`, so nothing materializes the key onto new nodes. + - **The absent case is unchanged**: an unauthored `surface` is forwarded as `undefined`, so + `<ChatbotEnhanced>`'s own `surface = 'card'` default still applies. This is pinned as + hard as the authored direction, because it is what a careless + `schema.surface ?? 'plain'` or a `defaultProps` entry would silently regress for every + existing document. + - `chatbot` and `chatbot-floating` do **not** gain the key: they render `<Chatbot>` and + `<FloatingChatbot>`, which have no such chrome to switch. The docs row is therefore + scoped to say the key applies to the enhanced registration — the table never again claims + more than the registrations deliver. + + `renderer.surface.test.tsx` pins all of it through the real SDUI host rather than a bare + component render, and asserts the rendered chrome rather than the forwarded prop, so a + regression where the key is forwarded but no longer acted on is still red. +- d3499b3: `chatbot-floating` now fences its `<FloatingChatbot>` spread the same way its + two sibling registrations (`chatbot`, `chatbot-enhanced`) already do — + `{...toDomProps(props)}`, at the head of the element, instead of a raw + `{...props}` spread at the end (objectui#7708). This is a deliberate, + user-visible behavior change, not a refactor: + + - **A message sent through a floating chatbot now actually renders.** + Previously the authored `messages` seed (whatever array was on the node + when it was authored) silently overrode the live runtime messages on every + render, because the raw spread landed AFTER `messages={runtimeMessages}`. + Neither the user's own message nor an `autoResponse` reply ever appeared — + the identical send on `chatbot-enhanced` worked correctly. Fixed. + - **`displayMode`, `systemPrompt` and `model` stop leaking as DOM attributes** + on the panel's root element (`systemPrompt` / `model` are still read + normally, by name, for the request they configure — only the second, + unfiltered forward is gone). Closes objectui#4425's leak class on the one + `plugin-chatbot` registration that had not closed it yet. + - **Three undeclared keys go dark on `chatbot-floating` nodes:** + `processVisibility`, `surface` and `showAvatars` reached the panel's + `ChatbotEnhanced` through the raw spread even though `ChatbotFloatingSchema` + never declared them. `ChatbotFloatingSchema` documents this explicitly and + always has — the face never promised these keys — so this closes an + accidental channel rather than removing declared behavior. A document that + relied on any of the three to affect a floating node loses that effect; + author them on a `chatbot-enhanced` node instead, where they are part of + the declared, tested contract. + + `@object-ui/types`: `ChatbotFloatingSchema`'s doc comment is updated to match + — no type-shape change, so nothing that imports the type needs to change. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. +- 63b4e0e: Fix the three breaks at the AI paywall moment (#7253), measured on a free plan's + second build iteration when the cloud guardrail refuses "Confirm changes" with a + 429 `AI_DESIGN_QUOTA_EXHAUSTED`. + + - **The upgrade CTA no longer opens a 404.** It used to open a client-composed + `${cloudBase}/apps/cloud-control/sys_environment`, which guessed the control + plane's console mount, app slug and route — all three wrong — and landed on + the API's `ENDPOINT_NOT_FOUND` JSON. `cloudInstallDeepLink` / + `cloudPricingDeepLink` are replaced by `cloudConsoleUrl()`, the + runtime-supplied cloud origin with no path appended; the control plane's own + root redirect decides the landing page. The former + `|| 'https://cloud.objectos.app'` default is gone: a runtime with no upstream + cloud now renders no upgrade link at all rather than pointing a self-hosted + user at the vendor's SaaS. + - **The confirm card gets an explicit failure state.** A quota refusal parks the + card on "not applied" with the server's own next step (reset tomorrow / + upgrade) plus the upgrade action, instead of silently rolling back to + "Confirm / Adjust" as though the click had never happened. Transient failures + (offline, per-minute rate limit) still roll back, because retrying is the + right next step there. + - **The composer is no longer refilled with an already-delivered message.** + Only text typed into the composer is restorable now; card-driven sends + (confirm, approve, suggestion chips) send canned text the user never typed and + no longer leave the previous prompt staged as if it needed resending. +- f157423: Studio workbench and AI tool cards speak the author's language (objectui#7254) + + - The Interfaces breadcrumb, canvas caption and navigation rail show the + metadata label plus a translated kind; the internal `type · name` pair moves + to the tooltip. An unlabelled nav leaf now falls back to its object name + instead of rendering an empty row. + - The Studio top-bar package switcher reads the package's human name from + either position the packages endpoint serves it in, instead of degrading a + registry-shaped entry to its reverse-domain id. + - The dashboard property panel is localized: the spec's authoring form is + overlaid through the platform's own `metadataForms.<type>` convention, so + section headings, field labels, hints and the `header` composite's sub-fields + render in Chinese (developer vocabulary such as "Tailwind units" is replaced + with something an author can act on, not transliterated). + - AI tool cards: tool titles resolve through `chatbot.tool.<name>` (all thirty + platform-provided tools, ten locale packs), the header status badge is + localized, and the plan count strip is a real plural family instead of an + English `+ "s"` concatenation. + - The tool card's header badge and its body badge now come from one producer: + a proposal that has been confirmed, built or published no longer keeps a + header reading "Awaiting Approval". + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 01c9023: `AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173). + + The AI HITL approval inbox held its own relative-time helper returning hardcoded + English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English + relative times on every row. It is the fifth spelling of that helper in the repo, + and the file had **no translation wiring at all** — the unwired-component shape, + not the lookup-swap shape. + + It is therefore swept whole. objectui#7142 wired one string into an otherwise + untranslated component and shipped something visibly half-done, and objectui#7149 + is what finishing that afterwards cost; the triage ruling on this card (2026-09-01) + carried that forward as *sweep the file whole or leave it*. Everything the user can + read now resolves from the locale packs: the card heading and description, the three + tabs, the refresh button, all five status badges, the six column headings, the empty + state, the row and drawer buttons, all nine drawer field labels, the outcome banner + and the whole reject-reason dialog. + + **No new rows for the four relative-time branches.** `detail.justNow`, + `detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed, + translated, in all ten packs, and cross-package key borrowing is this repo's settled + convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`, + `ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`, + `RecordDetailView` and `apps/console` all resolve `detail.*` from outside + `plugin-detail`. One phrase on one kind of control should not get a second + translation that can drift from the first. + + The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an + `aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately + separate from `approvalsInbox`, which is the human approval-**process** inbox — a + different surface and a different feature, so no rows are shared with it. Four + generic verbs are reused rather than forked (`common.refresh`, `common.cancel`, + `common.loading`, `common.ok`). + + **⛔ The five relative-time helpers are not unified.** They differ in real behaviour + — `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d + against 60s/7d, different tails — so normalising them is a behaviour change wearing + a refactor's clothes and needs its own card. This inbox's arithmetic is untouched, + and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s + threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives + `1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date). + + Two assembled English sentences became single interpolated keys — the outcome banner + (`Approve for {{id}}: {{message}}`) and the drawer subtitle + (`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments + around a `<code>` element cannot express, so the two identifiers lose their monospace + styling. That is the deliberate cost of making those sentences translatable. + + Evidence: an `en`-only assertion cannot discriminate here, because each key's `en` + value is byte-identical to the literal it replaced. The suite asserts in **zh and + ar**, and the provider-less path separately, in its own file (`createI18n` installs + itself as react-i18next's module-level global, so a provider-less render in a file + that has already mounted a provider silently reads that pack instead of the defaults + map). No inline `defaultValue` anywhere (objectui#3517). + + Two consequences of the sweep, both landed here rather than left for CI to find: + + `packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its + `vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph + reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so + the frozen surface made that read `undefined` and the file died during COLLECTION — the + objectui#6849 shape, which does not look like a test failure. It now spreads + `importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of + the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed + this to be the only file whose graph reaches the package. + + The ten pack blocks are locale DATA, and locale data lands in the console's eager + `framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's + ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh + measurement (502,405 to 514,863). Attributed by three console builds: the merge parent + reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and + this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing + else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression + the gate must catch) rather than widened; most of the overage was pre-existing drift, with + the merge parent already at 510,192 of the 512,000 allowed. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 3e377c9: Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an + ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's + "Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed + (objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44). + + ⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.** + Ships as `minor` per the launch-window convention: objectui's `major` is a cross-repo pin + to `@objectstack`'s so that "same major means compatible" holds across the two repos + (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes ship as + `minor` with the break named where it lands — this entry is the channel that carries it. + + ## What was retired, and why + + The node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one + selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling + of that same choice, and no renderer has ever read it: `chatbot-floating` renders the + trigger and panel unconditionally, and `chatbot` never looked at the key, so + `displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a + `chatbot-floating` node changed nothing. It was nevertheless declared on both faces, + painted as a **Display Mode** control in the designer's property panel, and written as + `'floating'` into every node the designer created — two surfaces teaching a switch that + did not exist. + + Re-measured on this branch's base rather than inherited from the card: a whole-repo + `git grep` census over tracked files, build output excluded, returned the declarations, + the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and + two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same + pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was + not blind. + + FROM → TO: + + - `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an + ADR-0049 retirement tombstone whose comment points at `type` as the replacement. + - `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, + the same tombstone. objectui#7655 declared the key on the floating face with + `ChatbotSchema`'s own lines precisely so this retirement would find it on both faces; + leaving the copy typed would have kept the published face teaching the switch. + - `chatbot-floating` `inputs`: the **Display Mode** control is removed. + - `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into + designer-created nodes. + + A control is restated, never deleted into a vacuum (objectui#7070): the restatement of + the removed control is the tombstone's guidance plus this note. + + **Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema` + or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` — + `'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for + inline. **No JSON document needs editing** — see the next section. + + ## Stored documents: runtime validation of this key is unchanged — zero before, zero after + + `displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for + both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is + `.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node + the designer ever created) parses green before this change and parses green after it, + and the value is dropped at render time exactly as it always was. + + That is deliberate, and it is why this tombstone has **no `retirementTombstone()` + half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis + (objectui#6152), a different defect, and a parse outcome the ruling did not ask for. + `packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins' + shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if + objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the + mirror adds the `retirementTombstone()` half at that time, flipping the control rather + than deleting it. + + ## Why a tombstone and not a deletion — measured on this carrier + + `ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index + signature, and on such a carrier deleting an optional member is **silent in every value + shape**: the index signature defeats both excess-property checking and the weak-type + check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature + control carrier (`FloatingChatbotConfig`) lit in the same run: + + | route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | + |---|---|---|---| + | declared (before) | clean | `TS2322` | clean | + | deleted | clean | **clean** | clean | + | tombstoned (after) | `TS2322` | `TS2322` | `TS2322` | + + Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned, + **presence with any value** is a compile error — a channel deletion cannot produce on + this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not + louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678). + Prong 2 of that discriminator licenses the tombstone: the key was advertised in the + 3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the + presentation switch. The deleted row is pinned in the test file as a live control — an + undeclared key that rides both shapes with no directive — so the contrast cannot rot. + + ## Accept-set change, one line per face + + - **TypeScript.** A write of `displayMode` against either chatbot face used to compile + and now does not. + - **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document + carrying the key parses green before and after, and keeps the value. + - **Designer.** The **Display Mode** control disappears from the `chatbot-floating` + property panel, and newly created nodes no longer carry the key. + - **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating` + registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to + 8, so the manifest projected from them no longer lists the prop. Measured on both sides + of this change: `validateTree` on a stored `chatbot-floating` node carrying + `displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity + **`warning`**, message `` `<chatbot-floating> has no prop "displayMode"` `` — which is + what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs + the props interface `generateDts` derives from those same `inputs` drops from 20 members + to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those + generated intrinsics no longer type-checks the attribute. + + **This is author-time only: no stored document stops parsing and nothing at render + moves.** The value survives compilation — `compile()` returns a tree still carrying + `displayMode: 'floating'`, byte-for-byte the same keys before and after — and a + `warning` never blocks a page, because the page renderer filters the diagnostics to + `severity === 'error'` before deciding whether to fail. Two neighbouring instruments are + untouched and worth naming so the scope is not read wider than it is: `os validate` runs + `safeValidateSchema`, the Zod path, and is silent on this key before and after; and the + build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does + not contain `chatbot-floating` on either side of this change. +- 4ce14f1: One named, importable authoring-face type per `plugin-chatbot` registration: + `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` join `ChatbotSchema` + (objectui#7655, under the objectui#6169 / #6172 family ruling — every component + node has exactly one named, importable authoring-face type). + + `packages/plugin-chatbot` registers three components — `chatbot`, + `chatbot-enhanced`, `chatbot-floating` — and `@object-ui/types` published ONE + face for the family with `type` pinned to `'chatbot'`. An author annotating a + `chatbot-enhanced` or `chatbot-floating` node either dropped to untyped JSON or + annotated with `ChatbotSchema` and lied about `type`; the docs' floating example + had to be a `json` fence because no `tsx` fence could compile. The two + registrations' real key sets lived in anonymous `ChatbotSchema & { ... }` + intersections local to the renderer, referenceable by nothing outside that file. + + ## The shape, and why not the smaller diff + + One interface per registration, not `ChatbotSchema['type']` widened to the union + of the three keys. The union would give three nodes ONE type and re-open what + #6169 closed — a single interface declaring keys only some of its own `type` + values read — and this card exists because the family's declarations had already + drifted from its reads. Each face declares what ITS registration reads, censused + per key on the PR's base (one `schema.KEY` read per registration body in + `renderer.tsx`, lit by keys that are NOT shared: `processVisibility` 0 / 1 / 0, + `floatingConfig` 0 / 0 / 1), and the twenty keys all three read are picked off + `ChatbotSchema` by name (`ChatbotSharedKey`) so they stay one declaration: + + - **`ChatbotEnhancedSchema`** (`type: 'chatbot-enhanced'`): the shared twenty, + plus `maxHeight` and `processVisibility` (read here, not by the floating + panel), plus `enableMarkdown`, `enableFileUpload`, `surface` (`'card' | + 'plain'`, objectui#6687) and the `onClear` runtime slot — four keys + `ChatbotSchema` never declared. + - **`ChatbotFloatingSchema`** (`type: 'chatbot-floating'`): the shared twenty, + plus `enableMarkdown`, `enableFileUpload`, `onClear`, and the two keys it + declares alongside `ChatbotSchema` — `floatingConfig` (`FloatingChatbotConfig`) + and `displayMode`. No `maxHeight`, `processVisibility` or `surface`: the + floating registration has no named read for any of them. (Its trailing raw + props spread does carry authored keys into the panel today — `processVisibility`, + `surface` and `showAvatars` are live there, measured through the real host; + that accidental channel is tracked as objectui#7708, and this face neither + declares nor promises it.) + - Neither face declares `ChatbotSchema`'s six legacy members (`loading`, + `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown`, `height`) — no + registration reads them by name — and neither redeclares `disabled`, which + stays `BaseSchema`'s `boolean | string` (objectui#7087). + + **`ChatbotSchema` is unchanged.** It keeps `displayMode` and `floatingConfig` + (declarations verbatim), and the floating face declares the same two, so + `ChatbotSchema['displayMode']` and `ChatbotSchema['floatingConfig']` stay the + typed members they were — the objectui#7669 `triggerIcon` tombstone keeps its + reach on `chatbot` nodes, now pinned on the node. `floatingConfig`'s doc comment + is rewritten on both faces: the old text said it was "only used when + `displayMode` is `'floating'`", which was false — it is read by `chatbot-floating` + alone and forwarded to the panel. `displayMode` is RULED RETIRED — objectui#7654, + maintainer ruling B (2026-09-05): `?: never` tombstone, designer control and + `defaultProps` seed removed, in that card's own change. This change carries the + key untouched on both faces (still unmirrored, still read by nothing) so that PR + finds the member exactly as ruled, and a tripwire test pins that any value still + parses green until that PR flips it. + + **New published symbol:** `ChatbotSharedKey`, the string-literal union of the + twenty keys all three registrations read. It is exported from `complex.ts` + because an exported interface may not extend a `Pick` over a private name + (TS4022), so it is emitted into `dist/complex.d.ts` and is reachable through the + published `@object-ui/types/complex` subpath (it is not re-exported from the + package entry). It is a census, not an authoring face. + + ## Zod twins, in lockstep + + `@object-ui/types/zod` gains `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` + (and `ComplexSchema` routes the two new discriminants). Every declared key is an + arm except: the three runtime slots (`onError`, `onSend`, `onClear`), refused by + name per objectui#6124; and, on the floating twin only, `floatingConfig` (no + `FloatingChatbotConfig` mirror exists — minting one is objectui#6152's axis) and + `displayMode` (unmirrored on `ChatbotSchema`'s twin too; retired by ruling on + objectui#7654 and executed there). The twins mirror the API body params under the + key the renderer reads, `requestBody`, and inherit `body` as the children slot — + they do not copy `ChatbotSchema`'s `body` naming collision. + + **Accept-set change, stated plainly:** a `chatbot-enhanced` or `chatbot-floating` + document parsed through the family's only twin used to fail on `type`; through + its own twin it now parses, and the keys the twin declares are VALIDATED where + they rode through `.passthrough()` unexamined before (`surface: 'frameless'`, + `enableMarkdown: 'yes'` and `requestBody: 'x'` are refused). A `chatbot` node's + parse outcome is unchanged: `ChatbotSchema`'s twin did not move. + + ## `@object-ui/plugin-chatbot` + + The `chatbot-enhanced` and `chatbot-floating` registrations type `schema` as the + published faces and drop the anonymous intersections. One consequence: + `chatbot-floating` used to write `disabled={schema.disabled}` and then spread + `{...props}` AFTER it — and `SchemaRenderer` always includes `disabled: verdict + || undefined` in those props, so the raw read was overridden on every render. + With `disabled` honestly typed as `boolean | string` the raw union cannot be + forwarded into the panel's `boolean` prop, so the registration now names the + host verdict (`disabled: hostDisabled`) the way its two siblings have since + objectui#4431. No render outcome moves; the pin renders through the real host + both ways. + + This ships as `minor` for `@object-ui/types` because it widens the published + surface with two new node types, two new Zod twins and one new type alias; + `ChatbotSchema`'s own accept set does not move: objectui's major is pinned to `@objectstack`'s + (`scripts/check-changeset-no-major.mjs`), and objectui's own contract changes + ship as `minor` with the semantics spelled out — as above. +- 79a4b8f: Recognize the landed AI quota ledger vocabulary in the chat error path + + `parseAiQuotaError` now accepts the three SCREAMING_SNAKE ledger codes the cloud + token guardrail emits (`AI_ALLOWANCE_EXHAUSTED`, `AI_DESIGN_QUOTA_EXHAUSTED`, + `AI_DATA_CHAT_TRIAL_EXHAUSTED`) alongside the legacy lowercase trio, which stays + readable for producers that have not converged yet. The companion fields + (`messageEn` / `upgrade` / `topUp` / `resetsTonight`) are now read from the + declared envelope's `error.details` as well as their legacy top-level position, + with the declared position winning. + + A quota-exhausted user gets the upgrade / top-up CTA again instead of the + generic "Response failed" banner. The per-turn message cap's generic + `QUOTA_EXCEEDED` deliberately keeps its existing rate-limit path — it has no + upgrade or top-up next step. +- cc3366b: The built-moment transition (#5799) now fires on auto-publish environments too: `detectBuiltAppPackage` reads the raw build envelope (`status:'drafted'` OR `'published'`, packageId + an `app` item), because an auto-publish posture rewrites apply_blueprint's envelope to `published` and the drafted-only `draftReview` lift never fired there — measured live on staging, where reopening a built conversation stayed on the full page. +- a31adc6: `useObjectChat` no longer rebuilds its `DefaultChatTransport` on every render + (objectui#4187). + + The transport `useMemo` listed the caller's `body` and `headers` in its dep list. + Both are object props and every caller passes a fresh literal each render — the AI + page's chat pane builds its `body.context` inline — so the memo never hit and a + transport was constructed on every render of every chat surface, which during a + streaming turn is once per token batch. + + `body` and `headers` are now read through refs inside + `prepareSendMessagesRequest`, the idiom this hook already uses for the live model + (`modelRef`) and the handoff conversation id (`parentConvRef`), and they are gone + from the dep list. Unlike memoizing at each call site, a future caller cannot + undo it. + + No user-visible behaviour changes: `@ai-sdk/react` keeps the transport in a ref + and re-keys its `Chat` only on `chat`/`id` (verified against the installed + 4.0.68), which `useObjectChat` passes neither of, so the message thread was never + at risk — the rebuild was pure waste. The one real difference is *when* the two + values are sampled: a send now reads them at send time, so it observes the values + of the most recent render instead of those of the last render that happened to + rebuild the transport. That is never staler than before, and it is pinned by + `useObjectChat.transportIdentity.test.tsx`. +- 0fce2ef: `maxToolRoundtrips` on `ChatbotSchema` is deprecated: it is inert, and an author + who sets it is now told so instead of being left believing the documented cap + applies (objectui#5605). + + The key was declared authorable in `@object-ui/types` (interface and zod, with a + description), threaded from the authored document through the chatbot renderer at + three call sites, accepted by `useObjectChat`, given a default — and then dropped. + Measuring the installed chat runtime says it cannot be honoured from here rather + than that someone forgot to wire it: `@ai-sdk/react`'s `useChat` takes `ChatInit` + plus throttle/resume, and `ChatInit` declares exactly one loop control — the + boolean predicate `sendAutomaticallyWhen` — and no numeric cap under any + spelling. The numeric knob was removed from `useChat` in a major, and its + successor was renamed through `continueUntil` to `stopWhen` / `stepCountIs`, + which the installed `ai` package declares only on `generateText`, `streamText` + and the tool-loop agent settings — all server-side. ObjectUI is backend-agnostic, + so it owns no server loop to cap either, and putting the number in the request + body would only move the same dead key one hop onto a wire contract no backend + reads. + + This is stage one of a two-stage retirement, so nothing an author already wrote + breaks: the key still parses, still carries its declared shape, and the renderer + still threads it. What changes is that it is now marked `@deprecated` in the + interface, the zod description and the docs, and that authoring it logs a + one-time notice naming the knob that does work — `planning.maxIterations` on the + agent. A follow-up removes the declaration once this deprecation has shipped in a + release. +- a392e1c: 「打开这条记录 →」卡片——ask 的记录交接终于有了客户端的另一半 + + 服务端半边(cloud#1659 的 `open_record`)先落了地,实测发现它是**半活的**:agent 发出 + `status:'record_handoff'`、回答说「点击上方链接打开」,而上方根本没有链接——控制台 + 有 `build_handoff` 的探测器,这个状态一处都不认识,信号被原样丢弃。 + + 按五步补齐:`detectRecordHandoff`(含持久化 `{type:'text',value}` 包裹形状——replay + 信封那课的规矩)→ live 映射提升 → 水合提升 → 卡片渲染 → 宿主回调。 + + 两个设计点: + + - **app 段点击时现场解析**。记录路由要 `/apps/:app/:object/record/:id`,交接载荷只有 + 对象和记录 id;宿主回调用一次同源元数据读取 `_packageId` 再导航,不给 agent 增加 + 它未必知道的参数。 + - **刻意不做「被取代」置灰**。builder 卡的旧 prompt 会过时,旧的记录链接不会——记录 + 不因为有新交接而失效。 + + 真机闭环验证:问「把《沉默的大多数》标记成已读」→ 卡片渲染 + (`沉默的大多数 — 把阅读状态改为已读`)→ 点击 → 落在 + `/apps/app.hdke/hdke_book/record/<id>` 详情页,「编辑」在手边。 + + 缺任一 id 的交接在探测器就被丢弃,与服务端的拒绝对称——指向空处的卡片比散文更糟。 +- ad404e0: Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- 9a9977c: 工具卡片的名字终于有了 i18n 通道(此前中文界面里必然是英文) + + 实测(cloud#1658,全中文环境): + + ``` + 统计一下每个阅读状态各有多少本书 + Describe object 已完成 执行过程 ← 工具名英文 + Visualize data 已完成 执行过程 ← 工具名英文 + 已统计完成,各阅读状态的书本数量如下:… ← 其余全中文 + ``` + + 卡片上每一处都本地化了——状态、动作、回答——**唯独工具名不能**,因为 + `humanizeToolName` 是个纯英文构词器(`describe_object` → `Describe object`), + 名字从未经过翻译,任何语言包都够不着它。而"它现在在做什么"恰恰是用户最需要读懂的一步。 + + 现在它接受一个可选的 `translate`(形状即 `useSafeTranslate()`),按 + `chatbot.tool.<tool_name>` 查;查不到就回落到与今天完全一致的英文标题。 + + **这一步只打通通道,不改变任何现有显示**:不传 translate 时行为逐字不变(测试的第一组 + 就在钉这一点),语言包也还没有条目。后续两件事各自独立、可分别推进: + 把两个调用点接上 `useSafeTranslate()`;以及按需往语言包里补 `chatbot.tool.*`。 + 先落通道是因为——在通道存在之前,翻译工作根本无处可放。 + + 回落刻意交给英文标题而非原始名:语言包缺条目时显示 `Describe object`(与今天相同), + 而不是 `describe_object`(比今天更差)。 +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-chatbot/package.json b/packages/plugin-chatbot/package.json index e6295bc502..3896e25f32 100644 --- a/packages/plugin-chatbot/package.json +++ b/packages/plugin-chatbot/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-chatbot", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Chatbot interface plugin for Object UI", diff --git a/packages/plugin-dashboard/CHANGELOG.md b/packages/plugin-dashboard/CHANGELOG.md index 0f5ae3c432..179b7293eb 100644 --- a/packages/plugin-dashboard/CHANGELOG.md +++ b/packages/plugin-dashboard/CHANGELOG.md @@ -1,5 +1,1812 @@ # @object-ui/plugin-dashboard +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- 8a6ecac: Retire `FieldMeta.referenceTo` and withdraw the README line documenting it as an + author-facing column override (objectui#6597, enforce-or-remove — withdraw branch). + + **The measurement.** The README documented `referenceTo` as an author override you could + pass on a `object-data-table` column to "bypass auto-detection" of a lookup's related + object. Two `keyof FieldMeta` populations exist, kept separate per the card's own trap + warning: `DatasetRelationship.referenceTo` (a resolver's *output*, unrelated) and this + package's `FieldMeta.referenceTo` (the card's actual subject). For the latter, neither the + schema-derived value nor an authored column override ever reached a reader: + `LookupCellRenderer` (`@object-ui/fields`) resolves its lookup target from + `field.reference_to` / `field.reference` — never `field.referenceTo` — and + `computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field types, never from an + authored column key. Re-measured on this branch's base (`881d5c292`) with the + `referenceTo`-vs-`options` positive control already in + `ObjectDataTable.overrideSource-6425.test.tsx`: `options` (a live override) separates two + equal-valued columns; `referenceTo` does not — an authored override renders + byte-identical to its absence. + + **No authoring story survived the search either.** `ObjectGrid`'s own relational-meta + pass-through (`applyRelationalMeta`, `plugin-grid/src/ObjectGrid.tsx`) copies + `reference_to` / `reference` / `display_field` / etc. from the SCHEMA field def only, at + all three of its call sites — never from an authored column override. No doc, example, or + fixture in this repo shows a table column pinning a lookup's target away from what its + schema field already says. Under the maintainer's standing startup-stage rule + (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no + measured demand selects withdraw. + + **Both `keyof FieldMeta` seam bands, both in scope.** `ObjectDataTable` derives two + refusal bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones + (objectui#6373) and `AuthoredColumnOverrides`' read-side band (objectui#6425) — so + deleting the member would have dropped `referenceTo` from both as a side effect, silently + un-enforcing objectui#6425's "not declared as spelled, still HELD" verdict. A new + hand-written `ObjectDataTableRetiredReferenceToTombstone` (`{ referenceTo?: never }`) is + intersected into both halves of the seam, the exact sibling of + `ObjectDataTableRetiredDecimalsTombstone` (objectui#6625) — same mechanism, same reason. + `ObjectDataTableColumnHolds` — the interface that carried the HELD verdict — is now empty + (kept, not deleted, as the documented extension point a future ruling holds a new key + onto). + + **Ablation.** Removing the tombstone intersection (replacing it with the old + `{ referenceTo?: unknown }` HELD shape) turns two `@ts-expect-error` directives unused + (TS2578) — `tsc -p tsconfig.test.json` exits 2. Restoring the intersection returns a clean + exit 0. This proves the tombstone, not the derived band or a lingering hold, is what + refuses the key now. + + **Behaviour is unchanged** — pinned by the unchanged `referenceTo reaches NOTHING on this + path` runtime assertion, and by two new counter-control tests (mirroring the ones + objectui#6625 added for `decimals`) proving the tombstone specifically is what refuses the + key at both seam bands. + + Marked `minor` per this repo's version-alignment rule (AGENTS.md 版本号策略), which + reserves `major` for following `@objectstack` across a major. Scope note, measured rather + than assumed: `FieldMeta`, `AuthoredColumnOverrides`, `EnrichedColumn` and + `ObjectDataTableColumnHolds` are absent from `dist/index.d.ts` — `plugin-dashboard`'s + barrel re-exports only the `ObjectDataTable` component, and the package's `exports` map + publishes only `"."`. No downstream type moves; this is a package-internal contract change + plus a README correction, not a removal from a published type surface. +- 891a188: Retire `FieldMeta.decimals` and the now-unfed `BuildFieldMetaParams.overrides.decimals` + from `plugin-dashboard`'s shared field-rendering helpers (objectui#6625, + enforce-or-remove). + + `buildFieldMeta` computed `decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale` + on **every** call and the value **reached nothing**. Re-measured on this branch's base + (`efdc6c62`): **zero `.decimals` member reads** across `@object-ui/fields`, + `@object-ui/i18n`, `@object-ui/components`, `@object-ui/core` and `plugin-dashboard` + itself — the only non-comment occurrence was the write being removed here. The + **positive control in the same query shape** fires: `.scale` member reads hit + `NumberField.tsx`, `GridField.tsx` and `fields/src/index.tsx`. So the zero is a + finding, not a broken query. The `overrides.decimals ??` head of that chain had already + lost its only feeder when objectui#6425's ruling removed the authored read from + `ObjectDataTable.enrich()`; `RecordDetailDrawer`, the only other `buildFieldMeta` + caller, passes no overrides at all. Both halves retire together, so the key leaves in + one move. + + Behaviour is unchanged — no reader existed to notice, pinned by the unchanged runtime + assertion in `ObjectDataTable.overrideSource-6425.test.tsx` that an authored `decimals` + renders byte-identical to its absence. + + **The refusal did NOT leave with the member.** `ObjectDataTable` derives *two* refusal + bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones (objectui#6373) + and `AuthoredColumnOverrides`' read-side band (objectui#6425) — so deleting the member + would have dropped `decimals` from both as a side effect, silently un-enforcing + objectui#6425's retire. A new hand-written `ObjectDataTableRetiredDecimalsTombstone` + (`{ decimals?: never }`) is intersected into both halves of the seam, the same shape and + for the same reason as `ObjectGrid`'s `ObjectGridRetiredOptionsTombstone`. The verdict + is unchanged since 2026-08-27; only the artefact enforcing it moved, from derived to + hand-written. + + Marked `minor` per this repo's version-alignment rule (AGENTS.md 版本号策略), which + reserves `major` for following `@objectstack` across a major. Scope note, measured rather + than assumed: `FieldMeta`, `BuildFieldMetaParams`, `AuthoredColumnOverrides` and + `EnrichedColumn` are **absent from `dist/index.d.ts`** — they are not re-exported by the + package barrel, and the `exports` map publishes only `"."`, so no consumer can name them. + No downstream type moves; this is a package-internal contract change. +- 220c18d: Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated + once for the surface (objectui#7063). + + Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that + renders a bare row-placeholder on an empty result is the PLATFORM's defect and + must be fixed uniformly — apps must not compensate widget by widget + (objectstack#13848). The measured scenario is a fresh flagship-demo install: + eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads + as "the dashboard failed to load" even though the widget, its declaration and + its (not yet produced) data are all legitimate. + + - New `WidgetEmptyState` is the seam the three dashboard surfaces now share. + There was no shared placeholder to fix: `DatasetWidget` wrote + `dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote + `dashboard.noDataAvailable` — three renders, two strings, no common code. + - The default now reads as a STATE, not a failure: `role="status"` (the empty + branches previously carried no role at all, while the failure branches beside + them are `role="alert"`), muted treatment with an inbox glyph rather than a + warning triangle, and a title plus an explanation where the placeholder was a + single terse fragment. + - It names WHAT is empty with zero authored copy — the widget's data source, + which is the half the reader cannot already see (the tile's title is rendered + by the card header directly above). That is `widget.dataset` on the dataset + path and `schema.objectName` on the object-bound table/pivot; `PivotTable` + takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable` + forwards. + - Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel` + added to `en` and all nine sibling packs. No inline `defaultValue` and no + interpolation — the source renders as a labelled value, so no separator is + concatenated in code and every pack spells its own punctuation. + + No new authoring obligation and no new spec key. Note that the `emptyState` + override the card assumes for this surface does not exist: `emptyState` is a + LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no + such key — so there is nothing here for an author to override, and adding one + would be a contract question rather than a rider. +- 0dc2c93: `compareTo` on a `scatter` chart is no longer supported — scatter joins pie / donut / + funnel on the list of chart families that ignore it (objectui#7402, maintainer ruling + 2026-09-03). + + **This removes a published capability, deliberately.** Until now a `chartType: 'scatter'` + chart (and the dashboard widget types `scatter` and `bubble`, which both render as one) + with `compareTo` set synthesised a muted "previous period" overlay series. It drew the + wrong picture: a scatter binds ONE measure, and the renderer reads y through the single + `YAxis dataKey={series[0].dataKey}`, so the overlay was plotted on the PRIMARY series' y + — "previous period" painted exactly on top of "current" (objectui#7194). + + Enforce-or-remove: rather than keep drawing that, the capability is removed until it can + be drawn honestly. Drawing a real second measure on a scatter needs the multi-measure + projection recorded as option A of objectui#7194, which is not built (zero authored + callers). **If and when that projection lands, `compareTo` on a scatter returns with + it** — it is the same missing mechanism, one payment. + + What changes for authors: + + - A `compareTo` on a scatter is now IGNORED rather than drawn. The primary series still + renders exactly as before — nothing refuses, nothing goes blank, and no comparison + query is issued on the inline chart path. + - No `<measure>__comparison` (inline chart) / `<measure>__compare` (dashboard) series is + appended for a scatter, so a compare-to scatter document also never reaches the + two-or-more-series scatter refusal being added under objectui#7194. + - Charts that keep the overlay: line, area, bar, horizontal-bar, combo. Charts that + ignore `compareTo`: pie, donut, funnel and — as of this change — scatter (and the + `bubble` widget type that renders as a scatter). + + Reachability at the time of the change: **0** authored scatter/bubble instances in-repo + across both spellings (control `"type": "bar"` fires at 5 example files); incidence in + deployed tenant metadata is not measurable from this repo. +- 1cca678: Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, + maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). + + **What changes for an operator.** A stored dashboard whose header came from a legacy + root `title` now shows its `label`. `label` is the only header source, then the raw + `name`. + + Per surface: + + - Console dashboard page (`DashboardView`) — header falls to `label`, then `name`. + - Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document + with no `label` now shows no header title at all. + - The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to + `label`, then the generic `Dashboard`. + - Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage` + heading) — both fall to `label`, then `name` / the generic heading. + + **Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name** + (`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no + authored dashboard can acquire the key, and what retires is compatibility with documents + stored before that refusal existed. Until now five surfaces read the legacy spelling + independently, which meant a legacy document could show one header in the console and a + different one in the designer. One spelling now answers everywhere. + + **Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard + already carries it and needs no change — it simply starts showing that `label` instead of + the legacy `title`. A document carrying `title` and no `label` was already invalid; give + it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found + 9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'` + component examples that declare no `header`, so none of them rendered a header title + either before or after. + + **Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared** + key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings, + the designer's widget-title input and its per-locale write path all behave exactly as + before. Root and widget arms were separated by receiver, and the retirement's pins carry + widget-level controls on every surface for that reason. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- 2e471a6: `dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title. +- 1d9100e: Dashboard `table` widget — a numeric **measure** column now sorts **descending + on the first click** (objectui#5845). + + The sortable headers shipped in objectui#5827 started every column ascending, + which left the card's own motivating complaint — the largest industry rendering + last — two clicks away. For a measure in an analytics widget the question a + click asks is "who is biggest", so that column now cycles + **descending → ascending → the dataset's own order**. + + Unchanged: a **dimension** column, and a measure whose values are not numbers + (a `min()`/`max()` over a text field), keep **ascending → descending → the + dataset's own order** — the idiom the console's own DataTable uses. The + first-click direction is decided by the same measure-and-all-values-are-numbers + classification that decides right-alignment, so it follows the column's *role* + rather than "it looks like digits": a digit-keyed dimension (a year, a quarter) + still starts ascending. + + Blanks still sort **last in both directions** — that arm is now what a measure + column's very first click runs — and `aria-sort` reports the direction actually + applied. +- 62c0fa1: Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering + fixes so it reads as a table rather than a raw dump (objectui#5827): + + - **Numeric columns right-align**, header and cells. `tabular-nums` was already + applied to every cell and could not do its job while the digits were flushed + left. A column qualifies when it is a MEASURE and every non-null value in it + is a number — the declared column `type` cannot decide it, because the + analytics executor stamps `type: 'number'` on every measure regardless of the + aggregate, so a `min()`/`max()` over a text field would have been + right-aligned on the strength of a label. Dimensions stay left-aligned even + when their bucket keys are digits: a grouping axis is not a quantity. + - **Sortable headers.** Each header is a keyboard-reachable button that cycles + ascending → descending → back to the dataset's own order, with the console's + existing sort indicator and `aria-sort` on the `th`. Blanks sort last in + BOTH directions. The sort is client-side over the rows already fetched — the + widget issues one `queryDataset` call and paginates nothing, so no round trip + is involved. + - **The empty-dimension bucket sorts last** in the default order instead of + floating to the top. It keeps its `—` label; only its position moves. + - **Row hover feedback on every row**, not only on a drillable one. A drillable + row keeps its stronger accent fill. + + The CSV export now follows the order the table is showing, which is the same + "the CSV is the table's data" convention its cell text already followed. + Drill-through is unaffected: rows are reordered as `(row, incoming index)` + pairs, so a drill still resolves through the server's parallel raw-value + sidecar. + + Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and + chart paths, and every non-dataset table in the console. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- 9e725e0: Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846). + + The flat branch now requests the `[]` marginal grouping — the same server-side + totals machinery the cross-tab has used since framework#1753 — and renders the + answer under the console's existing `dashboard.total` label. Totals are computed + by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/ + `max` reports the whole-set aggregate); nothing is recombined client-side. The + row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null + bucket, and never joins the drillable rows. + + No footer is rendered when the executor answers no `[]` grouping, and none is + requested for a table truncated by `options.limit`, whose visible rows a + whole-set total could not be reconciled against. + +### Patch Changes + +- baac3f4: Fix four `find()` calls that passed a query option without its `$`, and gate the shape. + + `QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies + exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw, + no warning, and it type-checks because the type carries `[key: string]: any` for + adapter-specific params. For a dropped cap the result is an **unbounded** read rather than + a truncated one: the platform's GET list route has no default page size, so the query + returns the whole match set and stays invisible until the object is large. + + - `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one + **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so + the dropped key turned "count only, fetch nothing" into "fetch every row in the object", + on every mount and every refresh of every list view. It now sends `$top: 0` and reads + the count off `total` only; the row-counting fallbacks are gone rather than repointed, + because once zero rows are requested an empty `data` means "you asked for none", not + "the object is empty", and counting it would assert a confident `0`. With no total the + footer line is omitted instead. + - `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`, + one line from three correct `$top` calls. + - `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so + a filter's option list read every row and every column of its source object while its + own comment described it as capped at 200. The same call read `records.items`, which is + not a `QueryResult` member, so against a real adapter the fallback produced no options + at all. + - `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the + result in its page-source metadata. + + A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time: + a known query-option name missing its `$` in the second argument of a `find`/`findOne` + call. It is narrow on purpose — a closed list of spellings, anchored to the call — because + the index signature exists so adapters can take adapter-specific params, and a rule that + flagged any unprefixed key would report the shape the type was written to allow. Its + sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- fc62bb4: `TableColumn.type` now has ONE canonical value set across all three ends that disagreed + (objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is + canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | + 'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted + anything; the renderer branched on a third set and could only read the key through an + `as any` cast. + + ## ⚠️ Accept-set narrowing — these spellings stop validating + + `TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now + `z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse + time with `type` named in the error path. Spellings that validated before and are **refused + now**, grouped by why they were being written: + + - **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, + `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is + the card's headline case: it validated, matched no renderer branch, and the column fell + through to plain text rendering with nothing reported. That silent fall-through is the + lenient-validation face that lets AI-authored metadata errors through, and it is now a + loud parse failure. + - **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, + `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of + `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, + not on the column: a column gets its dedicated widget from the field definition behind its + `accessorKey`, never from `type`. + + **Authored metadata in this repo needs no migration.** Measured before tightening, across + `examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files + plus the docs and playground sources): **zero** authored `TableColumn.type` values outside + the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column + position anywhere in the repository. If you author `type` on a table column, check it + against the eight; if the value describes the FIELD rather than the column, remove it. + + ## The renderer's undeclared vocabulary disappears instead of being declared + + `int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` + and `datetime-local` had its own editor branch, none of them declared. They arrived because + column-inference producers forwarded an object schema's field type **verbatim** into + `TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred + value onto the declared vocabulary at their emit seam via the new + `normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, + `datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the + column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) + and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote + the raw field type into the same slot. + + Dropping the annotation is behaviour-preserving at the only consumer that reads the key. + `data-table`'s inline editor branches on `date`, `datetime` and the numeric set and + otherwise falls through to a text input — which is exactly the `undefined` path. The + dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, + which resolves the field through `column.accessorKey` and never reads `type`. + + ## New public API + + `@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single + declaration the zod mirror builds its enum from, so the two cannot drift), the + `TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast + in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared + spelling is a tsc error rather than a silent widening. + + A value-level parity pin covers all three ends + (`packages/types/src/__tests__/table-column-type-canonical.test.ts` and + `packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). + objectui#5684's guard is key-set only and cannot see value drift — `type` was present on + both sides the whole time — which is how this instance survived while its siblings were + caught. A future inference value turning that pin red is by design; the note at the pin says + so, and names the two correct repairs. +- a100f77: The dashboard package now holds ONE relation predicate instead of two that + agreed only because a sweep had just aligned them (objectui#5876). + + `computeLookupExpand` in `ObjectDataTable.tsx` carried its own `isLookup`, + byte-identical to the exported `isLookupType` in `recordFields.tsx` after + objectui#5692 pointed both at `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`. + Nothing kept them aligned: a future edit to either — a member added, the + retirement gate moved — would have re-forked the `$expand` decision from the + predicate whose docblock claims to drive it. `computeLookupExpand` now calls + `isLookupType`, which gains its first production consumer, and the module no + longer imports the shared family or the retirement gate at all. + + **No behaviour changes**, and that is measured rather than assumed: + + - The two bodies were identical, so every boolean answer — `tree` is expanded, + `reference` is not, ordinary relations are — is the same before and after. + - The retired-spelling warning is not emitted a different number of times. + `reportRetiredFieldType` dedupes per SPELLING in one module-level set inside + `@object-ui/core`, which both bodies already shared, so routing two callers + through one function cannot change the count. + + Nothing published moves: `isLookupType` is not re-exported from + `@object-ui/plugin-dashboard`'s entry, so this is internal shape only. + + Because a refactor with no observable delta cannot be pinned by a behavioural + test — a byte-identical local copy satisfies every assertion you can write + about `$expand` — the pin is identity, in + `__tests__/expandableFamily.identity-5692.test.ts`: `computeLookupExpand` is + observed CALLING `isLookupType`, and `ObjectDataTable.tsx` is read at source + level to confirm no second body survives for it to call instead. +- 99a3c2d: `BaseSchema` declares `bind`, the data-scope binding path, on both halves — the TypeScript + interface and its Zod mirror (objectui#6357). + + `bind` was read by ten production sites and declared by no schema shape. It resolved as `any` + through `BaseSchema`'s index signature and rode `.passthrough()` on the validator, while three + separate documents taught it as an authorable key of *every* node: this repo's own `AGENTS.md` + §4 ("Every node in the UI tree follows this shape (`@object-ui/types`)"), the published + agent-facing `skills/objectui/rules/protocol.md` ("Every UI component node MUST follow this + shape"), and `content/docs/fields/grid.mdx`. So the agent-facing protocol told authors to write + a key the published types did not know existed. + + The census chose the home rather than guessing it. Nine reads go through + `useDataScope(schema.bind)` — `list` and `tree-view` in `@object-ui/components`, and the + `object-*` widgets in `plugin-charts`, `plugin-dashboard` (×2), `plugin-grid`, `plugin-kanban`, + `plugin-list`, `plugin-timeline`. A tenth is `plugin-grid`'s `gridNeedsDataSource` predicate, + where a present `bind` is one of the escape hatches that makes a missing data-source adapter + legitimate. Two more sites destructure the key out so `SchemaRenderer`'s prop spread cannot + write `bind="data.revenue"` onto the DOM. Per-component declaration was measured and rejected: + it costs nine copies of one key and buys nothing extra, because neither half can refuse the key + on a non-reader either way. `placeholder` is the standing precedent for a cross-cutting key + declared on `BaseSchema` and honoured only by a subset. + + **Accept-set narrowing, on the value and not the key.** `bind: 42` type-checked and parsed green + before this change and is refused by both halves now. It only refuses what already crashed: + `useDataScope` is `(path?: string)` and resolves via `path.split('.')`, so a non-string `bind` + threw a `TypeError` at render time. Every `bind` authored in this repo is a string, and the + declaration is optional, so nothing that renders today stops. + + **What this does NOT change**, stated because the pin would otherwise be read as more than it is: + an *undeclared* key is still accepted by both halves, so this did not buy rejection of a + misspelling such as `bindTo` (objectui#5155 / objectui#6269 own that ceiling). And `data-table` + still does not call `useDataScope`, so a `bind` on it is still ignored and still renders a header + over an empty body with no error — a documented silent failure that this declaration neither + causes nor cures, since the key was accepted on every node before it existed. + + `ObjectPivotTable` drops its local `bind?: string`: its `PivotTableSchema & {…}` intersection + extends `BaseSchema`, so the member was a true duplicate. Two other local declarations are left + in place and ratcheted rather than removed — their containing types never reference `BaseSchema`, + so deleting the member would delete the declaration rather than inherit it. +- 129c7a9: `ObjectDataTable` no longer writes six undeclared keys into the `data-table` columns slot + (objectui#6373). `enrich()` returned `NormalizedColumn`, whose `[key: string]: any` accepts + anything, so nothing checked the producer's output against + `DataTableSchema.columns: TableColumn[]`: `{ ...col, ...fieldMeta }` spread `label`, + `options`, `referenceTo`, `format`, `currency` and `decimals` onto every emitted column, and + `TableColumn` declares none of them. + + The measured read set of the consumer (`data-table.tsx`, comments stripped) contains none of + the six, so all six retire from the emit rather than being declared — declaring a key nothing + reads is the same `declared != enforced` defect facing the other way. Rendering is unchanged + because none of those keys was the live path for its own value: the `FieldMeta` the `cell` + closure captures is what this widget's type-aware rendering has always read, and it is + untouched. Authored spellings still pass through, so a column the author wrote as + `{ format: '$0,0' }` keeps its `format` exactly as before. + + `type` is unchanged — objectui#5853's fold at this seam still applies. `name` is unchanged + and still written: `data-table` reads `col.accessorKey || col.name` and objectui#5120 holds + that alias while two published skill guides still teach a `{ name, label }` column. The hold + is now declared at the seam instead of arriving anonymously inside a spread. + + The seam's emit type carries ADR-0049 `?: never` tombstones for the retired keys rather than + being a bare `TableColumn` annotation. Measured before the shape was chosen: a bare annotation + raises no error at all here, because TypeScript's excess-property check exempts properties + that arrive through a spread — it would have type-checked the boundary without enforcing it. +- 38a123c: Land objectui#6425's per-key ruling for `ObjectDataTable`'s authored column + override keys (maintainer, 2026-08-27): + + - **Declare `format`, `options`, `currency`** on `TableColumn` and its + `TableColumnSchema` zod mirror, in the same stroke. All three are honoured + by `object-data-table`'s cell pipeline — `format` / `options` were + documented author overrides the published types refused (a typed author got + a compile error and the zod parse silently stripped the key); `currency` + shipped in production but was never promised. The zod mirror now passes the + keys through instead of stripping them; `StaticTableColumn` and its mirror + tombstone all three under the #5474 lockstep rule (the static renderer + reads no field-meta overrides). + - **Retire `decimals`**, immediately: zero readers measured anywhere + (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads + `precision`), so no authored `decimals` could reach a render. The authored + read is removed and the key falls into `AuthoredColumnOverrides`' derived + refusal band — render output is pinned unchanged. + - **Re-arm plugin-grid's #6004 `options` retirement with an explicit + tombstone**: that refusal rested on the key's NON-membership (excess-property + freshness), which declaring `options` on `TableColumn` silently ended. + `ObjectGridRetiredOptionsTombstone` (`?: never`, intersected into both + `ObjectGridColumnDraft` and `ObjectGridColumn`) restores the refusal by + assignability; #6004's verdict itself is unchanged. + - **`referenceTo` is deliberately NOT declared as spelled** — it stays held, + owned by objectui#6597 (fix the spelling chain or withdraw the README + line). The remaining hold is that card's scope, not unfinished work here. +- aff3d7a: A `bind` authored on a `data-table` is now diagnosed at render instead of ignored in + silence (objectui#6575). + + `bind` is the data-scope vocabulary: a path string resolved by `useDataScope()`. + `list`, `tree-view` and the `object-*` plugin widgets read it. `data-table` does not + — it takes its rows from an inline `data` array on the node and never calls the hook. + A `bind` on a `data-table` was nevertheless accepted by every gate: the TS side via + `BaseSchema`'s index signature, the zod side via `BaseSchema` being `.passthrough()`, + which `DataTableSchema.extend(…)` inherits. Nothing read it at render, so the author + got a table drawing a correct-looking header over the "No results found" empty state, + with no error and no warning — a success receipt for a disagreement between the + author and the renderer, and the hardest failure shape for a human or an AI author to + self-check. + + The platform was already paying for this in teaching rather than in diagnostics: + `skills/objectui/rules/protocol.md` documents the pothole verbatim and a pin test + locks the behaviour. The warning now also reaches the console, where the author who + did not read the docs is standing: + + > `bind: 'customers'` is ignored: data-table does not read `bind`; it reads its rows + > from the inline `data` array on the node. This node has no inline rows, so the + > table renders its header over an empty body. + + It names the node's address, the path that was spelled, and the way out. The + consequence clause is measured rather than asserted: a node carrying BOTH `data` and + `bind` is not empty, and is told that its rows came from `data` and its `bind` + contributed nothing. + + **No behaviour change.** `data-table` still does not read `bind`, and per the + 2026-08-27 ruling it must not start — making it a `useDataScope` reader is a separate + published-surface question needing its own ruling, including a `data`-vs-`bind` + precedence. Refusing the key at parse stays blocked on the `.passthrough()` ceiling + (objectui#5155 / objectui#6269). The trap stops being silent; it does not stop being + a trap. The channel is the one `plugin-grid`'s `columnSpellingDiagnostics.ts` already + uses for this exact shape of failure — a pure describe function, a `useEffect` keyed + on the schema slice, one `console.warn`, no NODE_ENV branch. + + `ObjectDataTable` (`@object-ui/plugin-dashboard`) stops forwarding a `bind` it has + already consumed. It resolves the binding itself via `useDataScope(schema.bind)` and + then delegated with `{ ...schema, type: 'data-table', … }`, which handed the spent + key to a component that cannot read one. Without this, a correctly authored and + published-guide-taught `object-data-table` would have tripped the new diagnostic on + every render, over rows that were on screen precisely because its `bind` had been + honoured. The key is stopped where it was spent — the same shape its sibling + `DashboardGridLayout` already uses for `data`. Nothing else about that delegation + moved, and the bound rows still arrive. +- 4ca30d0: Two widget prop types anchor their `schema` to exported schema types that extend + `BaseSchema`, instead of hand-rolled inline literals with no `BaseSchema` in + their ancestry (objectui#6576, maintainer ruling 2026-08-31 option A; folds + objectui#6914). + + - `@object-ui/types` exports `ObjectGallerySchema` (`type: 'object-gallery'`) + and `ObjectDataTableSchema` (`type: 'object-data-table'`), each `extends + BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors + of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema` + declares the two keys the widget was reading behind casts — `drillDown` + (`DrillDownConfig`) and `onRowClick` — which no declaration carried before. + - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is + `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is + writable (`visibleWhen`, a real base member, was a compile error on the + literal) — and NARROWS in one place: `type` is now required and pinned to + `'object-gallery'`. `data` stays `Record<string, unknown>[]`. + - `@object-ui/plugin-dashboard`: `ObjectDataTableProps.schema` (not exported + from the plugin index) is `ObjectDataTableSchema`. The literal's own + `[key: string]: any` is gone, so a wrong-typed base member (`visible: 42`) and + a wrong-shaped `drillDown` are refused, and `type` is pinned to + `'object-data-table'` instead of bare `string`. + + Unchanged on both, stated plainly: an UNKNOWN key still compiles, because + `BaseSchema`'s index signature is inherited (objectui#5155, open). No runtime + behaviour changes; the widgets render exactly as before. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- d636743: Feed the lookup cells in `ObjectDataTable` and `RecordDetailDrawer` their reference target, + so schema-aware display-name resolution and drill-through links engage for the first time + (objectui#6694). + + Both widgets build their cell meta with `buildFieldMeta` and render it through + `renderFieldValue` → `getCellRenderer` → `LookupCellRenderer` (`@object-ui/fields`). That + renderer resolves its target from `field.reference_to || field.reference`, and `FieldMeta` + carried neither spelling — nor `display_field`. So the renderer resolved `undefined` and two + things failed, independently and both silently: + + - `useRefObjectSchema` never loaded the referenced object's schema, so the ADR-0079 / + objectui#2357 resolution never ran and every cell fell back to `pickRecordDisplayName`'s + generic `.name` / `.title` heuristic. Quiet, because that heuristic usually still produces + a readable name — it diverges only when the referenced object's display field is not + literally `name` / `title`, and then it silently shows the wrong one. + - `ReferencedRecordLink`'s `objectName` was always `undefined`, so `navigable` was always + `false` and no lookup cell in either widget ever rendered a real anchor — no + drill-through, no middle-click-new-tab, no copy-link. Quiet, because the cell still + rendered its value as plain text. + + This RESTORES intended behaviour rather than adding surface. `ReferencedRecordLink` was + placed in the shared cell renderer precisely so every surface would get the affordance once + ("Both surfaces resolve through `LookupCellRenderer`, so the affordance belongs here, + once"), and `plugin-grid`'s `ObjectGrid` has fed it all along via `applyRelationalMeta` at + all three of its column-building call sites. These two widgets simply never adopted that + copy. Nothing new is authorable: the keys come off the OBJECT SCHEMA field def authors + already write, never off a column override — the distinction objectui#6597 measured when it + retired `referenceTo`, and the reason this needs no new column hold. + + The copy is made once in `buildFieldMeta`, the seam both widgets funnel through, so the two + surfaces cannot drift — which is what that module exists for. + + ⚠️ The copy set is three keys where `ObjectGrid`'s `RELATIONAL_META_KEYS` is nine, and the + difference is measured per key, not preferred. The grid's cells are EDITABLE, so its extra + keys drive the inline picker's query (`LookupField` / `UserField` read `id_field`, + `description_field`, `lookup_filters`, `lookupFilters`); these two widgets are read-only and + their render path ends at a cell renderer. `packages/fields/src/index.tsx` reads exactly + `reference_to`, `reference` and `display_field` off a cell's `field` prop; `titleFormat` is + never read off a field meta at all (its readers take it off the object schema, which arrives + here through `useRefObjectSchema(reference_to)`), and `reference_to_field` has zero member + reads anywhere in the repo. Copying the other six would mint six members written on every + call and read by nothing — precisely what objectui#6625 (`decimals`) and objectui#6597 + (`referenceTo`) retired from this same file. + + No published type widened: `FieldMeta` is internal to the package — it is not re-exported + through the barrel (`dist/index.d.ts` names neither it nor `recordFields`) and the `exports` + map publishes only `"."`, so Node refuses `@object-ui/plugin-dashboard/recordFields` with + `ERR_PACKAGE_PATH_NOT_EXPORTED`. + + Behaviour note for existing dashboards: a lookup cell whose referenced object declares a + `nameField` other than `name` / `title` will now show that declared name instead of the + heuristic's pick, and valued lookup cells become links wherever the host publishes + `recordHref`. +- 9700dd9: A lookup cell in `ObjectGrid` now honours the author's `displayField` + (objectui#6875). + + `ObjectGrid` copies a set of relational keys off the object-schema field def + onto each column's `fieldMeta`, and that bag is what the lookup cell renderer + and the inline picker receive. The set was hand-kept and had become a strict + SUBSET of what those two consumers read — `displayField`, `descriptionField` + and `lookupColumns` were read on the grid's own path and never copied. + + They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema` + is strict and declares `displayField` / `descriptionField` / `lookupColumns` / + `lookupFilters` / `reference`, and none of the snake_case twins the copy set + mostly carried — those parse to `unrecognized_keys`, so a spec-compliant + producer cannot emit them. Nothing renames anything on the way in either: the + adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄ + `reference_to` pair. So an author who declared `displayField: 'project_code'` + got a grid cell showing the referenced record's generic `.name` instead. + + - The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from + a table that classifies every key the consumers read off this bag. A gate + re-extracts that read set from the consumer sources on each run and fails on + any unclassified spelling or orphan, so the two cannot drift apart again. + - `reference_field` and `lookup_columns` — the other two never-copied keys — + stay out on purpose: `FieldSchema` declares neither, so no producer can fill + them. The gate proves that against the installed spec rather than asserting it + in prose. + - `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the + same fallback chain and gains `displayField` too. +- 5ad86dd: **Breaking for authored metadata:** `TextSchema.value` is RETIRED (objectui#6951, + maintainer ruling A1 of 2026-09-04; objectui#7016; ADR-0049 enforce-or-remove). + A `text` node that authors `value` no longer validates: the parse fails loudly on + the `value` path with the explanation in the message, the TS member is a + `?: never` tombstone so the same document is refused at compile time, and the + renderer no longer reads the key. Write `content`. + + **What was measured, on this branch's base.** `TextSchema` declared two spellings + for its one content slot — `content` (read first) and `value` (the fallback limb + of `{schema.content || schema.value}` at `renderers/basic/text.tsx:162` and + `:167`) — both declared by objectui#6150, whose docblock called the pair "a + dialect, not a design" and deferred the choice. The ruling's premise, that + `value` is the minority spelling, was measured before any edit over the four + roots it named: **776 `content`-only `text` nodes, 25 `value`-only, 0 authoring + both** across `examples/` (674 / 13), `apps/` (59 / 0), the `examples/` + directories under `packages/` (0 / 1) and `content/docs/**` (43 / 11) — a + thirty-to-one majority for `content`, so the retirement went ahead as ruled. + (A further 14 `{ value, label, type: "text" }` objects in the filter-builder + catalog entries are field descriptors whose `type` is a field type, not `text` + nodes, and were excluded by kind.) + + **Who is affected — a `value` authored on a `text` node:** + + ```json + { "type": "text", + "value": "Hello" } // ← was tolerated (rendered as the fallback) + ``` + + now fails validation with: + + > RETIRED (objectui#6951) — `value` is no longer part of TextSchema; write + > `content`. It was a second spelling of the one content slot, read only as the + > fallback limb of `schema.content || schema.value`, and was retired under + > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling A1, + > 2026-09-04). The renderer reads `content` alone now, so an authored `value` + > would render nothing. Rename the key; the string is unchanged. + + **Two published faces, one retirement.** The TypeScript interface `TextSchema` + (`@object-ui/types`, `layout.ts`) declares `value?: never`; the Zod mirror + `TextSchema` (`@object-ui/types/zod`, `layout.zod.ts`) declares `value` as a + `retirementTombstone()`, so the key stays DECLARED and is refused BY NAME — + a plain deletion would have let an authored `value` ride `BaseSchema`'s + `.passthrough()` into a silent blank, which is worse than the tolerated + fallback it replaces. The `value?: string` members of `TextSpanSchema` and + `TabsSchema` in the same file are other schemas' contracts and are unchanged. + + **`@object-ui/components`** — the `text` renderer renders `{schema.content}` at + both arms (the `|| schema.value` limb is gone from each), and the `context-menu` + renderer's built-in fallback trigger node now spells `content`. Nothing else in + the package moves. **`@object-ui/plugin-dashboard`** — its three placeholder + `text` nodes ("chart type is not supported yet", "Custom widget — set + `component`…", the retired-widget notice) spell `content` so they keep rendering; + their wording is unchanged and still pinned. + + **Who is NOT affected.** A document that already wrote `content` is untouched; + `content`, `variant`, `align` and `className` are unchanged; `absent` stays + valid (`{ "type": "text" }` still parses). Every in-repo document that authored + `value` on a `text` node was rewritten to `content` in the same change: nine + `examples/schema-catalog` entries, `packages/types/examples/zod-validation-example.ts`, + eleven doc fences under `content/docs/`, and the `@object-ui/components`, + `@object-ui/react` and `@object-ui/types/zod` README samples; the catalog is now + pinned tree-wide against the retired spelling. + + **Migration:** rename `value` to `content` on every `text` node; the string is + unchanged. If a document authored both, `content` was already the value that + rendered — delete `value`. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spelling. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). + + objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope + (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This + closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, + `DetailView`, and `ObjectDataTable` (which builds its own whitelist in + `computeLookupExpand` rather than calling `buildExpandFields`). + + **Three of them pass no column list at all**, which makes them the sharp ones: + `buildExpandFields` reads an absent column list as "no column restriction" and falls + back to **every declared relation on the object**, denied ones included. So a standalone + calendar, a gantt, and every record page in the console asked the server to resolve the + object's full relation set by default rather than by configuration. + + **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column + list is already FLS-filtered field by field, which is exactly the route PR #7229 measured + as unsound: an emptied column list reads as "no column restriction", so a detail view + whose authored fields are all denied had its `$expand` **widened** from the relations it + asked for to every relation the object declares. The principal who may read least was + asking for the most. + + **Reproduced before it was fixed**, as a failing test per site. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip, and the + client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, + which contains only the object's declared reference-bearing fields, so the "`checkField` + answers false for an undeclared key" trap cannot be reached and derived / host-joined + columns are untouched. An unanswered permission policy filters nothing. Neither + `buildExpandFields` nor `computeLookupExpand` is changed. +- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts + (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer + ruling B, 2026-09-02). + + A dataset-bound chart's aggregate axis / legend title read the analytics + service's hard-coded English `Count` on a zh console whose category labels were + already Chinese. The renderer was passing `fields[].label` through verbatim — + correctly, for an author-declared measure (objectui#4106) — and had no way to + tell the server's built-in default apart from an author's label. + + The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural + discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | + 'count_distinct'`, populated only on the server-side built-in defaults + (objectstack#14492). This change is the consumer side of that contract: + + - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — + `ChartResultField` plus the optional `builtinAggregate` carrier + (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than + on it because the spec this release is built against does not carry the key + yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / + `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries + the locale strings in (core stays React-free and i18n-free — the same + division as `nullCategoryLabel`). A field carrying a recognised + discriminator resolves through that map; every other field keeps its wire + `label` verbatim — never by matching the label's text or the field's name + (the rejected option A). + - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings + through the existing `report.aggregate.*` keys (zh already carried 计数 / + 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). + - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), + `plugin-report` (`DatasetReportRenderer`): pass the resolved map to + `buildChartSeries`. + + Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same + chart titled `计数`; an `en` session still reads `Count`; an author-labelled + measure (`Tasks`) and a measure literally named `count` without the + discriminator are byte-for-byte unchanged. Until the upstream field is + populated the wire carries no discriminator and every chart renders exactly as + before. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- fc32921: Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing + (objectui#8266). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `field` — the normal way to author "how many records per status" — rendered an + empty chart. No error, no empty state: a plot frame with the category ticks drawn and + not one mark in it, which reads exactly like "this object has no rows yet". + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + built the series binding as `aggregate?.field || (options.yField || 'value')`, which for + a fieldless count resolves to `'value'`. The rows an object-bound fieldless count + returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned + since framework#3701. A `dataKey` naming a column no row carries plots nothing, and + neither of the renderer's two guards fires on it: the rows DO carry the category key, + and the series array is not empty. + + **Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to + `chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the value column an object-bound aggregate produces". Both relays now consult it, and + the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed + through the same function, so the two halves of the question cannot drift again. + + **What moves on screen.** A chart that was blank now draws. Charts that already drew are + unaffected: a field-bearing aggregate resolves to its raw field under both the old and + the new reading, and a chart with no `aggregate` at all keeps the author's `yField`. + One authored key changes meaning: a `yField` written on an object-bound chart that + ALSO declares an aggregate no longer wins over the aggregate's own column — it named a + record column that a grouped aggregate never returns, so it plotted nothing before. + + **Not fixed here, and out of scope.** The same widget with no `options.xField` is + refused by the category-axis guard naming `name`, a key the author never wrote (they + wrote `aggregate.groupBy`). That is the category half of the same relay gap and is + filed separately. +- 8f9d87a: Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being + refused for lacking a `name` column (objectui#8269). + + A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }` + and no `options.xField` rendered a refusal instead of a chart: + + > This chart cannot plot its category axis: no row has a `name` field. + + The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that + had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent + them to debug the wrong layer. + + **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each + floored the category binding on a literal — `options.xField || 'name'` — and handed it to + the `object-chart` node without ever consulting the aggregate that decides it. An + object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no + row carried `name` and the category-axis guard (framework#4033) fired correctly on a + binding that was already wrong when it arrived. + + **Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to + `chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of + "the category column an object-bound aggregate produces", and the published sibling of the + `chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now + consult it for the object-provider branch. + + **What moves on screen.** A widget that rendered a refusal now draws. Measured through + `ChartRenderer` at 480x320 over the rows a fieldless count returns + (`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from + `xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`, + 1 series and 2 marks with the category ticks drawn. + + **Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows + are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it + returns a single row with no category column), and the authored-literal-rows branch — the + `chart` node composed after the object-provider check fails — keeps its floor unchanged. + One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField` + written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the + aggregate's own column — it named a record column a grouped aggregate never returns, so it + produced the same refusal before. +- 3d79455: `plugin-dashboard`'s two private copies of the reference-bearing field family now + read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating + it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside + `computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the + already-converged consumers carry, so a member-identical private copy fails + rather than quietly re-forking the table. + + Two behaviour changes on the dashboard table's `$expand`, in opposite directions: + + - **A `tree` column is now expanded.** A self-referencing hierarchy field is + reference-bearing and a member of the shared family, so its cell renders the + parent record's display name instead of a bare id — the same treatment the form + and grid roads already gave it. + - **A `reference`-typed column is no longer expanded.** Measured before removing + it: `reference` is absent from `@objectstack/spec`'s closed `FieldType` + vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant + object schema can declare a field whose stored type is `reference`. Dropping it + is a no-op on real data; the spelling is a legacy dialect alias on the + action-param surface, folded to `lookup` before any field-type data is read. + + `EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license + widening a published shared set. +- cc5de5c: `DashboardRenderer` no longer emits an empty dashboard header wrapper. The + wrapper used to render whenever `header` was declared, while each of its + children — title, description, actions — was additionally suppressible. With + the console page chrome present (`hideHeaderText`, set because the chrome + already renders the dashboard's title and description) and no `header.actions`, + every child evaluated falsy and the DOM still received + `<div class="col-span-full mb-4"></div>`: zero children, yet a full grid row + (measured 64px) plus `mb-4` of dead band above the filter bar, on every console + dashboard page (objectui#5812, measured on HotCRM 17.1.0). + + The three children are now computed first and the wrapper renders only if one + of them survives. Nothing else changes: a standalone embed (no chrome) renders + title and description exactly as before, and declared `header.actions` keep the + wrapper alive even under the chrome, since the chrome renders text only. Authors + needed this — dropping `header` from the metadata to reclaim the pixels would + have cost the standalone embed its title, which is what `header` is for. +- 84d2e98: `DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained + (card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally + (objectui#4620). + + `isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the + `isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The + `datasetBound` side of that inner fork could never execute: reaching it required + `isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged — + the removed arm never ran, and the reachable fork in the Card branch (the one that gives + a dataset-bound metric its title and border chrome) is untouched. + + The cost was to readers, not to users: the shape read as "both branches handle + dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto + `DashboardGridLayout` had to pay for the reachability argument before it could decline to + copy the dead limb. A comment now names the invariant in place so the arm is not re-added. +- f24195a: `ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for + "no rows yet" instead of a fresh array literal per render (objectui#4629). + + Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so + whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind` + path that resolves to an object — the fallback produced a NEW array identity on + every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so + every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the + `isSystemField` pass, the `fieldLabel` lookups) and then discarded by the + `finalData.length === 0` early return. In `ObjectPivotTable` the value is handed + straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot + rebuilt its row/column sets, bucket map and totals on every render over no rows + at all. + + Nothing rendered wrong before or after; this is wasted work in the empty window, + plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is + the same module-scope frozen empty `data-table.tsx` adopted for its own + `EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings. +- 56f4e34: A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does. + + `ObjectDataTable` derives headers on two paths — from the author's declared + `columns`, and from the object schema when no columns were declared. The + declared path (and the static `data-table` half of the same widget family) + already used `humanizeFieldKey`, whose docstring names it "the single home for + the convention, because both halves of the `table` widget family need it and + they must agree". The auto-derived path carried a third, inline spelling that + split camelCase but never turned `_` into a space, so it left a raw underscore + on screen. Measured over the same object's columns: + + ``` + path close_date needs_analysis + object-bound, AUTO-DERIVED (before) Close_date Needs_analysis + object-bound, AUTO-DERIVED (after) Close Date Needs Analysis + object-bound, DECLARED columns Close Date Needs Analysis + static `data-table`, no columns Close Date Needs Analysis + ``` + + One dashboard can hold all three widgets over one object, so a single field key + rendered under two spellings — the defect class objectui#5425 rules out. The odd + path adopts the shared convention rather than the convention gaining a fourth + dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and + after — the coincidence that kept the snake_case divergence unnoticed), and a + translated header still wins: only the fallback handed to `fieldLabel` changed. + + Dimension MEMBER labels are untouched by this. The same card reported dashboard + members rendering a prettified enum instead of the picklist's translated label, + measured on 17.1.0; re-measured on this branch it no longer reproduces — the + analytics label net shipped in 17.5.0 routes every non-metric dataset dimension + through the field's declared options and the locale bundle. That behaviour had + no test stated in the card's terms and now has one, over the four dashboards the + card measured, including the property that a bar axis and a pivot header cannot + disagree about one stored value. +- 84d70bb: `ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel` + resolvers directly instead of holding them behind refs, so a pivot re-derives + its header and option labels when the resolver genuinely changes + (objectui#5625). + + The refs existed for a reason that no longer holds. `useSafeFieldLabel()` + returned a fresh object on every render outside an i18next provider, so a direct + dependency would have re-run the metadata-derivation effect on every render — + and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly + built objects, so each run scheduled the next one: an unbounded derive loop. + `ObjectPivotTable` worked around that locally with `fieldLabelRef` / + `fieldOptionLabelRef` plus a `useEffect` keeping them current. + `useObjectLabel`'s memo now holds with or without an i18next instance bound + (objectui#5564), so both resolvers have a stable identity on both paths and the + indirection buys nothing. + + It did cost something, and that is the user-visible half: a ref-hidden + dependency meant the derivation did NOT re-run when the resolver changed. A + pivot mounted before its `I18nProvider`, or rendered across a language switch, + kept showing its top-left header label and its select-option cell labels as + resolved by the old resolver — until some unrelated dependency (the data source, + the object name) happened to move. It now re-derives once on that transition and + renders in the active language. + + This is the same removal objectui#5587 made in `ObjectChart`, one package over. + + Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations + across forced re-renders both outside and inside a provider, checks that the two + derived state maps settle, and asserts the language-switch re-derivation. + Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the + no-provider case red (derivations in the dozens instead of 1), so the removal is + pinned to the fix that unlocked it rather than to a comment. +- f1c27f0: Dashboard record fields: percent columns now render through the one percent + scaling decision instead of a second, drifted copy of it. + + `renderFieldValue`'s `%`-format branch normalised the value itself before + calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`, + then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`, + which `@object-ui/core` documents as the single source of truth for percent + display scaling, so the branch was re-deciding what core owns — and its copy had + drifted from it in three measured ways: + + - `(value / 100) * 100` is not value-preserving in binary floating point, + re-introducing one call frame upstream the round trip that was removed from + inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000 + values change bit pattern and 1,108 rendered strings move, every one a + last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is + `1.61%`. + - A stored fraction below `0.01` was scaled twice — the local `* 100` put it + back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered + `50.00%`. + - The local test was `value > 1` rather than core's symmetric `|value| < 1`, so + a negative already in percentage points took the fraction arm: `-5` rendered + `-500.00%`. + + The branch now hands the raw stored value to `formatPercent` — the identical + call the list-view percent cell already makes for an ordinary percent column — + so a percent reads the same as a record field, as a grid cell and as a dashboard + measure. Output moves where it was wrong: values at or above 1 whose round trip + lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly + `1`, which is one percentage point by core's convention and now renders + `1.00%` at two decimals, where the local `value > 1` test had made it + `100.00%`. +- 0ccbdc1: `PivotTable` no longer re-runs its cross-tabulation memo on every render when it + has no rows (objectui#5562). + + The component spelled the empty array twice — as the destructuring default for + `schema.data` and as the `Array.isArray` fallback that keeps a provider-config + object out of iteration — so a schema declaring no `data` key, or one whose + `data` is a provider config rather than rows, produced a fresh array identity on + every render. That value is the first entry of the memo's dependency list, so + the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the + aggregated matrix and the row/column/grand totals on every render, over nothing. + Both spellings now resolve to one module-scope frozen empty, so "no rows" is a + stable value and the memo holds. + + Wasted work only: the churn feeds a memo rather than a `setState`, and + `PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no + render loop was possible. The identical fix landed for `data-table` in + objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the + direct-use path those two did not cover, where `DashboardRenderer` and + `DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the + chain. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. + + The dashboard widget config panel carried a private `resolveLabel` documented as + resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference + form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline + per-locale map `I18nLabelSchema` actually admits has neither limb, so + `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy + of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, + `MetricWidget` and `MetricCard`. + + This was not a display bug. The resolved value seeds the panel's editable draft, + so a widget whose stored title was a map opened with an **empty** Title field and + the next save wrote `''` over the author's map — on the ordinary path, not an + exotic one: open the widget, change anything, save. + + Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: + + - **Reading** goes through `pickLocalized(value, language)`, so the panel shows + the active locale like every sibling surface post-objectui#4032. + - **Writing** replaces only the active locale's entry and carries every other + locale across. A title the author never touched round-trips the stored object + itself through an unrelated config edit; an edited one merges into the entry + that was displayed. The live-update callback (`onFieldChange`) forwards the + merged map for the same reason — hosts feed it back into the widget the panel + re-opens from, so a bare string there dropped the map before a save ever ran. + + `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side + inverse of `pickLocalized`, so the rule is stated once instead of re-derived per + panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, + region-qualified sibling — and deliberately stops there: the `default` / `en` / + first-value limbs are display fallbacks that hand back *another* locale's string, + and writing to one would let an author editing in `fr` overwrite English. With no + entry for the active locale the edit adds one. The pairing + `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a + write that lands where the read does not look is how a "saved" string disappears. + + A full multi-locale editing UI remains out of scope (objectui#4163). +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index 688c9d7dbc..e157af2099 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-dashboard", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Dashboard plugin for Object UI", diff --git a/packages/plugin-designer/CHANGELOG.md b/packages/plugin-designer/CHANGELOG.md index 38c0b0feef..068e06c8cb 100644 --- a/packages/plugin-designer/CHANGELOG.md +++ b/packages/plugin-designer/CHANGELOG.md @@ -1,5 +1,962 @@ # @object-ui/plugin-designer +## 17.7.0 + +### Minor Changes + +- 864154e: The Field Designer no longer offers a formula-expression textarea, and no designer write + path emits a `formula` key (objectui#6043). + + **This is a behaviour change on an authoring surface: a control is removed.** A field's + `type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the + palette — but the expression itself is no longer authored here. Authors write formula + expressions in metadata-admin's field inspector, where they are checked. + + The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against + the installed `@objectstack/spec` 17.2.0: + + ``` + FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) + => success = false + => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?" + ``` + + so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because + the key was then stored, it blocked **every later save of that object**, not just the one + that introduced it. + + **The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges + the key name and never the expression LANGUAGE — measured, it accepts + `expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the + empty string is refused. Spec `expression` is CEL rooted at `record` + (`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity` + — bare field refs, which under the scope formulas bind evaluate to null silently. A rename + would therefore have converted a loud, immediate 422 into a formula that saves clean and + then quietly computes nothing, which is strictly worse than the bug it appears to fix. + + Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType` + inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and + app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a + dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a + feature, not this fix. `returnType` is likewise not authored here: it is only derivable by + inferring the CEL result type, and with no expression control there is nothing to infer + from. + + `formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already + carrying the key is stripped clean on its next save instead of staying blocked forever — + which matters more than usual here, because with the control gone an author would + otherwise have no way left to clear it. It is dropped rather than migrated to `expression`, + for the same reason the rename was refused. A `expression` authored in metadata-admin is + **not** touched: it is a real `FieldSchema` key and rides through the designer's + round-trip untouched. + + Also removes the now-unreachable `formula` read/write from + `views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for + the key that neither the card nor the parity gate named. + + The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s + `KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that + left a stale entry behind would be as red as a new offender. +- d18a0d3: Object-level metadata payloads no longer emit the three keys `ObjectSchema` refuses by + name — **group**, **sortOrder** and **relationships** (objectui#6223). + + Measured against the installed `@objectstack/spec` 17.2.0, whose `ObjectSchema` accept set + is 42 keys: + + ``` + const base = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } }; + + ObjectSchema.safeParse(base) => success = true (control) + ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control) + ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control) + + ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"] + ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"] + ObjectSchema.safeParse({ ...base, relationships: [ … ] }) => unrecognized_keys ["relationships"] + ``` + + The two controls are what make that a key-by-key result rather than a schema refusing + everything. Each key was resolved on its own, as the objectui#5761 family ruling requires: + + - **group** — the Object Manager's grouping is a UI-only display category. The spec has no + object-level grouping key (`fieldGroups` groups the fields *inside* one object), so the + grouping control and its column stay, and the value is now DERIVED from the spec key that + is accepted (`isSystem`) instead of round-tripped. `MetadataObjectsPage` also strips a + `group` already stored by an earlier build, because its save-back spreads the server + document verbatim and would otherwise keep re-sending it forever. + - **sortOrder** — what populated it was the array index the converter happened to be at, + i.e. the order the list was already in. The declaration is removed from the object + payload. The field-level `sortOrder` is a different key with a different card + (objectui#6045) and is untouched. + - **relationships** — the spec models relationships on the FIELD (`reference` / + `master_detail`, plus object-level `indexes`). The object payload stops declaring and + sending an object-level relationship array; what the designer should author for a + relationship is a data-model question this change does not settle. + + **Breaking for TypeScript consumers of `ObjectMetadataPayload`** (exported from app-shell): + the three properties are gone from the published type, so code that set them stops + compiling. That is the point — setting any of them produced a payload the metadata route + refuses. `ObjectDefinition` (the designer's UI model) is unchanged and still carries all + three. + + The parity gate built for objectui#5761 now has a **second oracle**: every shape in + `PAYLOAD_SHAPES` names the schema that judges it, `ObjectSchema` alongside `FieldSchema`, + and reach is resolved within an oracle rather than across one — `group` is a legal + `FieldSchema` key and a refused `ObjectSchema` key at the same time. That extension found a + fourth object-level key (`enabled`, objectui#6238) and a value-level rejection the key-name + check cannot see (`fields` sent as an array where the spec wants a map, objectui#6240); + both are filed and ledgered rather than fixed here. +- 1cca678: Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, + maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). + + **What changes for an operator.** A stored dashboard whose header came from a legacy + root `title` now shows its `label`. `label` is the only header source, then the raw + `name`. + + Per surface: + + - Console dashboard page (`DashboardView`) — header falls to `label`, then `name`. + - Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document + with no `label` now shows no header title at all. + - The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to + `label`, then the generic `Dashboard`. + - Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage` + heading) — both fall to `label`, then `name` / the generic heading. + + **Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name** + (`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no + authored dashboard can acquire the key, and what retires is compatibility with documents + stored before that refusal existed. Until now five surfaces read the legacy spelling + independently, which meant a legacy document could show one header in the console and a + different one in the designer. One spelling now answers everywhere. + + **Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard + already carries it and needs no change — it simply starts showing that `label` instead of + the legacy `title`. A document carrying `title` and no `label` was already invalid; give + it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found + 9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'` + component examples that declare no `header`, so none of them rendered a header title + either before or after. + + **Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared** + key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings, + the designer's widget-title input and its per-locale write path all behave exactly as + before. Root and widget arms were separated by receiver, and the retirement's pins carry + widget-level controls on every surface for that reason. +- bdb4dbd: A half-filled relationship field stays in the client and is never PUT (objectui#7714, + maintainer ruling on objectui#7122 item 4, 2026-09-05). + + **Breaking, deliberately, and stated rather than implied.** Both metadata writers — + `MetadataService.saveFields` / `saveObject` in `@object-ui/app-shell`, and + `MetadataFieldsPage` in `@object-ui/plugin-designer` — now REFUSE a `lookup` or + `master_detail` field whose `reference` is missing, empty, blank or not a string. A + caller that previously got a PUT now gets a thrown error and **no request at all**. The + refusal is raised while the wire `fields` map is being built, so nothing is sent and + nothing is partially applied. + + **Why the draft may not leave the client.** `@objectstack/spec` 17.3.0 turned + `reference` from prose into a hard requirement on relationship types (a `custom` + refinement at path `reference`). Driven against a real 17.3.0 backend in a running + designer: creating a `lookup` and leaving its target empty PUT the whole object, + came back `422 INVALID_METADATA` at `fields.<name>.reference` — and then the NEXT + edit, to a different and already-saved field, was refused identically, because the + half-filled draft rides along inside the same document. The author sees that later + edit rendered as applied while the server has none of it, and the only escape that + does not require noticing the lookup is a reload, which discards the work. An editing + session's half-finished state belongs to the client, not to the metadata store. + + ⛔ **Not** "strip the incomplete field from the body and report a successful save". + That shows the author a field the server never received — the silent-drop shape + objectstack#4001 closed. + + ⚠️ **A declared divergence: objectui is stricter than the contract here.** The + predicate is `typeof reference === 'string' && reference.trim() !== ''`. Measured on + the 17.3.0 artifact, at field level and again through `ObjectSchema`, the spec + ACCEPTS a whitespace-only `reference` while refusing an absent or empty one — so + `reference: ' '` is refused by these writers on their own authority, not the + platform's. Kept deliberately: whitespace names no object (the spec's own + `ObjectSchema.fields` key grammar `/^[a-z_][a-z0-9_]*$/` admits no whitespace-bearing + name), so admitting it only moves the identical failure past the PUT and into a stored + document, where it surfaces with no field named. Filed upstream as objectstack#16126; + if the spec trims, behaviour here is unchanged and only the declaration retires. + + The refusal message diagnoses which of the four states it found — absent, empty, + non-string (`invalid_type`, a value of the wrong kind rather than a missing target), + or blank — because the repair and the consequence differ per state. + + `minor` rather than `major` per `AGENTS.md`: objectui's major tracks `@objectstack`'s, + so objectui's own breaking changes ship as `minor` with the breaking semantics stated. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 2cf69e4: The field designer now reads and writes a lookup field's relationship target under the + spec's spelling `reference` (objectui#6041), in both directions. + + `referenceTo` is not in `FieldSchema`'s accept set. Measured against the installed + `@objectstack/spec` 17.2.0, through the whole object document that + `PUT /api/v1/meta/object/:name` validates: + + ``` + ObjectSchema.safeParse({ …, fields: { rel: { type: 'lookup', label: 'Owner', + referenceTo: 'user' } } }) + => success = false + => unrecognized_keys at ["fields","rel"] keys=["referenceTo"] + "Did you mean `referenceTo` -> `reference`?" + ``` + + so authoring a lookup field through the designer returned a hard 422 `INVALID_METADATA`, + and — because the key is then stored — blocked **every subsequent save** of that object, + with nothing in the UI to say which key did it. + + The read direction was broken symmetrically and is the half that would have survived a + write-only fix: `toDesignerField` read `raw.referenceTo` while a spec-parsed server sends + `reference`, so every already-saved lookup field loaded into the designer with an **empty + reference box**. Both wire-bound payload shapes move — `FieldMetadataPayload` + (`MetadataService.toFieldPayload`) and `ServerFieldSchema` + (`MetadataFieldsPage.fromDesignerField`). + + `referenceTo` also joins `RETIRED_FIELD_KEYS`. Renaming the emit sites alone does not + unblock an object whose stored fields already carry the misspelling: `carryOver` spreads + the previous server def verbatim, so the key would ride straight back out to the same 422. + The designer's in-memory `DesignerFieldDefinition` keeps `referenceTo` — that is the + internal prop name every other UI surface in this repo already uses (`LookupField`, + `filter-builder`, `ObjectChart`, `ListView`, `UserFilters`), it reaches no wire-bound + shape, and the parity gate classifies it as `uiOnly` rather than a violation. + + No behavioural change for a half-filled draft: the spec's prose calls `reference` + "required for relationship types", but that is not enforced by the zod parse at 17.2.0 — + `{ type: 'lookup', label: 'L' }` parses green at field level and through `ObjectSchema`, + and `undefined` is dropped by `JSON.stringify` under either spelling, so the wire bytes + are identical before and after. +- 2cf69e4: The field designer now reads the system-field marker under the spec's spelling `system`, + and never hands `isSystem` back to the metadata API (objectui#6044). + + `isSystem` is not in `FieldSchema`'s accept set. Measured against the installed + `@objectstack/spec` 17.2.0: + + ``` + FieldSchema.safeParse({ type: 'text', label: 'L', isSystem: true }) + => success = false + => unrecognized_keys keys=["isSystem"] "Did you mean `isSystem` -> `system`?" + ``` + + Two defects, one misspelling, and they are two different sites. + + **The read was dead** — the quieter and worse half. `toDesignerField` read `raw.isSystem` + while a spec-parsed server sends `system`, so the flag was always `undefined`. Nothing went + red, because the flag is optional and `undefined` is a valid "not a system field". But it is + load-bearing: `FieldDesigner` refuses to delete a system field and disables its name and + type inputs, so with the read dead `organization_id`, `created_at` and friends presented as + ordinary editable, **deletable** business fields. + + **The write had no emit site at all.** `fromDesignerField` never names `isSystem`; its only + route out is the verbatim `...carryOver(prev)` spread, so a stored misspelling round-tripped + back to `PUT /api/v1/meta/object/:name` as a hard 422 `INVALID_METADATA` that blocks every + later save. The repair is a `RETIRED_FIELD_KEYS` tombstone rather than a renamed line — and + it is deliberately paired with the read fix, never a substitute for it: stripping alone would + close the 422 and fossilize the dead detection. The spec spelling `system` is not stripped, + so a server-injected flag rides through untouched and feeds the read. + + `app-shell`'s `FieldMetadataPayload` never declared the key, so `toFieldPayload` had nothing + to fix. The designer's in-memory `DesignerFieldDefinition` keeps `isSystem`: it reaches no + wire-bound shape and the parity gate classifies it as `uiOnly`. +- 00c665e: `appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten + locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310). + + objectui#6043 retired the Field Designer's formula-expression textarea, which was the + key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label: + t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in + eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in + `packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner > + fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. + + Removed under objectui#4658's evidence standard, re-measured on this branch rather than + inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could + reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its + sole textual occurrence anywhere in the repo was the defaults-map row this change removes + with it — so the key goes from NEEDS-REVIEW to no footprint at all. + + The map and all ten packs move in one commit, which is what keeps + `defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack + lacks, and `all-locales-key-parity` fails a pack left behind. + + Not touched: `designer.field.formula` (`'Formula (CEL)'`) in + `packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging + to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula + expressions. + + `packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the + removal by name, following the four prior retirements (objectui#4145, objectui#4392, + objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can + see a dead key come BACK into the packs: the reverse sweep that found this one is report-only + by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and + `check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted — + reviving the row in all ten packs turns exactly that one case red, naming each pack, while the + parity gate and the defaults-map mirror stay green. +- 73e8c33: The Field Designer builds an object's `fields` map by defining own properties and refuses + the three field lists a name-keyed map cannot carry (objectui#6489). `MetadataFieldsPage` + keyed the map by blind assignment — `nextFields[f.name] = fromDesignerField(…)` inside a + bare `for` loop — which failed silently in three directions, all measured on the installed + `@objectstack/spec` 17.2.0: + + - **A field named `__proto__` never reached the wire.** `map['__proto__'] = def` invokes the + prototype setter instead of creating a key, so the field vanished from the serialised PUT + body. `__proto__` matches `ObjectSchema.fields`' key rule `/^[a-z_][a-z0-9_]*$/`, so the + spec stood ready to accept the field the client had thrown away. The map is now built + through `Object.fromEntries`, which defines an own property. + - **A nameless field was stored under the literal key `"undefined"`.** Measured: + `ObjectSchema.safeParse` with `fields: { undefined: … }` returns `success = true`, so the + document parsed, persisted, and had no reader anywhere. It is now refused before the + request. + - **Two fields sharing a name collapsed into one entry.** A designer list carrying two + `amount` fields PUT a single entry, the later silently replacing the earlier. Also refused + before the request. + + Both refusals raise before `client.save`, so a refused list issues no PUT at all, and the + message lands in the page's existing error surface naming the offending index — the caller + is fire-and-forget (`void handleFieldsChange(next)`), so throwing past it would show the + author nothing. + + This is the plugin-designer port of the refusals objectui#6240 landed in the sibling object + writer (app-shell's `MetadataService.toFieldsMap`), down to the wording, so the two writers + of the objectui#5761 parity family cannot drift. `fromDesignerField`'s carry-over semantics + are untouched. +- b49f9a1: `MetadataObjectsPage` keys its object-name lookups as own entries, so deleting an object + named `constructor` (or `__proto__`) from the Object Manager actually deletes it + (objectui#6522). + + Both name lookups in the page were plain object literals filled by assignment, and the + consequential one was a READ. The delete scan asked `!nextByName[name]`, which for an + object named `constructor` answered out of `Object.prototype` with the `Object` function — + truthy — so the deletion read as "still present" and `client.reset('object', …)` never + fired. Not a refusal: the row disappeared from the manager, no error was shown, the save + reported success, and the object was still there after the next reload. Measured against + the installed `@objectstack/spec`, `ObjectSchema` pins object names to + `/^[a-z_][a-z0-9_]*$/` and accepts both `constructor` and `__proto__` — those two are + exactly the intersection with `Object.prototype`'s own names, so both are storable and + neither was deletable. + + The second lookup, one function over, failed on the WRITE instead: `byName[item.name] = + item` for an object named `__proto__` invoked the prototype setter rather than creating a + key, so the object never became an own property, never reached the Object Manager at all, + and left its payload on the lookup's prototype chain for later name lookups to answer out + of. Both are now `Map`s — neither container is ever serialised, only its values are, so a + `Map` fits where the sibling `MetadataFieldsPage` fields map (which IS the PUT body) needs + `Object.fromEntries`. + + Keying only. Nameless and duplicate entries behave exactly as before: this page writes + per-object, so the refusal semantics objectui#6489 added to the fields map are a separate + question and are deliberately not ported here. +- 3e028c8: One tombstone registry for the designer seam's retired field keys + (objectui#6527). Three independently maintained `RETIRED_FIELD_KEYS` literals + — the metadata-admin read door (`object-fields-io.ts`), `MetadataService`'s + carry-over and `MetadataFieldsPage`'s carry-over — become derivations from a + single registry in `@object-ui/types` (`RETIRED_FIELD_KEY_TOMBSTONES` + + `retiredFieldKeysFor(site)`), naming each retired key, the card that retired + it, and its PER-SITE applicability. + + Per-site behaviour is unchanged — this is a consolidation, and each site's + effective strip set is pinned equal to its pre-consolidation literal. The two + deliberate asymmetries a naive union would have destroyed are now recorded as + data and pinned: + + - `formula` stays stripped by the two write-side carry-overs and is NOT + stripped by the read door — ruled on objectui#6526 (option B): the + `ObjectFieldInspector` migration path (objectui#6043) stands, and the + registry test makes that ruling mechanical. + - `sortOrder` stays a single-site strip at `MetadataService`'s carry-over, + now explicitly recorded as the registry's one DEFENSIVE entry (objectui#6045 + measured that no shipped writer ever populated a field-level one). +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 4db5989: A widget title stored as an inline per-locale map is editable again in both dashboard + authoring surfaces, and a save writes back only the active locale's entry + (objectui#5428). + + `@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at + 17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both + authoring panels edit a title in ONE single-line input. Writing the input's value back + as the whole value would collapse every other locale on the first keystroke, so both + surfaces took the same conservative branch: show a map-valued title resolved, and make + it READ-ONLY. + + That branch could not lose data, but it rested on a premise the spec had already + invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was + plain `string` through rc.5" — stated sixty lines below a comment in the same file + documenting the rc.6 widening that makes a stored map reachable. Both could not hold. + The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward + was not protect an unreachable path: it denied an author the ability to edit a widget + title in their own locale. + + objectui#5301's maintainer ruling settled the write rule for the sibling surface — a + save replaces only the active locale's entry and preserves the others — and + `@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because + the read and the write have to agree. Both panels now adopt it: + + - `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; + - `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. + + A plain-string title keeps saving as a plain string, so the common path is unchanged. + An edit made in a locale the stored map does not carry ADDS an entry under that locale + rather than overwriting the entry the display fell back to. + + The pins are preservation pins, not "the input is editable" pins: at both surfaces a + keystroke on a map-valued title must leave every other locale's entry byte-identical. + Reverse-verified by mutating each write back to the flattening form and confirming those + assertions go red at both surfaces. + + Not a multi-locale editor: an author still reaches only the entry for the locale they + are in. Authoring every locale from one panel remains an open product question. The + stale deferrals both comments carried pointed at objectui#4163, which closed as + completed on 2026-08-15 while the placeholders were still in the tree; they are replaced + with the rule that is actually in force rather than re-pointed at another tracker. +- f7e34ca: Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectui's own component extension. + + `DashboardWidgetSchema.type` was `string` on the TypeScript interface and `z.string()` in the Zod twin — an unbounded hatch. A typo'd family, a chart type the spec retired, and a component type nothing registers all type-checked and validated, surfacing only as the renderer's red `OBJUI-001` panel at runtime. + + It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum). + + Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one. +- Updated dependencies [9c74902] +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [062943f] +- Updated dependencies [993f312] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [67a87d9] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [94e2fa7] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [3e853c9] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [17ccec9] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [beccf1c] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [7a72422] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [5173a5e] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [c9a7252] +- Updated dependencies [5f19b92] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [e0b289d] +- Updated dependencies [46f0bb4] +- Updated dependencies [2da6441] +- Updated dependencies [3b9c774] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [c38162d] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [5127378] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [1c19722] +- Updated dependencies [5961030] +- Updated dependencies [faa863d] +- Updated dependencies [fd814d6] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [c4987fb] +- Updated dependencies [f55d666] +- Updated dependencies [f241a4d] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [4ac3769] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [19f3637] +- Updated dependencies [9bd08fe] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [fab4802] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [f07b976] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [f9984c0] +- Updated dependencies [9700dd9] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [1349400] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [84ffdbc] +- Updated dependencies [a276480] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [67dadd6] +- Updated dependencies [e21308e] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [6a99bb2] +- Updated dependencies [2d3fe73] +- Updated dependencies [ed27d7c] +- Updated dependencies [7dedec6] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [53ded82] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [f57ca75] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [b2e85a9] +- Updated dependencies [d5c1f52] +- Updated dependencies [c7cd2b6] +- Updated dependencies [b2065e7] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [b470e91] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [425762e] +- Updated dependencies [0fce2ef] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [9850c6e] +- Updated dependencies [9118a31] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [591bf27] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [43ca9d5] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [ba306e3] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [5d79faf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [f75810e] +- Updated dependencies [4d73b07] + - @object-ui/plugin-form@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/data-objectstack@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index c92439fe2a..96867df983 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-designer", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Visual designer plugin for Object UI with page, data model, process, and report designers plus collaborative editing.", diff --git a/packages/plugin-detail/CHANGELOG.md b/packages/plugin-detail/CHANGELOG.md index 1076d9b139..bc2de9651d 100644 --- a/packages/plugin-detail/CHANGELOG.md +++ b/packages/plugin-detail/CHANGELOG.md @@ -1,5 +1,2304 @@ # @object-ui/plugin-detail +## 17.7.0 + +### Minor Changes + +- d796c8d: Auto-derived related lists consume the field-level `relatedListFilter` + declaration — the list query AND-composed, the tab badge counting the same set + (objectui#4664). + + A `lookup` / `master_detail` field may now declare `relatedListFilter`, a + canonical Query-DSL `FilterCondition` such as `{ status: { $ne: 'deleted' } }` + (`@objectstack/spec` 17.1.0 — objectstack#8704 / PR #8955). Until now this repo + accepted that key at every gate and consumed it nowhere: a record page's derived + related lists answered wider than the metadata asked, and the driving scenario — + soft-deleted child rows on auto-derived record pages — had no way to be + expressed at all. + + What ships: + + - `deriveRelatedLists` reads the key off the FK and carries it on the derived + descriptor; `RecordDetailView` forwards it into the page synthesizer, which + emits it onto the `record:related_list` node's **existing** `filter` key. That + key already had a read site (objectstack#7118): `RelatedList` ANDs it with + `{ [referenceField]: parentId }`. The declared filter is therefore an authored + constraint that may only NARROW this parent's children — never a replacement, + which would leak other parents' rows — and no second filter dialect appears + for derived pages. + - The **tab badge honours the same composed filter**. `page:tabs` reads the + `filter` off the `record:related_list` node it is badging and the count store + composes it with the parent scope through the same `mergeFilterNodes` sink the + row query uses, so the badge and the rows send one `$filter`. Badge-count + parity is normative in the spec key's own contract text; without this half the + feature would ship the defect it exists to prevent — a badge saying 7 above a + list showing 3. + - Counts cache per (object, relationship, parent, **scope**), so a filtered and + an unfiltered probe over the same relationship are separate entries rather + than one wrong number. + + With no `relatedListFilter` declared, the synthesized node, the row query and + the badge probe are byte-identical to before. Consumption only — this change + adds no authoring UI for the filter. +- 6d63cd0: An auto-derived related list now orders its rows by the CHILD object's default list view + `sort`, instead of falling to the server's primary-key order (objectui#5795). A task + version's "check items" tab whose child object declares `sort: [{ field: 'seq_no' }]` + renders 10/20/30/40; before this it rendered whatever order the ids happened to give — + 20/30/10/40 in the reported case — while the child object's own list page obeyed the + declaration. + + **Declared as user-visible, deliberately, even though no key was added.** The contract + question ("where does a derived related list's sort declaration live?") was ruled on + objectstack#11345 (maintainer, 2026-08-23) as **direction 1**: inherit the child's list + view sort, and add **no** new spec key — the field-level `relatedListSort` the issue also + proposed was explicitly not approved. So there is nothing new to author, and + `record:related_list.sort` was already declared, parsed and consumed; this fills it. What a + host observes is nonetheless new: a derived related-list descriptor gains a populated + `sort` where it had none, and the query it issues gains an `$orderby`. An app whose child + objects declare a default list order will see those tabs re-order on upgrade — which is the + point of the change, and is why this is not a patch. + + Nothing is inherited where nothing was declared: a child object with no default list-view + sort produces the same descriptor, the same node and the same `$orderby`-free query as + before. + + The two `sort` surfaces declare the same union and mean different things by its string arm + — a `ListView` string is the legacy space-separated `'seq_no desc'`, while the related + list's own reader takes `'field'` / `'-field'` — so the inherited value is normalized to + the array arm once, at the derivation, through `@object-ui/core`'s + `convertSortToQueryParams` (the repo's single definition of both authored dialects). An + un-normalized inherit would have ordered by a field literally named `seq_no desc`. + + Known and unchanged: `$orderby` is only assembled while the related list is in windowed + (server-paged) mode, so a declared *or* inherited sort still disappears while the built-in + client text filter is active. That hole pre-dates this change and affects the authored prop + identically; it is now pinned as a recorded fact in + `plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx` rather than + fixed here. +- 4da5109: One `sys_activity` row → `FeedItem` constructor, and the console record page + stops dropping author-extended activity types in silence (objectui#5896). + + **The defect.** `RecordDetailView`'s `sys_activity` merge read the shared type + table (objectui#5878) and then built the `FeedItem` itself, ending in + `if (!feedType) continue;`. That one `continue` collapsed two different + situations: a type the table maps to `undefined` **on purpose** (`commented` / + `mentioned` / `login` / `logout`), and a type the table has never heard of. The + second is an **author-extended** value — `sys_activity.type` is + author-extensible (objectstack#11507 direction 4, ruled 2026-08-24), every + column on that table is `readonly` so objectql never validates a write, and + ADR-0052 §5b.2 forwards an author's `activityMilestones[].type` into it + verbatim. So an activity that happened, was written and is queryable had no row + on the console record page: no placeholder, no empty state, no console message. + Stored, queryable, invisible — objectui#5840's failure mode reached by another + route, and one objectui#5969 (PR #6112) had already removed from the block + side, leaving the two surfaces disagreeing about the same row of the same table. + + **The fix is convergence, not a second decision.** `@object-ui/plugin-detail` + now exports the whole reading — `activityRowToFeedItem`, + `UNMAPPED_ACTIVITY_FEED_TYPE` and the `resetUnknownActivityTypeWarnings` test + seam alongside `ACTIVITY_TYPE_TO_FEED_TYPE` — and `RecordDetailView` calls the + constructor instead of paraphrasing it. Publishing the table alone had left the + mirror one level up, and it had already drifted three ways: the silent drop, a + timestamp fallback that could leave `createdAt` `undefined` where the helper + yields `''`, and a second hand-written system-actor lookup. + + **Behaviour change on the console record page** (breaking in the objectui sense, + shipped `minor` — objectui's `major` tracks `@objectstack`): + + - an unmapped `sys_activity.type` now **renders** through the generic + `UNMAPPED_ACTIVITY_FEED_TYPE` (`'system'`) presentation instead of vanishing, + and is announced once per distinct value on `console.warn` — a **missing + decision**, not lost data: the row is visible, what it lacks is its own icon + and colour. `FeedItemType` is a closed spec enum, so minting a kind for "we + don't know" would be a platform change, not this surface's. + - `createdAt` is always a string for a row with neither a usable `timestamp` + nor a `created_at`. + + **Unchanged, deliberately:** the four exclusions still produce no row and no + warning. They are decisions — comment content lives in `sys_comment`, and + login/logout are account events rather than record activity — and a warning + about a decision teaches authors to ignore the channel. +- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). + + Seven modules resolved authored icon names into lucide's runtime `icons` record, each + with its own copy of the logic: **three different tokenisers** (`split('-')` on five of + them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` + rename on only **four** of the seven. The same authored name therefore rendered on one + surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened + with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, + and the other six call it. + + **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it + was measured rather than chosen.** Its regression set is empty three independent ways: + against the authored population, against a maximally-pessimistic every-authored-name x + every-surface cross-product, and against a bound-free differential over 8,298 spellings + derived from all 1,767 live record keys — each with a discrimination control that fired + in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface + pairs in that last reading, stripping two surfaces of every snake_case and + space-separated spelling they resolve today. + + **What changes for you — all of it widening, none of it removal.** No name that resolved + before stops resolving: no key of lucide's record contains `_`, whitespace or `-` + (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the + wider one produces the same key. Sixteen name-surface pairs start resolving where they + rendered a fallback or nothing before: + + - `layout_dashboard` and `building_2` (and every other snake_case or space-separated + spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, + `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and + the related list. + - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried + no rename map. `Home` is not a live record key, so this could only ever be a widening. + + **What does NOT change: what each surface draws when a name does not resolve.** The seam + answers `name -> component`, returning `null`, and decides nothing else (maintainer + ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at + the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning + (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, + `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` + and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and + then dropped once the tree was measured to have four such behaviours rather than two: a + lookup function is the wrong place to publish a presentation decision. + + `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface + this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: + its census goes from seven sites to one, and its normalisation stops being a + widest-common approximation of three disagreeing resolvers — so the under-reporting that + gate disclosed at objectui#5932 is closed rather than merely bounded. +- 1e7fe0a: `record:details` section headings converge on the declared `label` slot; the + `title` alias limb is gone (objectui#6190, maintainer ruling 2026-08-31 — + option A, three producers plus the consumer in one change). + + **Breaking for anyone reading `deriveFieldGroupDetailSections`' output — the + emitted key moves from `title` to `label`.** That function is public API + (exported from `@object-ui/plugin-detail`), so this is an output-shape change, + not an internal refactor. A caller that reads `section.title` off its return + value reads `undefined` after this release and must read `section.label`. + `BuildPageOptions.sections` and `ObjectDefLike.sections` declare the same move, + so a caller passing `sections` into `buildDefaultPageSchema` supplies the + heading as `label` too. + + **The retired authoring spelling is `title` on a `record:details` section.** + `RecordDetailsRenderer` read `s.title ?? s.label` — a strict-priority second + spelling of one slot, with byte-identical localization on both limbs, so a + producer emitting both silently disagreed with itself and `title` won. It now + reads `label` only. Nothing an author could publish is affected: `@objectstack/spec` + REFUSES `title` inside a `sections[]` entry (`unrecognized_keys`, pinned by + objectstack#11902), and `@object-ui/types` plus the authoring inspector have only + ever declared `name` / `label` / `columns` / `fields`. The declared and authoring + faces were already converged; only three runtime producers lagged, and all three + move here: + + - `buildDefaultPageSchema`'s `deriveFieldGroupDetailSections` section literal; + - `RecordDetailView`'s re-map of that output through the per-object i18n + convention; + - `RecordDetailView`'s auto-grouped "More details" section, authored in + app-shell and unreachable from the synthesizer. + + **No rendered heading changes.** Every existing producer yields byte-identical + headings, asserted by rendering rather than by inspection — including the + "More details" bucket, which the earlier two-step scope would have degraded to + the literal `details` in every shipped locale while every existing guard stayed + green. A new pin (`RecordDetailView.sectionHeadingsRenderPath-6190.test.tsx`) + walks the tree app-shell actually renders and closes that blind spot. +- 3777538: `isConcurrentUpdateError` no longer promises a `code` its runtime check + knowingly accepts values without. + + The exported predicate matches on two limbs — `code === 'CONCURRENT_UPDATE'` + (the wire shape) **or** `name === 'ConcurrentUpdateError'` (the deliberate + cross-realm discriminator, for a host that bundles the adapter twice so + `instanceof` fails). Its narrowed type declared `code: 'CONCURRENT_UPDATE'` as + a **required** literal, so for exactly the case the second limb exists to serve + the predicate handed the caller a property the value does not have. `code` is + now declared optional (`code?: 'CONCURRENT_UPDATE'`), stating only what both + limbs guarantee. + + **Type-level only; zero runtime change.** The two-limb check is byte-identical + — it is deliberate and documented, and the diff touches nothing but the return + type and the comment above it. There was no runtime symptom to fix: the only + consumer (`InlineEditSaveBar`'s conflict builder) reads just the optional + `currentVersion` / `currentRecord` fields. + + **Breaking for TypeScript consumers that read the narrowed `code`.** After + narrowing, `err.code` is now `'CONCURRENT_UPDATE' | undefined` instead of + `'CONCURRENT_UPDATE'`, so code that assigned it to a non-optional `string` will + newly fail to compile. That failure is the point: on a name-only error the + value was already `undefined` at runtime, and the old declaration was the + reason the compiler could not say so. Guard the read (`err.code === + 'CONCURRENT_UPDATE'` still narrows fine) or branch on the predicate itself + rather than on the field. + + Pinned by `ConcurrentUpdateDialog.narrowedCode-6421.test.tsx`, which asserts + the invariant by assignability — the value the runtime accepts through the + `name` limb must be assignable to the type the predicate returns — alongside + runtime coverage of both limbs, which `plugin-detail` had none of. +- b97790a: Seven more `find()` readers now read exactly what `QueryResult` declares — the + `records` arm is removed from each (objectui#6726, following objectui#5945). + + `QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` — + alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`. + `records` is not a member of it. It is the spelling the server envelope and the + client SDK use, which `ObjectStackAdapter.normalizeQueryResult` maps to `data` + before returning — a *below*-the-adapter spelling that had leaked into + above-the-adapter consumers. objectui#5945 removed it from two app-shell + readers; these are the seven the same producer sweep turned up and that card did + not name: + + | module | what it does | + | --- | --- | + | `components/src/hooks/related-count-store.ts` | related-list tab badge count | + | `components/src/renderers/basic/data-list.tsx` | `element:repeater` rows | + | `components/src/renderers/basic/elements.tsx` | `element:number` client-side aggregate | + | `components/src/renderers/basic/record-picker.tsx` | `element:record_picker` options | + | `plugin-detail/src/renderers/record-activity.tsx` | `record:activity` self-fetch | + | `plugin-detail/src/renderers/record-history.tsx` | `record:history` self-fetch | + | `plugin-view/src/ObjectView.tsx` | non-grid (kanban / calendar / gallery / timeline) fetch | + + **One of them was actively wrong, six were dead.** `related-count-store.ts` + read `records` *ahead of* `data` — the precedence inversion objectui#5945 was + filed about — so a `find()` answer carrying both would have been counted from + the key the contract does not declare. The other six read `data` first, so their + `records` arm could never be reached by a conforming producer. A dead tolerant + arm is not harmless: it is where a non-conforming producer keeps working + unrejected, and hardens into a second de-facto contract nobody is checking + (AGENTS.md #0.1). + + **What stops being accepted.** A `find()` answer shaped `{ records: [...] }` + now reads as **no rows** at these seams instead of silently resolving. Every + call site degrades rather than throws: the tab badge counts 0, the repeater and + the picker render their empty state, `element:number` reports 0, the activity + and history feeds render empty, and the non-grid views paint no rows. + + **Nothing produces that shape at this seam today**, which is why this is a + removal rather than a migration. Measured repo-wide over every tracked file: + `ObjectStackAdapter.normalizeQueryResult` CONSUMES the server/SDK `records` + envelope and returns `{ data, total, page, pageSize, hasMore }`; every other + `find()` implementation in the repo (`ApiDataSource`, `ValueDataSource`, the + runner and example mocks, the `@object-ui/types` REST example) returns `data` + or a bare array. The `records` producers that DO exist are on other seams and + are untouched: `ViewDataProvider`'s own `ResolvedData` interface, which declares + `records` legitimately; the raw Cloud HTTP payloads `marketplaceApi.ts` and + `packagedActions.ts` read; and the client-SDK doubles that sit *below* + `normalizeQueryResult`. + + **The bare-array arm is kept** wherever it existed, because it is live: fakes at + these seams answer with a plain array. Each module carries its own pin — + `*.contractEnvelope-6726.*` — asserting the contract read, the live arms, and + the refusal of `records`, so the live and the dead shapes cannot drift into each + other. + + `QueryResult` is **not** widened to bless `records`; that would be a + published-type change and a maintainer decision. +- 9409eb9: `RecordDetailDrawer` resolves a relationship target only from the two spellings + a contract carries, dropping the two no contract declares (objectui#6837, first + slice). + + The chain was `def.reference_to ?? def.reference ?? def.referenceTo ?? + def.target`; it is now `def.reference_to ?? def.reference`. + + **Accept-set move — a def carrying ONLY `referenceTo`, or ONLY `target`, stops + resolving a target** and the field renders without one (the drawer already marks + every reference-bearing field readonly, so nothing becomes editable that was + not). Two things bound that: + + - Any def that entered through the ingestion choke point is unaffected. + `normalizeSchemaReferenceKeys` reads `reference_to ?? reference ?? + referenceTo` and stamps both snake_case keys, so a `referenceTo`-only def + arriving via `MetadataProvider` or `ObjectStackAdapter.getObjectSchema` + already carries `reference_to` before the drawer sees it. Only a def that + bypassed that door entirely is affected. + - `target` was never read anywhere else in the stack — not by the normalizer, + not by the spec. `@objectstack/spec`'s `FieldSchema` refuses both deleted + spellings by name with `unrecognized_keys`, each carrying its own "did you + mean `reference`" rename, and `referenceTo` is additionally stripped at the + designer read door (`RETIRED_FIELD_KEYS`, objectui#6041 / #6519). + + A repo-wide structure-walk producer census found **0** emitters of `target` and + **0** reaching this seam for `referenceTo`, measured in the cell the drawer + reads (a value inside an object schema's `fields` container) against controls + `reference` (92 hits / 36 files) and `reference_to` (52 / 36) hot in the same + pass over the same cells. + + Pinned by `RecordDetailDrawer.referenceArms-6837.test.tsx`, which keeps the live + arms green beside a named refusal per deleted key. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- eeb6c2f: **Behaviour change.** `record:details` no longer forces `hideEmpty` on the + sections it synthesizes, so a sparse record keeps its section skeleton instead + of collapsing. Applications relying on the old auto-hide of *unauthored* + sections will now see headings, field labels and empty-value placeholders where + rows used to vanish. This is the loud-over-silent direction, ruled by the + maintainer on 2026-08-31: an empty detail body is a platform concern, and a + metadata application should not have to author its way out of one. + + `RecordDetailsRenderer` mapped every authored section with + `hideEmpty: s.hideEmpty ?? true`. `DetailSection` already states the correct + rule in its own heuristic — *"If a section is entirely empty (e.g., loading + state, brand-new record), do NOT auto-hide — the labels themselves are useful + as a structural skeleton"* — and the forced default overrode exactly the case + that sentence reserves. On a hand-created record whole sections disappeared and + the body collapsed to a couple of rows; seeded demo data hid it. Every + application then had to hand-write `hideEmpty: false` per section to stop + looking broken, which is per-app tax for a platform defect. The renderer now + passes the authored value through untouched and lets the heuristic own the + default. + + What changes, precisely: + + - an **all-empty** section renders its heading, every field label and one + empty-value placeholder per field (it used to render nothing at all); + - a **small** partly-empty section — below `DetailSection`'s auto-hide + threshold of 4 fields / 25% empty (3 / 20% on mobile) — now shows its empty + rows; + - a **large** mostly-empty section with at least one filled row still + auto-hides, with the "Show N empty fields" toggle unchanged: the + label-graveyard guard is intact and this is not a return to dense-by-default; + - empty rows are now visible while inline-editing a section, so an unwritten + field can be filled in place. + + What does **not** change: an authored `hideEmpty` keeps its exact former + meaning. `hideEmpty: true` remains the explicit opt-in to hiding, and + `hideEmpty: false` remains what it always was — "not `true`", not an override + of the auto-hide heuristic (measured, and pinned as pre-existing). + + Reference-app hit inside this repo: the Studio metadata-admin page preview + (`PagePreview`) binds a real sample record, so a `record:details` block over a + sparse sample now previews the skeleton rather than a collapsed body. No + application metadata needs editing — that is the point of the change. +- 1f31d3a: **Retired: `DetailViewSection.hideEmpty`.** The `record:details` section key is + gone from `@object-ui/types` and `RecordDetailsRenderer` no longer reads it. + Emptiness on a detail section is now decided entirely by `DetailSection`'s + auto-hide heuristic — hide empty rows only while the section still has at least + one filled row, never on an all-empty section — with the reader's + "Show N empty fields" toggle as the escape hatch. + + **Minor, not major, and deliberately so.** The key was never authorable on any + validated page: `@objectstack/spec` `RecordDetailsProps` REFUSES it, returning + `unrecognized_keys: ['hideEmpty']` on the `sections[]` element (measured on the + installed 17.2.0, against a `columns: 2` control that parses and whose value + survives). So a spec-compliant document could not carry the key, and a document + that carried it anyway failed to parse before it ever reached the renderer. + What this release removes is a *declaration* that invited authors — and code + generators reading the published `.d.ts` — to write a key the platform refuses. + That narrows a published type surface, which is what makes it a minor rather + than a patch; it retires no capability anyone could exercise. + + One key had four contracts and three answers: `@object-ui/types` declared it, + `RecordDetailsRenderer` honoured it, the `DetailViewSectionSchema` zod mirror + omitted it, and the spec refused it. The maintainer converged the four on the + spec's answer (2026-09-01): the spec keeps refusing, the mirror stays absent, + and the declaration and the read are retired. All four are now pinned together + in `record-details.hideEmptyRetired-7129.test.tsx`. + + Going with it is the paradox the key carried: `DetailSection` tested + `!section.hideEmpty`, so an authored `hideEmpty: false` was indistinguishable + from an unauthored section and overrode nothing. There is no longer a lever to + misread. + + **Supersedes one paragraph of the `record:details` empty-section changeset in + this same release.** Its closing "What does not change: an authored `hideEmpty` + keeps its exact former meaning" no longer holds — an authored `hideEmpty` of + either polarity is now inert, and the release notes should read that way. + Everything else in it stands: the unauthored default is unchanged, and so is + the label-graveyard guard. + + **Migration:** delete `hideEmpty` from any `record:details` section you author. + A section that used `hideEmpty: true` to hide an all-empty block will now show + that block's skeleton — headings, field labels and one empty-value placeholder + each. That is the platform's answer for a sparse record, and it is a UI + decision, not something metadata should have to make. + + **Not affected**, despite the shared name: `record:reference_rail`'s own + `hideEmpty` prop, which is a different surface and still live; and the + `detail.hideEmptyFields` i18n label behind the toggle. +- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one + spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). + + **BREAKING, deliberately, with no deprecation window.** + + Two published contracts declared OPPOSITE dialects for the same four lookup keys, and + `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object + contract *refuses* outranked the one it *declares*: + + | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) | + |---|---|---| + | camelCase | **declared** | compile error (`TS2561`) | + | snake_case | refused (`unrecognized_keys`) | **declared** | + + `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the + snake members are **removed**: + + | before (removed) | after | + |---|---| + | `display_field` | `displayField` | + | `description_field` | `descriptionField` | + | `lookup_filters` | `lookupFilters` | + | `id_field` | `idField` | + + **Migration.** Rename those four keys wherever you author lookup or user field metadata + — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema` + that returns them. The old spellings are no longer read: a def still carrying + `display_field` falls back to the referenced record's generic name heuristic rather than + the field you named. + + `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name + hydration — committing a record field other than the id as the lookup's stored value — + which is picker behaviour with no `FieldSchema` twin, and none owed. + + **Not renamed** (outside this ruling's four keys, still snake on the widget bag): + `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`, + `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's + `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def. + + Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose + snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the + inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s + `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict + class; its gate is restated to assert the class no longer exists rather than passing + vacuously. +- b652514: Mixed id/object action arrays are refused; use all ids or all objects + (objectui#7182, maintainer ruling 2026-09-02, option C). + + An `actions` array on `page:header` or `record:quick_actions` (and the bar's + spec-declared `actionNames`) is either **all action ids** or **all inline + `ActionDef` objects**. A mixed `['convert', { … }]` array is now refused on both + surfaces: none of its authored actions is rendered, and the console names the + offending index (`… refused at index 1 — element 1 is an inline action object + but element 0 is an action id …`). Before this change the two renderers + disagreed on exactly that input — `page:header` normalised per element and drew + both halves, `record:quick_actions` switched on the whole array and rendered + nothing for the id — so one authored array meant two things depending on which + surface drew it, and the mixed form is precisely what a half-migrated page under + the objectstack#11592 ids ruling produces. + + **Breaking, deliberately, and narrowing.** `@objectstack/spec` has always + declared `PageHeaderProps.actions` as `z.array(z.string())` — the spec already + refuses an object element at validation, naming its index — and + `RecordQuickActionsProps` declares `actionNames` (ids) only. What narrows is + the renderers' undeclared tolerance: an all-object array still passes through + (transition tolerance for the migration, retired on its own card once the last + inline array is converted), a mixed one no longer does. **Migration:** convert + each array whole — every element an id naming an action declared on the + object — never one element at a time. + + New on `@object-ui/types`, beside `actionRendersAt`: the pure + `resolveDeclaredActionIds(elements, registeredActions)`, with the + `DeclaredActionsResolution` / `DeclaredActionsRefusal` result types (the shape + classifier stays module-internal: called with no registry, the function already + returns the registry-independent verdict a renderer needs before its lookup). Both + renderers call it; the whole-array switch in `record-quick-actions.tsx` and the + per-element normalisation in `containers.tsx` are gone. The rule is closed: a + string is an id, a non-null non-array object is an inline definition, and any + other element (`null`, a number, a nested array) is refused at its index too. + An all-id array resolves by `name` in authored order, first registration + winning on a duplicate name; ids that name nothing are reported back with their + index for the caller to warn about once its lookup has settled. + + **Three further behaviour changes ride on the one rule, all on published + packages:** on `page:header`, a padded id (`' convert '`) no longer resolves — it + was previously trimmed before the lookup, and ids are now compared exactly as + authored; on `page:header`, a blank `''` id is now reported by the unresolved-id + warning instead of being silently skipped; on `record:quick_actions`, an all-id + `actions` array with no object bound now renders nothing (the ordinary empty + placeholder) instead of handing the bare strings to the action engine as action + definitions. +- b61d7d8: `resolveTitleField` delegates to the shared ADR-0079 ladder (objectui#7287). + + ADR-0079 collapsed ~6 divergent record-title resolvers onto one — `@object-ui/core`'s + `record-title.ts`, whose header tells the "Untitled everywhere" story that produced it. + `plugin-detail` grew one back. `resolveTitleField` now calls core's `resolveNameField` + and nothing else, so the detail page, the list column and the lookup chip cannot + disagree about which field titles an object. + + **Two rungs are gone.** + + `def.primaryField` — a `DetailViewSchema` key (`@object-ui/types` `views.ts`), read off + an OBJECT def and ranked ABOVE the canonical `nameField` ADR-0079 Phase 2 made the + pointer (AGENTS.md Commandment #0.1). No producer can put it there: `@objectstack/spec`'s + object schema is a `strictObject` that answers `unrecognized_keys: ['primaryField']`, + and `ObjectSchema.create()` throws — which is why objectstack#6326 deleted the identical + read from two lint rules. A census across both repos found **zero** object payloads + carrying it (the only writers are three test fixtures), and `primaryField` appears in + **zero** files of the shipped `@objectstack/spec@17.2.0` dist against 68 for `nameField`. + Same shape as the undeclared `objectDef.titleField` read objectui#6531 measured and + #6557 removed. `DetailViewSchema.primaryField` is untouched and still honoured by + `DetailView`'s own header — it is a view key, and on a view it is legitimate. + + The literal `['name','full_name','title','subject','display_name']` walk — a NAME match + with no type check. + + **User-visible, deliberately.** Two shapes retitle: + + - An object with no conventional name field — `{ due_at: datetime, headline: text }` — + resolved to `null` here while every other surface titled the record `headline`. The + detail page now agrees, and the highlight strip stops repeating the H1 as its first + chip (the objectui#2548 duplication, which until now only DECLARED titles were spared). + - A field named `title` of a non-title-eligible type (e.g. a `select`) was chosen by + name alone and is now correctly skipped, so the strip no longer hides a chip on + account of a field the H1 never shows. + + **A third user-visible change, in the highlight strip's skip set.** It previously read + `primaryField`, `nameField` and `displayNameField` separately and skipped EVERY declared + one; it now skips the single field the shared ladder resolves. These differ on one input: + an object declaring `nameField` and the deprecated `displayNameField` alias to DIFFERENT + fields. The alias's field is no longer hidden from the strip — deliberately, because + `getRecordDisplayName` reads the same ladder and renders the WINNER's value, so the loser + is an ordinary field the H1 never shows and hiding it spent a strip slot for nothing. + This also makes the heuristic branch agree with the declared branch, which has only ever + filtered a single title field. + + `deriveHighlightFields` asks the retirement gate (objectui#4914) about the title field + **before** skipping it: an object whose only retired-typed field is the one that titles + it must still get its report, which skipping-first would have silenced. + + `ObjectDefLike.primaryField` stays DECLARED but is never read. The interface is + re-exported from this package's index and this package is published, so removing the + member would narrow a shipped `.d.ts` — a contract change owed its own card, not a rider + on a behaviour fix. +- daf9d57: The last two consumer-side reads of `primaryField` off an OBJECT def are gone (objectui#7586). + + `primaryField` is a `DetailViewSchema` key (`@object-ui/types` `views.ts`) — a **view** key, + which `DetailView.resolveDisplayTitle` reads off `schema` and is welcome to. Read off an + **object** def it is undeclared: `@objectstack/spec`'s object schema is a `strictObject` + answering `unrecognized_keys: ['primaryField']`, and `ObjectSchema.create()` throws. + `primaryField` appears in **zero** files of the shipped `@objectstack/spec@17.2.0` dist, + against 68 for the canonical `nameField`. objectstack#6326 removed the identical read from + two lint rules; objectui#7287 / PR #7585 removed it from `resolveTitleField`. These two + survived it — and three of this repo's own changelogs already called the probe *"not a spec + property — always undefined"* while the code kept honouring it. + + **They are two different repairs, not one patch applied twice.** + + `@object-ui/components` — `PageHeaderRenderer`'s record-chip chain ranked + `objectSchema.primaryField` directly under `schema.title` and **above** the unified ADR-0079 + resolver, on the surface that renders the **actual H1** of a synthesized record page. The + rung is deleted, so the heading now comes from `titleFormat` → the ADR-0079 resolver + (`nameField` → `displayNameField` → type-aware derivation) → the record-key walk, the same + precedence `DetailView`'s own header uses. + + `@object-ui/plugin-detail` — `record:details`' dedupe ladder decides **which row the body + grid hides**, so that the field already shown as the H1 is not repeated underneath it. That + is a different question from "what is the title", and `primaryField` was its *first* + candidate. The rung is deleted; the ladder is the literal display-name walk that mirrors the + tail of the header chip's chain. Its docstring, which had described the chip as resolving + "from objectSchema.primaryField", went stale when PR #7585 landed and now describes the chip + as it is. + + **User-visible, deliberately.** A payload that carries the off-spec key anyway changes in two + ways: the H1 of its record page stops being `primaryField`'s value, and a different row + survives the detail grid. Nothing spec-legal can reach either path. + + `DetailViewSchema.primaryField` is untouched, and so is `ObjectDefLike.primaryField` in + `buildDefaultPageSchema` — a deliberate declaration (not a read) that keeps an external + caller's object literal type-checking. +- cf1d29e: `ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one + (objectui#8067). + + A registration's `type: 'array'` said a value was a list and stopped there, so a member + that drifted from `@objectstack/spec` was invisible to every layer that reads a + declaration. `page:header.actions` is the measured cost: the contract declares + `z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` + objects, and the repo-wide parity gate in + `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the + whole life of the drift because both sides carried the key and neither could say what + was inside it. What settled it was a maintainer ruling, not a test — and even after the + fix, "these are ids" survived only as English in the registration's `description`. + + **What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind + vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an + `object` used as a map. One kind, or an array of them for a member contract that is a + union, with `type`'s semantics — a member passes when any declared arm accepts it. The + manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per + input instead of six. + + **Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 + is the precedent: five `ComponentInput` keys declared and read by nothing). The + repo-wide parity gate compares every declared `of` against the member kind + `ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; + `sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new + `member-type-mismatch` diagnostic naming the offending positions; and the generated + `sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is + `string[]` where it used to be `unknown[]`. + + **Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: + every container key's member position was probed with one value of each coarse kind and + a declaration written only where exactly one kind was accepted. A member contract that + admits several kinds — `record:highlights.fields` takes a field name or an inline field + object — is deliberately left undeclared and pinned with its reason, because picking one + arm there is a narrowing this repo leaves un-gated and picking all of them would + advertise shapes only a per-block pin can vouch for. + + **The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer + ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` + is the publication face's expression ceiling, and spec is the sole judge of values — + stands exactly as written. `of: 'object'` says the members are objects; which keys they + carry is still `description`'s job and `os validate`'s. + + **Nothing published before this changes.** An input that declares no `of` validates, + serializes and types byte-identically: `validateTree` checks no member, the serializer + emits no key, and the codegen emits the same `unknown[]`. +- d06fba8: The console record page and the `record:activity` block read ONE `sys_activity` + type table, so a scheduled meeting no longer appears on one and vanishes on the + other. + + `RecordDetailView`'s `sys_activity` merge carried a hand-written copy of the + table that `record:activity` exports as `ACTIVITY_TYPE_TO_FEED_TYPE`. Neither + file imported the other and nothing compared them, so the two could drift + silently — and they had. objectui#5840 added `scheduled` -> `event` to the + exported table, because a shipped producer (HotCRM's `schedule_meeting`) writes + that value and the row was being dropped before any filter ran. The copy here + was left untouched, so the same row rendered on a hand-authored record page and + was dropped on the console record page: same record, same row, two answers. + + `RecordDetailView` now imports the exported table, and the copy is gone. The + table is re-exported from `@object-ui/plugin-detail`'s entry point, which is + what makes a single reading possible at all — it was previously reachable only + from inside the plugin. No module enters the eager closure: the module holding + it was already pulled in by the renderer beside it. + + Adding an activity type is now one edit, in one place, that both surfaces see. + A re-fork is caught rather than merely discouraged: the new pins spy on the + shared object and inject a member into it at runtime, so a private copy holding + today's members exactly — the failure this change removes — fails, where a + value comparison would pass on the defect. + + Only the table converges. The row-to-`FeedItem` construction around it is still + written twice, so an unmapped activity type is still dropped silently on the + console surface where the block warns once; that mirror is filed separately. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- 0b12a33: `record:path` stops looking like a control it cannot be. + + The record page draws the object's lifecycle across the top from the + `stageField` role, and it drew each stage as a filled, shadowed, equal-width + pill — a segmented button group, sitting exactly where a CRM user reaches for + the stage control. Nothing was behind it. Measured in a browser on a shipped + build (HotCRM `crm_quote`, and this is generic record chrome, so every object + that declares a `stageField` has it): the segments were `role="listitem"` with + `cursor: auto` and `tabindex` null, no ancestor `button`/`tab`/`a`, and a full + pointer sequence (pointerdown → mousedown → pointerup → mouseup → click) left + the record's status untouched. Advancing a record needs the edit form or a bulk + action. Users spent two or three clicks on the path before concluding it was + decoration. + + There is no write path to connect it to, and this change does not open one: + this renderer's only channel is `useRecordContext()`, whose value exposes + `data` / `refresh` / `headerSystemActions` / `onToggleFavorite` and no + record-field mutation. Editing runs through `record:details`' + `<InlineEditProvider>` + `<InlineEditSaveBar>` (`dataSource.update(..., + { ifMatch })`) or an action via `useActionEngine`; neither reaches this + component. + + So the promise is withdrawn rather than honoured. Each stage now renders as a + thin decorative rail segment with its label as plain text beneath it — the + vocabulary app-shell's approval step readout already uses. Gone: the per-stage + filled pill, the shadow, the ring, the bordered chip, the equal-width tap + target. Kept exactly as they were: which stage is current (`aria-current="step"` + and type weight), the travelled/untravelled distinction, the check on completed + stages, and the separated `lost`-terminal group. The accessible semantics did + not move — `role="list"` / `role="listitem"` with no tab stop was already + correct for a readout, and it stays that way. + + Three DOM attributes carry the state that colour used to be the only carrier + of, so the classification is assertable without reading CSS: + `data-stage-state` (`completed` | `current` | `upcoming`), + `data-stage-terminal` (`won` | `lost`), and `data-stage-rail` on the decorative + indicator. + + **Not in this change:** click-to-advance. A stage control that writes + `stageField` through the same permission/validation envelope an edit takes is a + separate feature with its own appetite, and folding it in here was explicitly + ruled out. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- 8a44390: `record:alert` binds the row through `usePredicateRecordContext`, so an + author-declared `properties.visible` is actually consulted. + + `renderers/record-alert.tsx` was the last predicate face in the repo still + handing `useCondition` a root-only `{ record }` bag. Every other row-scoped + predicate — the four generic action renderers (objectui#4075) and app-shell's + `DeclaredActionsBar` (objectui#4077) — binds the row through the shared + `usePredicateRecordContext(record)` helper, which resolves the three spellings + objectui#5330 ruled on: canonical `record.status`, the deprecated row-action + shorthand `status`, and deprecated legacy `data.status`. + + Under the root-only bag only the canonical spelling worked, and the two others + failed in **opposite** directions — both of them silently, because this call + site is fail-soft: + + - **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator + threw. The legacy `${…}` path answers a throw with its own source text, a + non-empty and therefore truthy string, so the verdict was **SHOWN on every + row**. A banner the author had gated was permanently on screen. + - **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's + ambient predicate scope (`providers/ExpressionProvider.tsx`) carries + `data: {}`, so the predicate read that object instead of the row, compared + `undefined`, and the verdict was a constant false — **never shown**. + + **Behaviour change, stated plainly:** a shipped `record:alert` whose `visible` + was written in either deprecated spelling was inert and is now live. A banner + that was permanently visible may begin to hide, and one that never appeared may + begin to show — that is the point of the fix, but it is a verdict change rather + than a no-op. Canonical `record.*` predicates are unaffected in verdict: they + resolved before and resolve now, pinned on both polarities. An in-tree census + found no `record:alert` `visible` predicate outside this package's own tests. + + A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`, + with its own deliberate bindings (`data` is the data-source adapter there, not + the row). This change does not touch it; the two still compose as AND. + + The renderer's header comment described the shared-scope behaviour it did not + have. It now describes what the file does, including the fail-soft policy and + the two-gate composition. +- 23705b7: **`ReferenceRailEntry` is now the spec's type, and the reference-rail `icon` key retires** (objectui#5494, maintainer ruling 2026-08-22). + + `ReferenceRailEntry` is owned by `@objectstack/spec` as of 17.1.0. The hand-written interface in `record-reference-rail.tsx` is replaced by a re-export of the spec's `ReferenceRailEntry` (derived from `ReferenceRailEntrySchema`, `$strict` over `{ objectName, relationshipField, title?, limit?, displayField? }`), and `buildDefaultPageSchema` no longer emits `icon` on the reference-rail entries it synthesizes. + + **Migration — if you write `icon` on `record:reference_rail` entries, remove it.** Be aware of what this does and does not change: + + - **Runtime validation does not move.** The spec's strict schema already refused `icon` at save — that refusal is unchanged. What was broken was objectui's declaration: the TS type advertised a key that could never be saved. + - **Nothing ever rendered `icon`.** No render path in `RecordReferenceRailRenderer` has ever read the key (measured back to the file's first commit, and independently by the spec's `ui-reference-rail-unknown-keys-refused` migration entry). An authored `icon` that survived in unsaved/preview metadata was already a silent no-op — after this change the TS type says so instead of suggesting otherwise. + - **The published TS surface narrows.** Code that imported `ReferenceRailEntry` from `@object-ui/plugin-detail` internals and set `icon` will now get a type error. That error is the contract speaking: remove the key. + + The reference-rail icon affordance is retired rather than proposed upstream: a stock scan of this repo, its examples and schema-catalog corpus, and the objectstack tree (examples, templates, docs) found no reachable authored usage of `icon` on reference-rail entries. Stored customer metadata is not reachable from this seat and was not scanned. + +### Patch Changes + +- f66072d: Inline `t(key, { defaultValue })` strings are now held to the one placeholder spelling a + provider-less host can resolve, and five of them are pinned to the pack value for the first + time (objectui#4905). + + `check:i18n-keys`' `default-value-drift` class pins an inline default byte-identical to its + `en` row, and objectui#3512 holds `en` to the one spelling `createSafeTranslation`'s + `fallbackT` interpolates — so most inline defaults were covered transitively. Re-measured + on this tree, 66 of 1003 were not: three literal defaults on dynamic keys, and 63 written + as a computed expression. A new `unresolvable-default-spelling` class in + `scripts/check-i18n-call-site-keys.mjs` now judges the text every inline default carries — + the folded sentence, or a template literal's static segments — so `{{ name }}`, + `{{count, number}}`, `{{- name}}` and `$t(key)` are refused wherever they are written, + rather than only inside a copy table. + + Four call sites gain a real pin because their default carries no placeholder at all: the + record-form submit button now falls back to `Update`/`Create` (the pack's wording) instead + of `Save`/`Create` via a nested `t('common.save')`, the context selector's package label + and the approvals separator now state their literal. One more (`detail.showEmptyFields`) + is behind a `createSafeTranslation` hook, whose fallback does interpolate, so it can safely + say what the pack says. + + The other 61 are deliberately left computed, and the measurement behind that is the useful + part: react-i18next's not-ready `t` returns `options.defaultValue` **verbatim, without + interpolating it**. At a call site bound to a bare `useObjectTranslation()`, a default + written as `` `Signed in as ${user.email}` `` is therefore the only form that renders + correctly with no provider — rewriting it to `'Signed in as {{email}}'` would put literal + braces in front of the user, which is the exact defect this family of cards exists to + prevent. Those sites keep their template literals and are covered by the spelling class + instead. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 1117414: `@object-ui/plugin-detail` now declares `"@objectstack/spec": "^17.1.0"` rather than + `^17.0.0`, which is the lowest published spec that carries the symbol its own build + output re-exports (objectui#5793). + + `dist/renderers/record-reference-rail.d.ts` reads + `export type { ReferenceRailEntry } from '@objectstack/spec/ui'`, and + `ReferenceRailEntry` first appears in `@objectstack/spec@17.1.0` — measured against the + published tarballs, not the installed tree: the name is absent from 17.0.0's + `dist/ui/index.d.mts` (425 exported names) and present in 17.1.0's (442). So the old + range was a claim the package could not honour. Any consumer resolution that lands + 17.0.0 — a sibling pinning it exactly, an `overrides` entry, a mirror a minor behind — + satisfied `^17.0.0` and got a dangling type re-export. + + Nothing a consumer installs today changes: normal resolution already picks the newest + 17.x, and `pnpm-lock.yaml` still resolves 17.2.0 on this edge after the bump. The change + is to the declared floor only, which is why it is scored `patch` rather than `minor` — + the same reasoning objectui#5753 used for the other direction on this dependency. + + The bump is one instance; the durable half is `scripts/check-spec-range-floors.mjs`, a + gate that compares every published package's `dist` imports of `@objectstack/spec/*` + against the export set of that package's own declared minimum. It runs on the publish + path (`pnpm changeset:publish`) and nightly, and it names the symbol behind every range + it asks for. No other package's floor is touched: the gate finds nothing else to justify + one across the 19 packages that declare the spec in a consumer-facing field. +- 7c96c94: Four more private copies of the reference-bearing field family converge onto + `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`, and the "fourth and last private copy" claim + that `paramToField` still stated is corrected (objectui#5874, objectui#5875). + + Each copy diverged from the published family in BOTH directions, so this is a behaviour + change on every face and not a refactor: + + - **`user` and `tree` are now treated as relations.** Both carry the same foreign-key + storage as `lookup` and resolve through the same expand path (objectui#2032), and each + face's own stated reason for special-casing `lookup` applied to them verbatim — so + gaining them restores the rule each face already meant. A `user` / `tree` field is now + read-only in the quick-look drawer (`RecordDetailDrawer`), where the drawer wires no + relation picker and a plain text input let a user overwrite the relation with a + free-form string; it gets the wide layout basis in the record header's highlights strip + (`HeaderHighlight`), whose inline editor is a record picker; and a field-backed action + param over one now inherits the picker config it needs (`resolveActionParams`). + - **`master_detail` is now treated as a relation by `resolveActionParams` too** — it was + the only face missing that member as well, so a field-backed `master_detail` action param + inherited no `referenceTo` at all and degraded to the unexplained "paste a record id" + text input that objectui#3405 exists to prevent. + - **The undeclarable `reference` spelling is gone from the three field-type faces.** + Measured against `@objectstack/spec`'s closed `FieldType` vocabulary with live controls + (`lookup` / `master_detail` / `user` / `tree`) and dead ones (the retired `owner`, plus a + nonsense spelling): `reference` is absent, so no spec-compliant object schema could + declare a field that reached those branches. It sat exactly where `owner` sat before + objectui#4814 retired it — dead weight that read as live capability. + + `resolveActionParams` keeps answering for `reference`, deliberately and by a different + route: it is refused by the spec's `ActionParamSchema` too, but the dialog still accepts it + from params already authored with it, and that acceptance belongs to the one alias table in + `paramToField` rather than to a hand-copied membership test. This face now asks the shared + family over the widget key that table produces — the same expression `paramToField` + evaluates one step later, so the half that populates a param's picker config and the half + that forwards it can no longer disagree. + + No face copies the set: each calls `.has()` on the object `@object-ui/core` exports, and + each carries an identity pin (a spy on that `has`) so a member-identical private copy fails + instead of quietly re-forking the table. +- 95f8704: `record:path` now announces each stage's state, not just its label (WCAG 2.2 SC 1.4.1) + + The lifecycle path distinguished travelled, upcoming and lost-terminal stages with + colour plus a `✓`/`✗` glyph, and both glyphs are `aria-hidden` decoration. + `aria-current="step"` marked the current stage and nothing else, so a screen-reader + user heard a run of identically-announced items — and a rejected stage announced + exactly like an ordinary stage the record had not reached yet. + + Each stage now carries an accessible name composing its (already picklist-localized) + label with its state, from five new `detail.pathStage*` keys translated in all ten + locale packs. The glyphs stay decorative and the readout's `role="listitem"` / + `aria-current` semantics are unchanged. + + The name is composed into `aria-label` rather than visually-hidden text because + `listitem` takes its name from the author only: text placed inside a stage computes + to an empty accessible name, so the visually-hidden shape would have looked right in + the markup and delivered nothing to the accessibility tree. +- e30ed15: Correct two retired lucide spellings on component-registration `icon` meta + (objectui#5936, the behaviour-neutral slice). + + `ui:page` declared `icon: 'Layout'` and `record:alert` declared + `icon: 'AlertTriangle'`. lucide retires a spelling by dropping it from the runtime + `icons` record while keeping the deprecated named export, so both names still + import and still type-check while resolving to nothing through any resolver that + reads that record. They are now the live keys `panels-top-left` and + `triangle-alert`. + + **Behaviour-neutral by identity, in every resolution world.** The retired export + and the live record entry are the SAME OBJECT — measured against the installed + lucide 1.31.0, not asserted: `Layout === icons['PanelsTopLeft']` and + `AlertTriangle === icons['TriangleAlert']` are both true. So the repair cannot + substitute one glyph for another; it can only turn a name that resolves to + nothing into one that resolves to the glyph it always meant. + + - Through a record-reading resolver (`renderers/action/resolve-icon.ts`), the old + spellings resolve to `null` and the new ones to that shared object. + - Through the dynamic surface (`iconNames`, 2025 names, retired aliases included), + both spellings already resolved, and to the same glyph. + - The kebab spellings were chosen because they are the only ones live on BOTH + surfaces: `PanelsTopLeft` and `TriangleAlert` are record keys but are absent + from `iconNames`, which is kebab-case only. + + Nothing is retired and no gate is extended here. `check-lucide-icon-record-names.mjs` + deliberately does not judge a registration's `icon` meta, and it stays that way — + its verdict is unchanged by this diff (182 names judged, green, both before and + after). Whether the registration `icon` meta itself should be retired is the open + half of objectui#5936 and is a maintainer decision under ADR-0049. +- 8d3a529: `record:path` finishes localizing and de-colouring its accessible names — the two residues + objectui#5916 named and deliberately left behind (objectui#5956, objectui#5957). + + **The list's own label was English on a localized surface, and the other one named nothing.** + Both the desktop and the mobile `role="list"` row did + `aria-label={schema.aria?.label || 'Record path'}`, so a zh/ja/ar session heard `Record path` + for the list while every stage inside it announced in the session locale — one control + speaking two languages at once. The fallback is now `detail.pathLabel`, translated in all ten + packs; the `schema.aria.label` author override still wins ahead of it. + + The lost-terminal alt group was a different defect wearing the same clothes: its + `aria-label="Alternative terminal stages"` sat on a bare `div`, which has the `generic` role, + and browsers expose no accessible name on a generic element. That string reached nobody — + inert, not merely untranslated — so translating it would have shipped copy to ten packs that + no user can hear. It is removed rather than given a role that takes a name, on three + measurements: nothing is lost (it was never announced), it would be redundant (every stage + inside already announces `closed lost` in the session locale after objectui#5916, in the one + place `role="list"` can carry it), and it would fork the two rows (the mobile row renders one + flat list with no alt group, so a named group would make one control expose two structures by + viewport). + + **An unreached goal terminus was distinguished by hue alone.** `railClass` paints it + `bg-emerald-500/30` where a plain upcoming stage gets `bg-muted` — the renderer's own note + calls this "a faint emerald so the goal is legible" — while both announced the identical + `{{stage}}, upcoming`. Two stages ahead of the record painted differently and read the same: + the WCAG 2.2 SC 1.4.1 class objectui#5916 closed, on the one distinction it left behind, and + reachable without authors opting in because `classify()` finds `won` through the `WON_TOKENS` + heuristic as well as an explicit `terminal: 'won'`. New key `detail.pathStageWonUpcoming` + (`{{stage}}, goal stage, not reached`), translated in all ten packs. + + Scoped to the UNREACHED goal, which is a measurement of the stylesheet rather than a + preference: a reached goal terminus paints `bg-primary` when current and `bg-emerald-500` when + completed, byte-identical to any other current or completed stage. Naming it apart would hand + a screen reader a distinction the screen does not make — the mirror image of the defect — so + it is one new key, not a pair, and a test pins that decision so it cannot drift into a fourth + state unnoticed. + + Both new keys also land in `DETAIL_DEFAULT_TRANSLATIONS`, which + `defaults-maps-mirror-en-pack` compares against the `en` pack key by key, so neither can fork + between a provider-mounted console and a provider-less embed. No existing `en` value changes, + so no pack is asked to follow an edit. +- 63d54dd: `record:path` now derives ONE stage classification and hands it to both of its rows, so a + stage can no longer paint and announce two different ways depending on viewport width + (objectui#5998). + + The renderer draws a desktop row (`hidden sm:flex`) and a mobile row (`flex sm:hidden`) from + the same `stages[]`, and each used to compute its own `terminal` from that array. + `renderStage` passes the same `terminal` to `railClass` and — since objectui#5957 — to + `stageAriaLabel`, so any disagreement surfaced in the colour and in the accessible name at + once. The rows disagreed on two axes: + + Mid-path goal. `WON_TOKENS` matches `完成`, an ordinary word rather than a Salesforce-style + `closed_won` value, so a path like `草稿 → 完成 → 已归档` classified index 1 as `won`. + Desktop declined it, because only the last forward stage can be the goal terminus; mobile + marked it `bg-emerald-500/30` and announced `goal stage, not reached`. + + The lost slice. Desktop renders `stages.slice(firstLostIdx)` as a visually separated alt + group and hardcoded `terminal: 'lost'` on every member of that positionally-defined group, + while mobile classified each stage on its own. A plain stage after a `lost` one + (`草稿 → 失败 → 已归档`) therefore painted destructive and announced `closed lost` on + desktop and plain on mobile; a `won`-classified stage in the same position drew `'lost'` + from one row and `'won'` from the other. + + Both rows now index a single `stageTerminals` array: `lost` is a property of the stage + itself, `won` is the goal terminus and so is the last forward stage or nothing, and the + positional grouping is a layout concern that no longer overrides what a stage is. Behaviour + is narrowed on both axes and never widened — no stage gains a `terminal` on either row that + it did not already carry there, and a goal terminus that really is last keeps its faint + emerald rail and its `goal stage, not reached` name on both rows. +- b4393e5: The last three sort-axis consumers read the platform's per-column sortability signal instead + of re-deriving it from the field's type (objectui#6108, inheriting objectstack#10235 ruling A + through objectui#5729's landed contract). ListView's toolbar sort picker and both of + RelatedList's sort entry points — the embedded table's column headers and the `data-list` + sort-button row — now go through `isPlatformSortableField`, the same spelling the grid header + adopted; their `UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType` re-derivations are + deleted. + + The re-derivation was not wrong about `formula`: the platform computes its own projection from + the same `@objectstack/spec` storage fact, which is why the drift went unnoticed across two + cards. It parts company on everything the projection encodes as ABSENCE — an unknown name, a + dotted path a caller can put in a related list's `columns`, an unprovisioned audit column — + where a type read finds no field definition, answers "sortable", and offers a control the + runtime meets with `400 INVALID_SORT`. It parts company again on any refusal that carries no + `reason: virtual-type`, and it cannot follow the platform in the other direction either: a + field the platform now DOES order by stays withheld forever on its type alone. + + Two behaviours are deliberately unchanged. The relational carve-out stays separate from the + signal — the projection answers `sortable: true` for a `lookup` because the platform can order + by the stored foreign key, while the UI withholds because that order means nothing beside a + column of names — so a relational column does not get its sort back. And ListView's picker + still lists a field the CURRENT sort already names, which is the only way to remove a sort the + server refuses outright; that exception now covers platform-refused fields, not just formulas. + + A deployment that served no `sortability` key at all is a different case from "nothing is + sortable": that branch keeps the type read as a compatibility floor, so behaviour on a backend + older than objectstack#10235 (or an inline/mock data source) is byte-identical to before. +- d3005f7: `DetailSection` now resolves `section.headerColor` through a lookup of complete + Tailwind class literals instead of building `bg-` + the authored value as a + template literal (objectui#6178). + + Tailwind v4 has no runtime — it builds the stylesheet by scanning source text + for complete class tokens, and this workspace ships no `bg-*` safelist — so the + old expression contributed nothing to the compiled CSS. Measured, not assumed: + compiling `apps/console/src/index.css` with that expression deleted produced a + byte-identical stylesheet (same sha256). An authored value styled the header + only when some other source file happened to author the identical class + literally, which is why both documented examples appeared to work: `bg-muted` + occurs 691 times and `bg-primary/10` 63 times elsewhere in the workspace. That + liveness was accidental and moved with unrelated edits in unrelated packages. + + The shape matches the sibling this repo already solved the same way — + `useRowColor`'s `COLOR_TO_CLASS` in `@object-ui/plugin-grid`: + + - a lookup of literal, tint-only design-system classes: `muted`, `muted/50`, + `accent`, `primary/10`, `secondary/10`, `destructive/10`. Both values the + `@object-ui/types` mirror documents (`muted`, `primary/10`) are in it, so + nothing that rendered before renders differently now; + - a value that is already a complete `bg-*` class is passed through untouched. + This is new — `headerColor: 'bg-muted'` previously produced the meaningless + `bg-bg-muted`; + - anything else contributes no class at all, instead of a fabricated one. + + Behaviour change to be aware of: an undocumented bare suffix outside the + vocabulary (say `headerColor: 'blue-100'`) no longer reaches the DOM as + `bg-blue-100`. It rendered before only where another file happened to author + that exact class; write it as the complete class (`headerColor: 'bg-blue-100'`) + to keep it, on the same terms as any `className` a schema carries. No value is + rejected and the declared type is unchanged. + + `headerColor` remains undeclared on the strict `@objectstack/spec` + `record:details` section schema, which refuses it today on the strength of this + defect (objectstack#11661). Declaring it, and with which vocabulary, is a + separate spec decision. +- 830ed58: The record page's approval band offers its **Recall** button to the approval's submitter + only (objectui#6464). + + Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval; + user B — not the submitter, read access, not an admin — opens the record and the band still + lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes + on submitter identity and refuses everyone else, so the only outcome available to that + button was a failure toast. Record state was never at risk; this was purely a + writability-feedback mismatch, the same family as objectui#3794. + + The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at + all" — which is a question about the DataSource, not about the viewer. Identity now joins + it, threaded the way every other signal on that band already travels: the HOST resolves it + and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and + never re-derives who submitted what. + + - `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on + `InlineEditContextValue`. Additive and optional; no existing prop changes. + - `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a + resolved `false`. + - `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing + approvals read and threads it. + + **The signal is tri-state, and the third state is the load-bearing one.** `true` offers + recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity + — renders exactly as it did before this release.** Omission preserving prior behaviour + mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide" + would have traded a cosmetic defect for a functional loss: every host whose band runs off + the record's `approval_status` mirror alone would silently lose its submitter's only way to + unlock their own record. + + **Withdrawn rather than disabled-with-reason.** The card offered either. For a + non-submitter this control is never actionable on any pending record, so a permanently + disabled button is standing clutter rather than a lesson; and the two sibling submitter + levers already hide — the approvals panel's Remind button, and the declared + `approval_recall` action's `visible` predicate. The band, its quorum tally and the + approvals timeline still tell a non-submitter exactly what state the record is in. Only the + lever they can never pull is gone. + + **This changes no permission.** Nothing about what the server allows moves, `canEdit` and + the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an + authorization verdict — the recall endpoint remains the sole authority, and it refused + these callers before this change and refuses them after. There is deliberately **no admin + carve-out** (the reporter ruled that case out, cf. objectstack#9464). + + The derivation itself is now one function, `isSubmitterOf` — server-resolved + `viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends + that predate it, joined with `??` so a server that resolved `false` is believed rather than + re-litigated client-side. The approvals panel's Remind gate, which already carried that + expression inline and whose behaviour is unchanged, now reads the same answer: two copies + would have been two definitions of who submitted. + + The **untranslated refusal text** the reporter also saw ("No pending approval request found + for this record", concatenated after a localized prefix) is a separate defect and is not + addressed here; it is tracked on objectstack#11993. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 06b8c42: Re-key three more renderer effects onto the primitives they actually read, + instead of the memoised object identity that produced them (objectui#6697 — + the three census members from objectui#6592 that sit outside its + `getDataConfig(schema)` family): + + - `RelatedList`'s collection fetch now depends on `defaultSortKey` / + `filterKey` (the `JSON.stringify`-derived content strings the two memos are + already keyed on) rather than on `defaultSortSpec` / `listFilterNode`. + - `page:tabs`' related-count probe now depends on a serialised `probeKey` + rather than on the `probeTargets` `Map`. + - `ListView`'s data fetch now depends on the `expandFields` memo's own INPUTS + — `schema.columns`, the alternate views' binding blocks and + `objectDef?.fields`, all props and state a discard cannot move — rather than + on the `expandFields` array the memo returns. + + `useMemo` carries no semantic guarantee — React is permitted to discard a memo + cache and recompute even when its dependency array compares equal to the + previous render — and all three factories return a FRESH value on every call + (`normalizeSortSpec`/`toFilterNode` build a new array / a freshly lowered AST, + the probe factory builds a new `Map`, `buildExpandFields` returns a new array + in every branch). So each effect re-ran on a discard alone, with nothing an + author or a caller controls having changed: an extra `dataSource.find` for the + related collection, an extra `dataSource.find` for the list window, and a + redundant re-probe of every tab's count. Keying on the primitives makes a + cache discard a no-op and returns `useMemo` to being a pure optimisation. + + Severity is low and the fix is deliberately narrow: the observable was a + redundant round trip, never incorrect data, so only the re-run condition + moves — each effect body still reads the memoised value, and a genuine change + still refetches exactly as before. + + The three take two routes on purpose — key on the nearest DISCARD-IMMUNE + thing. `RelatedList`'s memos are keyed on exactly one primitive each, and + `page:tabs`' probe memo is keyed on another MEMO's output (`items`), which is + not discard-immune, so both take a content string. `ListView`'s memo is keyed + on props and state, so it names those directly: a value key over + `expandFields` would NOT have been content-equivalent there — `buildExpandFields` + collapses the collected set down to the relation roots, while the effect body + also builds `$select` from `schema.columns` and the view bindings — and it + would have defeated objectui#4567's live-dependency pin, which ruled that + "ListView's by-identity dependency is correct for a real column change" and + put the identity stabilisation at the PRODUCER. + + One correction to the census card's account, measured while pinning it: for + `page:tabs` the redundant probe costs nothing on the wire. + `RelatedCountStore.fetch` returns the cached count as its first act and dedupes + concurrent probes, so the extra work is the effect re-running, not an extra + request. +- 03380aa: Inline edit: a rejected save now says WHICH field the server refused, and why. + + Editing a record in place on a detail page and hitting Save used to surface the + backend's own string when the write was refused — `VALIDATION_FAILED: + Validation failed for crm_opportunity` — leaving the user to guess which of the + fields they had just edited was the problem. The refusal has always been + field-scoped (`@objectstack/objectql`'s validators throw `VALIDATION_FAILED` + with `fields[]`, and both the REST layer and the runtime dispatcher pass those + entries through intact); the inline surface was the last one still dropping + them. `<InlineEditSaveBar>` now renders one reason per rejected field, named by + that field's own label — the same treatment record forms have had since #3222. + + Attribution never guesses. It reads the envelope through + `@object-ui/react`'s `extractFieldErrors`, the single in-repo normaliser the + form surface already uses, and an entry with no usable `field` is dropped + rather than pinned on whichever input is nearby. In the drawer's callback mode, + where persistence loops `onFieldSave(field, value)` one key at a time, a + rejection is attributed to the key that was in flight — a fact about the write, + not an inference. Anything that is not field-scoped (a network failure, a + permission denial) keeps the cleaned single-line message it had before. + + `@object-ui/react` gains one additive public API member to carry this, and it + is the reason that package's entry is `minor` rather than `patch`: + `InlineEditContextValue` now has **`fieldErrors`** — a nullable map of field + machine name to the server's reason — alongside a **`setFieldErrors`** setter, + the exact companions of the `error` / `setError` pair that interface already + carried. Nothing is removed and nothing changes shape, so every existing host + and consumer compiles and behaves as before; a host that never reads the new + member sees no difference. It exists because the save bar and the field rows + are SIBLINGS under `InlineEditProvider` in both persistence modes, so before + this there was no channel between the component that receives a refusal and the + components that render the fields it is about. + + Also recorded in code, per the maintainer's ruling on objectui#6868: **the + server is the validation authority on the inline-edit surface.** That was + previously an absence — `InlineFieldInput` runs no rules and takes no `error` + prop — and it is now a decision, written into both modules' headers with a + pointer to the ruling. No client-side rule evaluator was added, and none should + be: the server is the only rule source, and this surface only presents it. +- 639114c: Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122). + + ⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.** + The exported `ObjectSchemaClientExtensions` narrows from + + ```ts + export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' } + ``` + + to + + ```ts + export type ObjectSchemaClientExtensions = Record<never, never>; + ``` + + Two breaking consequences for a consumer that names the type directly. **(1)** It + no longer declares `editMode`; the key is now carried by the spec's + `ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against + the extension type ALONE loses it. **(2)** `interface` → type alias also ends + **declaration merging**: a consumer that reopened + `declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }` + to add its own client-side member no longer compiles, because an alias cannot be + reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule — + objectui's own breaking changes are graded `minor` with the semantics stated in + the body, since any `major` in the fixed group would push all 39 packages off + `@objectstack`'s major. + + **`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured: + the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty, + declared as the same `'page' | 'modal'` union objectui carried). Its local copy + is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin + prescribed for this event, leaving the client delta empty. Nothing is removed + from the product: `editMode` stays authorable and stays typed on + `ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a + local member — and a published, spec-validated object document may now carry it, + which at 17.2.0 was refused by name. + + **`user:profile` is retired across all three sites.** 17.3.0 dropped it from + `PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly + `['user:profile', 'element:form']`, gained set empty). objectui went on knowing + it in three places, so all three moved together: the Studio palette exclusion + ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the + regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing + user-reachable went with it — neither type had a renderer, `user:profile` had + only the dashed "Component Placeholder" scaffold, and the app shell's own + profile affordance is a React slot, never this block type. A page schema still + naming it now draws the loud "Unknown component type" panel rather than a silent + grey box, which is this repo's standing treatment for a type outside the + supported surface. + + **`record:details` sections document the eight keys 17.3.0 added.** + `group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`, + `description` and `headerColor` are now declared on a section entry (4 → 12 + members). Six of the eight are already honoured by `DetailSection`, so the + `sections` input description now teaches all of them, and says plainly which two + are not read here. Designer controls for them are a separate feature and are + deliberately not added. + + **`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` → + `^17.3.0`, and this is the second half of its `minor`.** The package's emitted + `dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which + `17.0.0` does not export, so the old range was a claim the artifact did not + support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]` + and names `^17.3.0` as the lowest version carrying every symbol the package + references. Breaking for a consumer pinned below 17.3.0: it can no longer + resolve this package. That is the range stating the truth rather than a new + restriction — the artifact already required those symbols — and it is the + remedy the gate itself prescribes ("Raise that package's range to the lowest + version that exports the symbol… Do not add a tolerant re-declaration on this + side: the range is the claim, and the claim is what is wrong", objectui#5793). + `@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0` + and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is + this repo's normal state, not an exception. + + ⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing + and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main + workflow that cannot red a pull request, and `main` is green on it — the last + eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1 + with CI's own `--cross-check` invocation, and exits 0 with this raise, judging + 278 (subpath, symbol) pairs across 19 published packages either way. Its blocking + copy runs on the publish path, so leaving it would have surfaced as a cancelled + release rather than as a red check. The correction is recorded on objectui#7688. +- 854cba3: `ActivityTimeline`'s empty state speaks the session locale (objectui#7142). + + The title was a raw English JSX literal — `title="No activity recorded"`, not a + `t()` call and not an inline `defaultValue` — so it never reached the pack + system and stayed English in all ten locales. Measured before the fix by + rendering `activities={[]}` under a zh `I18nProvider`: the card read + `"Activity(0)No activity recorded"`, while its sibling `RecordActivityTimeline` + rendered `"活动(0)全部动态暂无活动记录"` from the same packs. + + The call site now reads `detail.noActivity`, the key the sibling already uses. + Reusing it rather than minting a second key is a measured decision: the `en` + pack value for that key is `'No activity recorded'`, byte-identical to the + literal it replaces, so both surfaces were already saying the same words in + English and a new key would have forked one sentence across ten packs for no + copy difference. No pack was edited — the key is already translated in all ten, + verified by reading `detail.noActivity` out of each pack object. + + Both routes to the box are covered: an empty `activities` array, and a + populated timeline filtered down to a type with no entries. +- d8ec8d6: `ActivityTimeline` speaks the session locale — the other 18 literals + (objectui#7149). + + objectui#7142 gave this component its first `t()` call (the empty-state title) + and filed the sweep that found the rest. Until now a zh activity tab read + `"Activity(0)暂无活动记录"`: one translated string in a component that was + otherwise entirely English. + + All 18 now resolve from the ten packs, in three groups: + + - **Relative timestamps and the card title** (`just now`, `{{count}}m/h/d ago`, + `Activity`) — these render on *every* activity tab. All five were a pure + lookup swap: the `en` pack value was already byte-identical to the literal, + and the sibling `RecordActivityTimeline` already used the same keys. + - **The `formatFieldChange` sentences** — assembled in code, so they needed new + keys *with* interpolation holes rather than a lookup. Same reachability as the + timestamps: they render for any entry whose optional `description` is absent. + The quotes live inside each pack's value, so every locale punctuates its own + way (de `„…“`, zh `“…”`, ja `「…」`, fr/ru `«…»`). + - **The six filter chips and the chip group's accessible name** — reachable only + through the published export, since no host in this repo passes `filterable`. + + Ten new `detail.*` keys across all ten packs (no inline `defaultValue` — + objectui#3517), mirrored byte-for-byte into `DETAIL_DEFAULT_TRANSLATIONS` so a + provider-less host still reads English rather than a raw key. + + One deliberate English copy change: the chip group's `aria-label` was + `"Activity type filter"` and now resolves `detail.filterActivity` + (`"Filter activity"`) — the key `RecordActivityTimeline` already uses for the + accessible name of its own activity filter, so one control does not carry two + names across two components. + + Also drops the unused `Filter` import (a pre-existing eslint warning). +- 866cd1d: `RecordComments` and `PointInTimeRestore` resolve their copy from the locale + packs instead of hardcoded English (objectui#7163). + + Both files carried their own `formatTimestamp` whose relative-time branches + returned English literals, so a zh/ja/ar session read `5m ago` next to + otherwise translated chrome — the defect objectui#7142/#7149 fixed one file + over in `ActivityTimeline`. + + - **`RecordComments`** was already wired to the packs (11 `t('detail.…')` + references), so this is a pure lookup swap onto `detail.justNow` / + `minutesAgo` / `hoursAgo` / `daysAgo` — keys already present in all ten packs, + each `en` value byte-identical to the literal it replaces. **No new key, no + copy change.** + - **`PointInTimeRestore`** used no translation hook at all, so it is swept + WHOLE rather than having only its timestamps converted: card title, empty + state, field-count line, preview panel, snapshot heading, restore + confirmation and all three buttons. Wiring one string into an otherwise + untranslated component is what shipped objectui#7142's visibly half-done zh + card; that is not repeated here. + + Ten new `detail.*` keys land in all ten packs and in + `DETAIL_DEFAULT_TRANSLATIONS`: `revisionHistory`, `noRevisions`, + `revisionFieldsChanged`, `revisionFieldsChangedOne`, `revisionPreview`, + `revisionSnapshot`, `restoreConfirm`, `restoring`, `confirmRestore`, + `restoreToPoint`. `detail.cancel`, `detail.activityEmptyValue` and + `detail.emptyValue` are **reused**, not forked. + + The restore confirmation becomes one key with a `{{when}}` hole rather than a + sentence assembled around a JSX expression, and the field-count line uses the + repo's two-key plural convention selected by a static ternary over two literal + keys — never `t(KEYS[n])`, which objectui#7149 measured as invisible to the + i18n scanners. + + One deliberate copy change: the snapshot panel's null placeholder was an EN + DASH (`–`, U+2013) written inline and now resolves `detail.emptyValue`, which + is an EM DASH (`—`, U+2014) in all ten packs — the glyph the rest of the detail + package already uses for an empty value. +- 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). + + objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope + (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This + closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, + `DetailView`, and `ObjectDataTable` (which builds its own whitelist in + `computeLookupExpand` rather than calling `buildExpandFields`). + + **Three of them pass no column list at all**, which makes them the sharp ones: + `buildExpandFields` reads an absent column list as "no column restriction" and falls + back to **every declared relation on the object**, denied ones included. So a standalone + calendar, a gantt, and every record page in the console asked the server to resolve the + object's full relation set by default rather than by configuration. + + **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column + list is already FLS-filtered field by field, which is exactly the route PR #7229 measured + as unsound: an emptied column list reads as "no column restriction", so a detail view + whose authored fields are all denied had its `$expand` **widened** from the relations it + asked for to every relation the object declares. The principal who may read least was + asking for the most. + + **Reproduced before it was fixed**, as a failing test per site. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip, and the + client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, + which contains only the object's declared reference-bearing fields, so the "`checkField` + answers false for an undeclared key" trap cannot be reached and derived / host-joined + columns are untouched. An unanswered permission policy filters nothing. Neither + `buildExpandFields` nor `computeLookupExpand` is changed. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- fe8f451: Fix `record-alert` with `severity: 'success'` rendering a database glyph instead of a + check mark (objectui#7593). + + `SEVERITY_STYLES.success.icon` was the string `'CheckCircle2'`. That literal reaches + `<LazyIcon name={…} />`, whose resolver degrades an unresolvable name to the `Database` + icon — deliberately, because server-driven schemas reference icons from other + libraries and it is better to degrade than to throw. The result was an emerald + "success" banner with a database glyph in it, on every such alert, silently. + + The fix is the live spelling `'circle-check'`, and it is **substitution-free**: it was + derived by identity rather than remembered. On `lucide-react@1.31.0`, + `CheckCircle2 === icons.CircleCheck` is `true` — the same glyph object under two names + — so the repair cannot change which glyph is drawn, only whether one is drawn at all. + `CheckCircle2` remains a live named export, so static `import { CheckCircle2 }` + call-sites are unaffected; it is dead only for NAME-BASED lookup, which is the route + this constant takes. + + The load-bearing half is a new pin, because nothing could catch this in either + direction. The icon gate does not judge dynamic-surface resolvers (its own verdict line + says so), and the renderer's suite mocks `LazyIcon`, so its assertion could only echo + the literal back — and it did, pinning the dead spelling. The pin consults the real + resolver and asserts that every `SEVERITY_STYLES` icon RESOLVES, rather than that it is + spelled any particular way, so it fails on the actual failure mode rather than + restating the diff. + + The other three severities (`Info`, `AlertTriangle`, `AlertCircle`) are **not** changed + and were never defective: two of them are absent from lucide's `icons` record but + present on the dynamic surface (1767 keys vs 2025 names), which is the surface that + decides here, so they resolve and render correctly today. +- 6c1b105: `record:activity` now says out loud when an unrecognised `filterMode` is folded onto `all`. + + `normalizeFilterMode` folds every value it does not recognise onto `'all'` — the widest of the four declared modes — so a near-miss like `comments-only` opened the panel on the unfiltered stream instead of the slice the author asked for, and said nothing. The fallback is kept (a dropdown handed a value with no matching item renders blank), but the fold now emits one deduped diagnostic naming the offending value and the declared modes. Nothing that renders changes. +- 17fbbaf: `record:activity` renders an author-extended `sys_activity.type` instead of dropping it + + **Behaviour change.** `activityRowToFeedItem` used to return `null` for any + `sys_activity.type` outside `ACTIVITY_TYPE_TO_FEED_TYPE`: the row was stored, + queryable and invisible, with only a console warning to say so. It now renders + through a defined fallback presentation (`UNMAPPED_ACTIVITY_FEED_TYPE`, the + generic `system` feed type), still announced once per distinct type. + + This follows the maintainer ruling of 2026-08-24 on objectstack#11507, + direction 4: `sys_activity.type` is **author-extensible**. Every field on + `sys_activity` is `readonly: true` and objectql's `validateRecord` skips + readonly fields on both write branches, and ADR-0052 §5b.2 forwards an author's + `activityMilestones[].type` into the column verbatim — so a value the platform + never declared is legitimate stored data, not a mistake, and dropping it + reproduces the objectui#5840 failure for every author who writes one. + + The types this map deliberately excludes (`commented` / `mentioned` / `login` / + `logout` → `undefined`) are unchanged: those are decisions, not gaps, and they + still return `null` silently. `ACTIVITY_TYPE_TO_FEED_TYPE` itself is unchanged, + so the copy `RecordDetailView` reads is unaffected. + + The pin that goes with it replaces the set-equality check objectui#5840 removed, + and is deliberately two-directional: the map must cover every **built-in** type + (superset — a new built-in turns it red), and an unknown type must reach the + feed through the fallback (never dropped, never crashing). Set equality is not + asserted in either spelling, and the docblock says so, because pinning to the + closed declaration is what made #5840 drop stored rows. +- d8cf1cb: `record:activity` no longer discards scheduled activities. `sys_activity` rows with + `type: "scheduled"` now map to the `event` feed kind instead of being dropped before any + filter runs, so a not-yet-held meeting reaches the timeline and `types: ['event']` stops + being a permanently empty tab. Held meetings are unchanged: `completed` still maps to + `task` and still hides unless `showCompleted` is set, which is what keeps an upcoming + meeting visible by default while a finished one is not. + + The unknown-type default is unchanged and deliberately so — a row whose type nothing maps + is still dropped rather than bucketed into `system`, because rendering an unmeasured kind + as something it is not would be new wrong data rather than recovered data. What changes is + that the drop is no longer silent: an unmapped type now logs one `console.warn` naming it, + once per type. Types the map knows and deliberately excludes (`commented`, `mentioned`, + `login`, `logout`) stay silent, since those are decisions rather than gaps. +- 0d1e702: `record:activity` no longer widens when its `types` filter cannot be honoured. An + unrecognised or empty kind list used to sanitise to `undefined`, which the filter + pipeline reads as "no filter authored" — so three distinct authored intents collapsed + into one rendering, and the widest one: a page that named the wrong kind was served + every activity on the record, with no diagnostic anywhere at runtime. The principle now + held is that a sanitiser may narrow an author's request or refuse it, but must never + silently widen it; widening turns a typo into "show the user everything", which is the + one outcome no author asked for, and it hides behind a plausible result — a populated + timeline reads as working, an empty one gets investigated. + + **Behaviour change.** A page authoring `types: []`, or a `types` list whose every entry + is unrecognised, now renders an EMPTY timeline where it previously rendered every kind. + That is the fix rather than a regression, but a page relying on the old fallback will + visibly change: `types: []` is honoured as "no kinds", and a list that keeps nothing + filters to nothing. A mixed list keeps its recognised entries and drops the rest. Omitting + `types` is unchanged and remains the only way to say "no filter". A `types` that is not a + list at all (`types: 'comment'` — brackets dropped) is likewise refused rather than + ignored, since a filter that cannot be read is not a request to remove the filter. + + Unrecognised entries are now named out loud: one `console.warn` listing them and the + declared feed item types, deduped so one bad kind warns once however many times the feed + re-renders, matching the unknown-activity-type diagnostic already in this file. A + well-formed filter stays silent, `types: []` included — there is nothing to report about + a request that was carried out exactly. +- b03ba3a: `record:activity` now says out loud when a `types` entry names a feed kind nothing produces. + + `FeedItemType` publishes thirteen kinds and ObjectUI produces five of them + (`comment`, `field_change`, `task`, `event`, `system`). Authoring + `types: ['approval']` parsed, typechecked, built, and rendered a permanently + empty timeline with no diagnostic anywhere — a declared surface enforced by + nothing, which reads to an author, or to an AI writing the metadata, as a + working feature that simply has no data yet. The entry is still honoured exactly + as authored and nothing that renders changes; what changes is that the emptiness + is now diagnosable, on its own deduped channel beside the unrecognised-entry and + unrecognised-`filterMode` warnings already in this block. + + The message keeps two populations apart, because reporting a decision as a + defect is how a warning channel gets trained out of an author's attention: + `record_create` / `record_delete` / `sharing` are named as **deliberately not + adopted** — those rows map to `field_change` on purpose, since the record page + and this block must agree what a `created` row is before either can move to the + richer kind — while the remaining kinds are reported as having no producer, and + the message stops there rather than calling that a gap. Whether somebody ruled + against a kind and left no note is not recorded anywhere the renderer can read, + and the wording says so instead of guessing. + + The producer census the diagnostic reads is derived from the producers + themselves — the `sys_activity` map's range plus its unmapped fallback — so + giving a kind a producer retires its warning in the same edit. The one producer + that cannot be derived (`comment`, built from `sys_comment` rows by the console's + record page, a package this one cannot import) is declared, and a new test + re-runs the census over the whole repository so the declaration cannot go stale + silently. The warning also names what it cannot see: a host that supplies the + feed itself can produce kinds no census taken here bounds. +- 0068348: `record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is + widened to `string | I18nLabel` in both copies (`properties.*` and the flat + compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. + + The renderer already resolves `action.label` through the same inline-locale-map + `pickLocalized` call as `title` / `body` (`const ctaLabel = + pickLocalized(props.action?.label, language)`), so a bare `string` declaration + was narrower than the renderer's own runtime behavior — the same + declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for + `title` / `body` one level up in the same interface (objectui#4998). + + Type-only: the block's published authoring surface still declares `action` as + a bare `object` with the member shape in prose only + (`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — + that half stays parked on the `ComponentInput` member-shape question (PR + #3795) and is out of scope here. +- 641543f: `record:alert`'s `visible` predicate no longer logs a spurious `record is not + defined` evaluation-failure warning during the record-detail loading frame. + + The predicate is evaluated on every render (Rules of Hooks), including the + frame before `useRecordContext().data` has loaded. In that frame `record` is + unset, `usePredicateRecordContext` binds an empty context bag by design (no + `record` key at all), and a bare/`${…}` predicate referencing `record.*` + faulted with `record is not defined` — logged via `console.warn` on every + page load, even the correct, working ones, because the SAME predicate + resolved fine one frame later once `record` populated. The banner's own + visibility was never wrong (it already hides unconditionally while unloaded); + only the diagnostic was permanently misleading on the happy path. + + The predicate is now skipped while `record` hasn't loaded — the same + condition the banner's own "hide while unloaded" early return already used — + since its verdict in that frame was never consulted anyway. A predicate that + is genuinely broken (bad field, bad syntax) still faults, and still logs, on + every frame once `record` is populated (objectui#5776). +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index 5d81f5c700..7561988102 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-detail", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "DetailView plugin for Object UI - comprehensive detail page with sections, tabs, and related lists", diff --git a/packages/plugin-editor/CHANGELOG.md b/packages/plugin-editor/CHANGELOG.md index 9aca54cdca..3412caacc0 100644 --- a/packages/plugin-editor/CHANGELOG.md +++ b/packages/plugin-editor/CHANGELOG.md @@ -1,5 +1,405 @@ # @object-ui/plugin-editor +## 17.7.0 + +### Minor Changes + +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-editor/package.json b/packages/plugin-editor/package.json index ca773a4642..ad36b43a9c 100644 --- a/packages/plugin-editor/package.json +++ b/packages/plugin-editor/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-editor", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Rich text editor plugin for Object UI, powered by Monaco Editor", diff --git a/packages/plugin-form/CHANGELOG.md b/packages/plugin-form/CHANGELOG.md index 8907c722cf..e4452e1076 100644 --- a/packages/plugin-form/CHANGELOG.md +++ b/packages/plugin-form/CHANGELOG.md @@ -1,5 +1,1316 @@ # @object-ui/plugin-form +## 17.7.0 + +### Minor Changes + +- 9c74902: Retire the form-view section `className` / `gridClassName` reads (objectstack#13626, + maintainer ruling 2026-09-01, director decision batch C). + + **Breaking, deliberately.** A `className` or `gridClassName` authored on a form-view + section no longer has any effect. Before this change an authored `gridClassName` + reached the section's field-grid `<div>` and an authored `className` reached the + section wrapper / divider header; both are now dropped at the renderer. + + The two keys sit on the SDUI-only side of the authorable boundary: `@objectstack/spec` + deliberately does not declare either on the form-view/section surface (its + `component.zod.ts` says so in as many words) and the authorable-surface ledger carries + no entry for them. The renderer nevertheless reached them off the parsed view through + `as any` at seven sites — the boundary declared on one side and crossed on the other, + with the two repos each deliberate and in opposite directions. + + Declaring the keys instead was weighed and **not** adopted: it would formally invite + free Tailwind strings into authored metadata, the exact class the boundary exists to + keep out — and per ADR-0065 / ADR-0080 (rev. 2026-06-30) utility classNames in runtime + metadata are never scanned by the build-time Tailwind, so they silently produce no CSS + anyway. Declaring them would have published a styling surface whose most obvious use + does nothing. If per-view styling becomes a real product need it gets an explicit + controlled token surface, not two leaked keys. + + **Migration.** Nothing in the measured corpora has to change. A census across the + objectstack corpus, this repo's corpus, and the hotcrm application found **zero** + authored uses of either key on a form-view section (201 authored section nodes reached, + 0 carrying either key). If you author them in your own metadata, move the styling to + the host application's own CSS, or to the form ROOT `className` — which is a different + key on a different node and is **unaffected** by this change. + + Six sites in `ObjectForm` (the tabbed / wizard / split / drawer / modal section maps and + the stacked section-divider) and one in `DrawerForm` (its own divider) stop copying the + keys. The omission is pinned behaviourally across all seven arms rather than by a source + grep, because `ObjectFormSection` still declares both keys — so a later uncast + `className: s.className` would type-check and silently restore consumption. +- 636b236: `navigateOnSuccess` is relative-only, escapes the interpolated id, and is deprecated in favour of `submitBehavior` + + The url contract for this key was undeclared: it was same-origin-guarded (so a same-origin + ABSOLUTE value was accepted), it interpolated `{id}` / `{recordId}` without escaping the + substituted value, and nothing said which of those was intended. The maintainer ruled it on + 2026-08-17: `navigateOnSuccess` is the pre-ruling ancestor of the `submitBehavior` family + rather than a second dialect, so as a compat alias it runs under the semantics + objectstack#7496 ruled for that family. + + **Relative paths only.** A same-origin absolute such as `https://own-host/record/{id}` is + now refused like any other out-of-contract value, rather than accepted and navigated at + browser level. The destination is authored metadata, which is exactly where an address + somebody else chose gets copied in. Cross-origin and protocol-relative values were already + refused and still are; every relative shape that worked before still works. + + **The interpolated id is URL-escaped.** `/r/{id}` with an id of `a/b c` resolved to + `/r/a/b c`, silently growing a path segment, and a template of `{id}` let the id become the + whole destination. The substituted value now goes through `encodeURIComponent`, so a token + is a value in the path and never a way to add path structure. The template is the author's + and is untouched — only the id, which is data read off the written record, is escaped. + + Both halves are needed and neither implies the other: relative-only is a rule about where a + destination starts, so it cannot see structure injected further along; escaping runs only on + the substituted value, so it cannot see an absolute the author wrote out. + + This can only narrow what is reachable. Every destination the key now accepts is a relative + reference, and a relative reference cannot carry an authority, so it was already accepted by + the same-origin guard this replaces — no value that was refused is now followed. With every + accepted destination relative, the browser-level `window.location.assign` fallback at both + call sites became unreachable and was removed; an accepted destination goes to the injected + navigation seam, and the absent-seam fallback inside the shared hook is unchanged. + + **Deprecation.** `navigateOnSuccess` is marked `@deprecated` in favour of `submitBehavior`, + which already takes precedence over it and carries the richer `{{record.field_name}}` + interpolation. The `{id}` / `{recordId}` dialect keeps working for forms that already + declare it — the ruling converges the documentation and the semantics, not the spelling. +- 7a72422: Publish the create-payload rule from `@object-ui/plugin-form`'s entry, so a + second form renderer can call it instead of composing it by hand + (objectui#6059). + + Newly importable from `@object-ui/plugin-form` — two functions, nothing else: + + ```typescript + import { omitServerResolvedDefaults, isRequiredInForm } from '@object-ui/plugin-form'; + ``` + + - `omitServerResolvedDefaults(values, objectSchema)` — drop the keys a CREATE + payload must leave to the producer: a field whose declared `defaultValue` is a + runtime instruction (`NOW()` / `current_user`, or a CEL envelope) and whose + submitted value is empty. `ObjectQL.applyFieldDefaults` resolves a declaration + only for a field that arrives absent or null, so submitting a blank stores + `''` and silently defeats it. **Create-only** — the caller keeps the mode gate. + - `isRequiredInForm(field, isCreateForm)` — the `required` a form should + enforce, given the mode. Published as the pair's other half on purpose: + excusing a server-owned field from `required` and then submitting the key + anyway is not half a fix, it is no fix. + + Both are pure functions over plain data (no React, no registry). The rest of + `schemaDefaults.ts` — `seedCreateValues`, `schemaDefaultValues`, + `isSeedableDefault`, `isCreateFormMode`, `SeedContext` — stays module-private, + and `isRuntimeDefault` stays `@object-ui/core`'s to publish. + + No behaviour change. The console's `FormPage` now calls the published helper + instead of composing `isRuntimeDefault` + `isMissingForRequired` locally; its + create payload is decided identically before and after, pinned against the + deleted implementation over the full matrix of default shapes, value spellings + and both modes. +- 5173a5e: ⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on + the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read + this before upgrading if any of your form metadata gates on the session user. + + objectui#6010 bound the host predicate scope on the five authored-predicate call + sites in the components form renderer, so `current_user` (plus the ADR-0068 + `user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn` + there. Two other authored-predicate evaluators were still passing `undefined` + for that argument, so the same authored text meant two different things + depending on which surface opened the form (objectui#6110): + + - **`apps/console`'s form renderer**, on the authed internal route + `/forms/:name`. The internal route is a runtime record surface by ADR-0089 + D1's own words (*"runtime record surfaces bind `record` + `current_user`"*), + and its `visibleWhen` metadata is the same `*.view.ts` FormView the + object-view chain renders — so a role gate authored once behaved differently + depending on which route opened the form. + - **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`), + the gate that re-checks the whole declared field set at final submit because + `allowSkip` can jump past a step. Its docstring promises *"the same verdict + from all three rather than a second, divergent dialect"*, and since #6010 it + was the divergent one. + + **Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails + OPEN: a field on screen is what you get when the predicate resolves TRUE, when + the scope was never bound so the predicate faulted, *and* when the predicate is + broken. Those worlds were indistinguishable, so an app that authored a + `current_user` gate saw the field render and had no way to tell the rule was + inert. After this change the predicate is evaluated for real, and fields and + sections that have always been visible will disappear for the users the rule + excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user` + requiredWhen that has been silently not applying will now start holding submits. + + In the wizard the change is a fix in the user's favour as well: a required field + the wizard HID from this user was still counted as visible by the submit gate, + so the submit was refused on a control the submitter could neither see nor fill + in. + + **Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in + your form-view and object metadata that names `current_user`, and confirm each + predicate says what you actually want evaluated against `record` + + `current_user`. + + **The public anonymous form `/f/:slug` is deliberately unchanged.** It is + mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is + no authenticated principal, and no provider is mounted above it — so its scope + is empty and a `current_user` predicate authored on a public form still faults + and still fails open, exactly as before. Nothing new is declared to say so: the + two routes are told apart by which component mounts them. + + `@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider` + user normalisation, so every console surface that mounts the provider publishes + the same `current_user` shape rather than re-deriving it. +- 971d387: ⚠️ **Behaviour change: an authored `FormSection.visibleWhen` that has been doing nothing + will now START HIDING SECTIONS.** Read this before upgrading if any of your metadata + authors a section predicate. + + `@objectstack/spec` declares `FormSection.visibleWhen` and this repo's spec bridge maps it + through, but every plugin-form layout renders a section header as a virtual + `section-divider` pseudo-field and none of them copied the predicate onto it. On the + object-view chain — the create/edit modal, the drawer, the split form, and the full-page + record form — the key was declared, mapped, carried, and then dropped one hop before + anything could evaluate it. The section rendered unconditionally, with no diagnostic + (objectui#6111). + + **Why nobody noticed, and why the fix is felt as a regression.** `visibleWhen` fails OPEN: + a section that renders is what you get when the predicate resolves TRUE, when the predicate + never arrives, *and* when the predicate faults. Those three worlds were indistinguishable, + so an app that authored a section predicate saw its section render and had no way to tell + that the rule was inert. Every such app has been running with the rule switched off, and + some will have been authored — or simply grown used to — that state. After this change the + predicate is evaluated for real, and sections that have always been visible will disappear + for the users the rule excludes. + + This is the intended ADR-0089 contract being delivered, not a new capability: the key was + already declared, already documented, and already honoured by the console form renderer. + The object-view chain was the one that silently ignored it. + + **Before upgrading**, audit any `sections[].visibleWhen` in your form-view metadata and + confirm each predicate says what you actually want, evaluated against `record` + + `current_user`. A predicate that was written speculatively, or left behind after a rework, + now takes effect. + + **Measured scope of the hide.** The predicate gates the section's HEADER row. The renderer + treats `section-divider` as presentational and holds no association between it and the + fields that follow it, so a false predicate removes the heading and the section's fields + keep rendering. The console renderer (`apps/console`) drops the whole `<section>`, fields + included. That divergence is real, is pinned honestly by this change's tests rather than + implied away, and is filed separately — it needs a renderer-side grouping contract, not + another line in a layout. + + Two hops were dropping the key and both are repaired: `ObjectForm` rebuilds each section + key by key when it delegates to Split/Drawer/Modal (and `ModalForm`'s own `groups` map does + it again), so a key those maps did not copy never reached the layout at all; and the six + `section-divider` synthesis sites across the four layout files. + + `@object-ui/types` gains the matching `ObjectFormSection.visibleWhen` declaration. +- 5ef9c4f: The section grouping contract (objectui#6236, maintainer ruling 2026-08-27): a + `section-divider` row may now CLAIM its member fields — `FormField.fields: string[]`, the + same membership shape `FormFieldTab.fields` / `FormFieldPane.fields` already model — and + the form renderer then gates the WHOLE group on the divider's own visibility verdict + (`visibleWhen` / `visibleOn` / legacy `condition`). + + Before this, one authored `FormSection.visibleWhen` meant two different things: the + console renderer drops the whole `<section>` (heading and fields), while the plugin-form + chain's renderer treated `section-divider` as a purely presentational row and hid only + the HEADING, leaving the section's fields rendering (measured in objectui#6111, which + pinned that honestly rather than implying a guarantee it did not deliver). + + Ruled semantics, now pinned in `section-grouping-6236.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** (console precedent, 2026-08-22 + ruling after #5594) — a hidden section's values still submit. + - **A hidden section's fields skip client-side validation** — a user is never blocked by + an error pointing at a control they cannot see (the objectui#6110 defect shape); the + server-side contract remains the loud floor for genuinely-required data. A section + hiding mid-session also clears its members' stale errors, the way a field's own false + predicate already did. + - **A divider without a claim keeps the old contract** (its predicate gates only the + heading), so existing schemas are untouched. + + Both halves ride the mechanism the field-level predicate already uses (return `null`; + react-hook-form keeps the value and skips the unmounted control), so field-level and + section-level visibility cannot drift apart. The zod mirror (`FormFieldSchema`) declares + the key with the same scope note. + + `@object-ui/plugin-form` wires the producer half: all six `section-divider` synthesis + sites (ObjectForm's stacked simple path, ModalForm's sectioned and derived-fieldGroup + paths, DrawerForm's sectioned and derived-fieldGroup paths, SplitForm's panes) now stamp + the membership claim onto the divider they emit, from the RESOLVED member list — so an + authored `FormSection.visibleWhen` finally hides the whole section on the object-view + chain, matching the console renderer. The #6111 honest pin (`measured scope`) flipped + accordingly: it now pins heading-and-fields hiding together, and every per-layout DENIED + row asserts the claimed member as well as the heading. The derived-fieldGroup sites carry + the claim for uniformity but stay fail-open — the spec `fieldGroups` vocabulary has no + section-predicate slot to author. The tabbed arm's predicate slot (objectui#6237) is + designed to reuse this same grouping contract. +- 46f0bb4: The tabbed arm of the grouping contract (objectui#6237, same maintainer ruling as + objectui#6236): `FormFieldTab` gains the predicate slot the ruling named — + `visibleWhen?: string | { dialect?: string; source: string }` — so a section rendered as + a TAB PANEL (`ModalForm` `contentLayout: 'tabbed'`) can finally carry an authored + `FormSection.visibleWhen`. The tabbed layout synthesises no `section-divider` at all, so + the #6236 membership-claim mechanism had nothing to stamp the predicate onto and no slot + to copy it into; the predicate was silently dropped one hop before evaluation (measured + in objectui#6237's card). + + The form renderer evaluates the tab's predicate with the same record assembly the + field-level rules use (`ruleRecord` / `previousRecord` / host predicate scope, #6010), + fail-open, and when FALSE draws neither the tab's trigger nor its panel. Not drawing the + panel unmounts the claimed fields through the exact mechanism a field's own false + predicate uses, so the ruled hidden-group semantics are inherited rather than + re-implemented, and are pinned in `fieldtab-visiblewhen-6237.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** — a hidden tab's values still + submit. + - **A hidden tab's fields skip client-side validation** — a user is never blocked by an + error pointing at a control they cannot see; the server-side contract remains the loud + floor for genuinely-required data (#2959's trap, answered the same way for tabs as for + sections). A tab hiding mid-session clears its members' stale errors. + - **Deterministic re-selection**: a predicate hiding the ACTIVE tab activates the user's + pick if still visible, else the declared default, else the first visible tab — never an + empty panel — and the user's pick is restored the moment its tab is re-admitted. + - **No mid-interaction collapse**: whether the tabbed arm engages stays judged on the + DECLARED tabs, so a predicate hiding one of two tabs filters the strip (and hides the + tab's fields) instead of collapsing the modal into the stacked layout under the user's + cursor. With every tab hidden the strip is omitted; unclaimed fields still render. + - **A tab without the key keeps the pre-#6237 contract** (always drawn), so existing + schemas are untouched. + + `@object-ui/plugin-form` wires the producer half: `ModalForm`'s tabbed synthesis site now + copies the section's `visibleWhen` onto the tab it emits, and the #6111 layout matrix + gains the tabbed-modal rows (direct and via `ObjectForm` delegation). `TabbedForm` / + `WizardForm` still declare no section predicate in their own section configs — those arms + remain open on objectui#6237. +- 2da6441: `formType: 'tabbed'` now honours an authored section `visibleWhen` (objectui#6237). + + The tabbed arm of the one grouping contract ruled 2026-08-29 (option A). Before + this, an authored `FormSection.visibleWhen` was dropped on the tabbed route + while `split` / `drawer` / `modal` and the flat layout all honoured it — the key + never reached a renderer at all, so it did nothing. + + `TabbedForm` already synthesised the renderer's `fieldTabs`, which is the same + machinery the `modal` + `contentLayout: 'tabbed'` arm runs on. The predicate was + simply dropped at three points on the way there, and all three now carry it: + `ObjectForm`'s tabbed section map, `FormSectionConfig` (which declared no such + key), and `TabbedForm`'s `fieldTabs` synthesis. + + Because the arm reaches the existing evaluator, the three ruled semantics are + inherited rather than re-implemented beside it: a hidden tab's values still + submit, its fields skip client-side validation (so a required field on a hidden + tab cannot block a submit invisibly — objectui#2959's defect through a new + door), a predicate hiding the ACTIVE tab re-selects deterministically instead of + drawing an empty panel, and arm engagement stays structural on the DECLARED + tabs so a predicate cannot collapse the strip mid-interaction. + + Two boundaries are deliberate: + + - A single-section tabbed form never engages the tab arm, so it degrades to the + untabbed layout's own predicate mechanism — a chrome-less `section-divider` + claiming its members by name. Existing single-section forms are unchanged; the + gate is emitted only where a predicate was actually authored. + - Wizard STEPS still do not take a predicate, and now say so in the type: + `WizardStepConfig` omits the key, because a step predicate is a different + contract (step-boundary reactive against the ruled live-record reactivity, and + needing navigation and final-gate semantics none of this machinery supplies). + `ObjectForm` continues to report that gap at runtime for untyped JSON. +- 2d3fe73: Publish the parameter types of the entry's own exported functions, so a consumer can + name what it must pass (objectui#7324). + + `ChildObjectSchemaLike` and `FieldDefaultsSchemaLike` are now exported from + `@object-ui/plugin-form`. They are **type-only** additions — no runtime name is added + to the entry, which is pinned. + + **Why `minor`, not `patch`.** Nothing breaks and no behaviour changes, but two names + join the published surface of a published package. Additions are `minor` in this repo, + and a new public export is the kind of addition a consumer's lockfile-pinned range + should be able to see. + + **What was wrong.** Five exported derive functions (`deriveDetail`, `deriveColumns`, + `deriveFormFields`, `findRelationshipField`, `resolveInlineMode`) take a `childSchema`, + and the exported `omitServerResolvedDefaults` takes an `objectSchema` — and neither + parameter type reached the entry. A host with its own form renderer (the reason + objectui#6059 published `omitServerResolvedDefaults` in the first place) has to hold + that schema in a variable or a prop, and could not annotate it. Structural typing means + such a host still compiled by writing the shape out by hand, so the cost was not a hard + failure but a producer-owned shape restated in every consumer, invisible to every gate + until the producer's shape moved. The package README carried exactly that restatement, + and now imports the real name instead. + + **Renamed at the declaration site first, deliberately.** Both types were called + `ObjectSchemaLike`, in two files, and they are **not** the same type: the defaults one + pins the four field members its rule reads (`defaultValue`, `type`, `reference`, + `reference_to`), while the child one leaves a field value as `any` because the derive + functions read much more of it. Measured with `tsc`, they are mutually assignable + **only** through that `any` — replace it with `unknown` and the child → defaults + direction fails (TS2322) — so re-exporting either under the shared name would have put + a name on the public surface that already meant something else two files over, with + nothing in the name to say which. Neither old name was reachable from outside the + package (the package `exports` map has a single `.` entry and the entry never re-exported + them), so the rename is not a break for any consumer. + + **Not** `@object-ui/types`' `ObjectSchemaMetadata`: measured, it requires `name`, + requires a `type` on every field, and has no `reference_to` member — while + `isCurrentUserSeedField` honours both `reference` and `reference_to` on purpose. Adopting + it would have narrowed what these functions accept and dropped one of the two honoured + spellings, not widened anything. +- 3c9fca3: Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 3e853c9: Let a producer-marked refusal reach the drag-write surfaces (objectui#5902). + + The kanban card-move toast, the calendar drag-to-reschedule toast and the OCC + conflict dialog each substituted a generic string for a refusal the producer had + marked as user-facing (`userMessage`), so a user was told "Save failed" where the + application author had written a sentence addressed to them. All three now read + the marking through the shared `declaredUserMessage` reader, which covers both + places the adapter boundary parks it — the typed member on + `ConcurrentUpdateError` and the details bag on `DataApiValidationError`. + + Nothing unmarked changes: the reader answers `null` for it, so every existing + generic substitution — including the localized "not authorized" message that + keeps raw server diagnostics away from end users — still governs unmarked + refusals exactly as before. + + The two toasts substitute; the conflict dialog augments. Its description also + explains what the destructive "Overwrite" button does, which is affordance copy + that surface owns rather than a refusal message, so the marking leads and that + paragraph stays. +- 17ccec9: `object-master-detail-form` declines to fetch a detail collection whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. + + `childObject` is REQUIRED on `MasterDetailDetailConfig` and is what every downstream read is keyed + on — `deriveDetail(d.childObject, …)`, the child-schema cache, and the FK scope of each child + fetch. But a detail entry reaches the renderer straight off an authored schema, so a malformed one + arrives with the key `undefined`, and the resolve effect asked the data layer for it anyway. + Measured: mounting the block with a detail entry that carries no `childObject` issued + `getObjectSchema(undefined)` — a real backend receives a query for an object literally named + `undefined`, and whatever it returns becomes the console's problem. + + The resolve effect now skips such an entry and warns, leaving it in place so the grid card shows + its config hint and the row-state array stays index-matched. This is the choice `RelatedList` + already makes for the same class of missing key (*"has no referenceField/parentId — refusing to + fetch all rows"*), and the sibling child-schema-cache effect in this same component already spelled + it `.filter(Boolean)`; the three now agree. A detail collection that names its child object fetches + exactly as before. +- c9a7252: `record:line_items` declines to fetch the child schema of a panel whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. + + `childObject` is declared `required: true` on the block's registry entry and typed `string` on + `LineItemsPanelSchema`, but nothing enforces either — `inputs[].required` is designer metadata, and + the block has no spec schema — so a node reaches the renderer straight off an authored schema with + the key `undefined`, and the child-schema effect asked the data layer for it anyway. Measured: + mounting the block through the registry with `childObject` unset issued + `getObjectSchema(undefined)`, and a real backend receives a query for an object literally named + `undefined`. The effect's `.catch` then turned the answer into a null child schema, so the visible + outcome was a silently unsanitized child grid rather than an error. + + The effect now declines and warns, naming the key and what to set it to, and clears the cached child + schema so a later save is never sanitized against a previous object's fields. This is the choice + `RelatedList` already makes for the same class of missing key (*"has no referenceField/parentId — + refusing to fetch all rows"*), and the one `object-master-detail-form` makes on this exact key. A + panel that names its child object fetches exactly as before. +- 5f19b92: `record:line_items` declines to LOAD OR WRITE the rows of a panel whose child object it never resolved, instead of calling `find(undefined, …)` — the sibling site of the child-schema decline, in the same component. + + `LineItemsPanel` read `schema.childObject` at two sites. The first now declines; the row load still + asked the data layer to `find` an object literally named `undefined`, scoped by + `{ [relationshipField]: parentId }`. `load` guarded the *data source* and the *parent id* — the two + things `RelatedList` calls "can I scope this query" — but not the *object being queried*. + + Declining that fetch is not enough on its own, and this is the part worth reading: `load` owns + `loading`, and the panel branched `loading ? "Loading…" : !parentId ? "Save the record first…" : + <grid>`. So the moment the fetch declined, an unresolvable panel with a parent id bound fell to the + third branch and showed an **empty editable grid with an Add button, over an object that does not + exist** — a worse outcome than the fetch it replaced. Measured on the pre-fix component: one + keystroke in the grid's always-present ghost row materialised a row, which enabled Save, which + reached `batchTransaction([{ object: undefined, action: 'create', data: { qty: 3, invoice: 'inv-1' } }])`. + The bad *read* was one keystroke away from a bad *write*. + + An unresolvable panel therefore gets its own render branch — a config hint naming `childObject` and + what to set it to, following the precedent `object-master-detail-form` set for this exact key and + `AdvancedChartImpl`'s refusal placeholders. It is checked ahead of `loading`, because nothing is + pending: the schema itself already says the panel can never resolve, so there is no honest moment at + which "Loading…" is true. `save` takes the same one-line guard, for the one route the render branch + cannot close — a schema edited to drop `childObject` while rows are already dirty. + + A panel that names its child object loads, renders and saves exactly as before. +- e0b289d: An authored section `visibleWhen` on `formType: 'tabbed'` or `formType: 'wizard'` now + **reports** that the layout cannot honour it, instead of being silently dropped + (objectui#6237). + + `ObjectForm` rebuilds each section key by key when it delegates to a layout, so a key + the map does not copy never reaches a renderer at all. Three of those maps copy + `visibleWhen` (`split` / `drawer` / `modal`, objectui#6111) and the flat arm carries it + on the `section-divider` pseudo-field — but the `tabbed` and `wizard` maps copy nothing, + so an author writing the key on those two arms watched it do exactly nothing, with no + signal anywhere. That silence is the defect this ships against. + + The two arms now log a warning naming the layout and the sections whose predicate is + being dropped, through one shared message builder so they cannot drift apart. + + **This changes no rendering behaviour** — the predicate is still not evaluated on those + arms. It is the interim half of a maintainer ruling (2026-08-29) that the real repair is + a **design** task: one renderer-side section/group contract with a predicate slot, + designed once for every layout arm (tabbed / TabbedForm / WizardForm / flat) rather than + patched arm by arm. The ruling requires the diagnostic to land first, so the gap stops + being invisible while that contract is designed. + + Deliberately silent on the arms that work, so the warning stays worth reading: + + - `split` / `drawer` / `modal`, and the flat layout — all honour a section `visibleWhen`. + - `ModalForm` with `contentLayout: 'tabbed'` — honours it through the real + `FormFieldTab.visibleWhen` slot that landed in objectui#6619. "Tabbed" names two + different things on this card; only `formType: 'tabbed'` (`TabbedForm`) is inert. + - A master-detail parent, which re-enters `ObjectForm` through its own parent schema — + the report is left to that inner pass, where the real layout is decided (a + master-detail `wizard` parent renders `simple`, which honours the key). Reporting at + both would double-report the tabbed parent and false-report the wizard one. + + No authorable key is added anywhere: declaring `visibleWhen` on a type whose renderer + ignores it is the defect this card family exists to close, and the shared + `FormSectionConfig` that `WizardForm` uses for its steps makes that trap concrete. +- 3b9c774: Split `WizardStepConfig` off `FormSectionConfig`, and correct the section-predicate + support table (objectui#6237, maintainer ruling 2026-08-30). + + `WizardForm` typed its steps as `Omit<FormSectionConfig, 'visibleWhen'>` — a + subtraction from the TabbedForm section type, which is the predicate-CARRYING + type. That defended the one key it named and left the mechanism open: every key + added to `FormSectionConfig` reached a wizard step by default, so the next + predicate in the same family (`readonlyWhen` / `requiredWhen`, already this + package's field-level vocabulary) would have handed the wizard a silent slot its + renderer does not read — the declared-but-unenforced shape the ruling split the + types to stop. + + `WizardStepConfig` is now declared independently in `WizardForm.tsx`, which is + simply what `SplitFormSectionConfig`, `ModalFormSectionConfig` and + `DrawerFormSectionConfig` already do: each layout owns its group shape, documents + `className` / `gridClassName` in its own terms, and declares `visibleWhen` only + where its renderer honours it. The derivation flips from subtractive to additive + — a key is authorable on a wizard step only if someone writes it there. + + No behaviour change and no key added or removed: `WizardStepConfig` exports the + same key set it already had, and `visibleWhen` on a wizard step literal was, and + remains, a compile error. What is new is that it stays one for the whole + predicate family, pinned by a type-level assertion that fails the build if any + `*When` key ever appears on the step type. + + Documentation repair in the same stroke: the support table in the README and in + `content/docs/plugins/plugin-form.mdx` still said `formType: 'tabbed'` sections + drop the predicate. That stopped being true when the tabbed arm landed — the row + now reads **Yes**, the surrounding prose no longer claims two inert arms or a + diagnostic that fires for `tabbed`, and the wizard row stays **No**, which is + still exactly true. +- 1c19722: `object-master-detail-form` now renders a config hint naming `childObject` for a detail + collection whose child object never resolved, instead of `Loading columns…` forever + (objectui#6360). + + `MasterDetailForm` already declines to fetch the schema of such a detail (objectui#5940) + and returns the entry unresolved, which is correct — asking the data layer for an object + literally named `undefined` is what that guard removed. But the decline is precisely the + guarantee that the entry's columns can never arrive, and the render branch it fell into + read `!d.columns?.length ? <p>Loading columns…</p>`. The author was shown a + spinner-shaped message that was permanently, unfixably wrong, and that never named the + key they had to set. + + The `!d.childObject` case now takes its own branch, checked **before** the columns arm + because nothing is pending — there is no first paint where "loading" is honest. The copy + and structure are `LineItemsPanel`'s, which took the same branch for the same key in + objectui#6194 / PR #6359; the two components had been disagreeing about what an author + sees for the identical authoring mistake, and the weaker of the two was the one that read + as the precedent. The hint carries its own `data-testid` (`md-detail-no-child-object`). + + Two source comments — at the decline itself and at the resolver's `catch` — asserted that + "the grid card shows a config hint". They were false, and following them cost a reader a + run of the component. The first is now true and says so. The second is **corrected rather + than made true**: a detail whose schema fetch *threw* does name a child object, so it + skips the new branch and still lands on `Loading columns…`. Distinguishing that from + "still in flight" needs per-entry error state the resolver does not keep, so it is filed + as objectui#6372 and the comment now points at it instead of promising a hint that is not + rendered there. + + No spec or schema change: `childObject` is already REQUIRED on `MasterDetailDetailConfig`. + This is renderer-side reporting of an authoring error that the type system cannot catch, + because a detail entry reaches this renderer straight off an authored JSON schema. +- faa863d: `MasterDetailForm` gives every detail collection a per-entry record carrying its own + identity and its own resolution status, closing two defects that both came from the same + absence (objectui#6372, objectui#6371). + + `resolvedDetails` was a plain `MasterDetailDetailConfig[]` with no per-entry metadata, so + both *what happened to this entry* and *which entry is this* were inferred from the + entry's position in the array. One record answers both, which is why they land together — + either one alone would have reshaped this structure and the second would then have + rewritten the first. + + **objectui#6372 — a detail whose schema fetch threw sat on "Loading columns…" forever.** + The resolver's `catch` returned the entry unchanged, and an entry with no `columns` is how + *still in flight* is represented too, so the two states were indistinguishable and the + render branch showed the same spinner-shaped message for both. For the failed one it never + ended: the fetch is not retried, so nothing could ever replace it. Entries now carry a + resolution status, and a failed one renders a refusal placeholder naming the child object + whose schema could not be loaded (shaped on `AdvancedChartImpl`'s refusal placeholders — + `role="status"`, because a refusal is a state, not an alert). Measured before the fix + rather than read from source: a detail whose `getObjectSchema` rejects rendered + `<p>Loading columns…</p>`. + + The thrown error is no longer discarded. The bare `catch` threw away the whole diagnosis, + so whoever debugged this had neither a message nor a stack; the decline arm next to it has + warned since objectui#5940, and this arm now matches it and passes the error object + through. + + ⭐ The fetch and the derive are caught **separately**, because they are different failures + with different truths to tell. A schema that loads fine and then yields no relationship + field is a configuration error, and calling it a load failure would be false. That arm's + render is deliberately unchanged; only its error stops being swallowed. + + **objectui#6371 — a declined entry had no identity across a reorder.** There was no + duplicate-key collision: the map index is unique among siblings by construction, so two + declined details keyed as `undefined-0` and `undefined-1`, distinct. The real defect is + that for a declined entry the data half of that key is `undefined`, leaving position as + the entry's whole identity — and the row-state store was addressed the same way, seeded + once at mount and never re-synced when the authored config changed. Reordering or removing + an entry therefore handed a collection a different collection's rows. + + Entries now carry an id synthesized once from the incoming config: the child object for a + named collection, and the authored position for a declined one, which has no other + identity to offer. Row state is keyed by that id, so a collection can only ever read its + own slot. Three reads were affected, not the one the report named: + + - the grid value, which showed the wrong collection's rows; + - the document **subtotal** reducer, so a reorder did not merely mis-associate a grid, it + mis-computed the total; + - the batch payload on save, which read + `details.filter(d => d.relationshipField).map((d, i) => state[i])` — after the filter `i` + indexed the filtered array while the row state was indexed against the full one, so a + declined entry above a real collection shifted every read below it by one and that + collection's rows were **silently dropped from the transaction**. Data loss on save, not + a display defect. +- fd814d6: `MasterDetailForm` shows a config hint naming `relationshipField` for a detail collection + whose child schema **loaded fine but could not be derived from**, instead of a permanent + `Loading columns…` (objectui#6394). + + This is the third and last arm of the same resolver to be closed. `deriveDetail` throws + when no lookup/`master_detail` field on the child object references the parent — a + configuration error whose remedy is a key the author writes. The `catch` returned the + entry unresolved, so it fell through to `!d.columns?.length ? <p>Loading columns…</p>`, + and that message never ended: the derive is not retried, so those columns could never + arrive. Same unbounded-wait-shown-as-a-spinner family as objectui#5940 / objectui#6188 / + objectui#6194 / objectui#6360 / objectui#6372. + + The entry now carries `status: 'underivable'`, and the renderer gives it a branch of its + own that names both ends of the relationship it could not find and the key to set: + + > Could not work out how `po_line` links to `purchase_order`: no lookup or master_detail + > field on it references the parent. Set `relationshipField` on this collection to the + > field that holds the parent record. + + ⛔ Deliberately **not** objectui#6372's refusal placeholder, which states the schema could + not be loaded — false for a schema that loaded fine. The two failures keep separate copy + because they have different remedies: one is "check the object exists and reload", this one + is "set this key". The thrown error is still logged with its stack (objectui#6372), since + the placeholder shows the author the key rather than the raw message. + + Behaviour is unchanged for the other two arms and for a detail that is genuinely still + fetching — that one keeps `Loading columns…`, where the message is true. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- ecd9cb2: Wizard view v1, the objectui half (Card R, objectui#6985) — alignment + pins for the + ruled `type: 'wizard'` tightening (objectstack#13622 D1–D8, maintainer ruling + 2026-08-31; spec half objectstack PR #13733). + + The renderer was already aligned: `WizardStepConfig` carries no predicate/collapse + keys (objectui#6237's ruled split), the wizard route drops-and-reports an authored + step `visibleWhen`, and `allowSkip` has been navigation-freedom-not-validation- + exemption since #2959. This card lands the residue: + + - **metadata-admin view create seeds one starter step for a wizard** (app-shell + `anchors.ts`): the create body used to emit `sections: []` for every form type, + which for `type: 'wizard'` is exactly the shape the tightened spec refuses at + parse (D7 — a stepless wizard silently rendered as a plain simple form). Same + seed-the-required-shape move the flow anchor makes for its `type` enum + (objectui#2326). Other form types keep the bare `[]` — only the wizard variant + refuses emptiness. + - **`@object-ui/types` TSDoc states the ruled wizard boundary** where the shared + section/form types restate the form-view family: `ObjectFormSection.visibleWhen` + / `collapsible` / `collapsed` name the wizard drop + spec-door refusal; + `ObjectFormSchema.sections` states sections-ARE-steps and array-order-is-step- + order; `allowSkip` states the D4 semantics. Type SHAPES are unchanged — the + spec's own ruled mechanism is a parse-time refinement over the single shared + section schema (D2 option A), which these types mirror at the type level. + - **Consumer-side behaviour pins** (`wizardRuledSemantics-6985.test.tsx`): the + wizard-inert step keys are dropped, never honoured (a denying `visibleWhen` + does not remove a step; `collapsible`/`collapsed: true` produce no collapse + affordance, with a positive control on the affordance probe); the empty-steps + wizard's measured degradation to a simple form is pinned as the shape the spec + door now refuses (one-step wizards stay legal — no arity floor); array order + is step order (with a reversed-array control). + - **Installed-spec door pins** (`wizardSpecDoor-6985.test.ts`), gated on a + capability probe of the installed `FormViewSchema` rather than a version + string: the post-Card-S half (refusal messages, prescriptions, the authored- + `false` collapse boundary, the wizard-scoped control) activates by itself on + the lockfile bump that brings the tightening in; until then the pre-tightening + half records the 17.2.x accept-set it measured. `steps:` is pinned refused on + every spec line. + + No teaching material — the #13337/#13086 fence lifts only after both halves land; + docs changes here are TSDoc/comments only. +- 7dedec6: A master-detail form no longer ends on a screen asserting both a failure and a success + (objectui#7345). + + `MasterDetailForm` raised its two save outcomes — `handleSaved`'s confirmation and + `handleError`'s refusal — under sonner's auto-generated ids, so nothing held a handle on + the previous attempt's toast. A save the server refused left its error toast on screen, + and when the user corrected the input and saved again inside that toast's lifetime the + confirmation landed *beside* the refusal, exactly the objectui#7252 defect on a renderer + that fix did not touch. + + Both outcomes now travel under one stable per-form id (`React.useId()`-scoped, the same + spelling the form renderer and the console's `FormPage` publish under), and each save + attempt retires the previous attempt's toast before it starts: + + - with no host `onSuccess` (SDUI / embedded hosts), the confirmation supersedes the + refusal instead of stacking beside it; + - with a host `onSuccess` (the console), where the built-in confirmation is deliberately + skipped, the dismissal is what retires the refusal — otherwise it stood over a save + that had succeeded. + + Toast durations are unchanged: this is about supersession, not lifetime. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 425762e: `object-master-detail-form` declares `formType` as a closed vocabulary instead of a bare `string`. + + The block declared `formType` as `type: 'string'` while the sibling `object-form` declared the + same key as an `enum`, and both funnel into the renderer that switches on those variant names. A + value outside the vocabulary therefore matched no branch and fell through to the flat field list + with no diagnostic — measured, a `formType` of `'wizzard'` renders the parent half with its + authored sections silently gone. + + The declared set is `simple | tabbed`, measured against the master-detail composition rather than + copied from the sibling's six: `drawer` and `modal` host the parent half in a portal dialog outside + the master-detail container, so its single bottom Save bar has no form to submit; `wizard` mounts + only the current step's fields and turns that Save bar into a `Next`; `split` renders inline but + persists through `dataSource.create` instead of the atomic batch. + + Authoring-surface only. The manifest, the JSX-page compiler and the save gate now report an + out-of-vocabulary value as `invalid-enum`; rejection at publish time remains `@objectstack/spec`'s. +- 83ec618: `README.md`'s "Not a `FormField` key" table said a field-level `className` is + "read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier + holds only for the renderer's *explicit* read — `className={fp.className}` on + the `section-divider` branch of + `packages/components/src/renderers/form/form.tsx`. The same renderer forwards + every key it did not destructure, and `className` is not among the names taken + off the field config, not among the ones `stripRendererOnlyProps` removes, and + so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input` + branch spreads it onto `<Input>`. A field-level `className` therefore lands + visibly on ordinary built-in controls, and a reader taking "exactly one" + literally concludes the opposite of what the code does (objectui#5131). + + The cell now describes the contract rather than the reader count: an undeclared + key still rides the props spread down to whichever component the field resolves + to, nothing in the contract promises that, and a registered widget honours it + only if it happens to spread its leftover props — the wording the docs site + already ships, so the two sources agree again. The advice in the row is + unchanged and was never wrong (`span` / `colSpan` for width, + `FormSchema.fieldContainerClass` for the grid), and the explicit + `section-divider` read is kept, now named as explicit. + + This is a documentation fix to a file `plugin-form` publishes to npm, which is + why it carries a version: the npm landing page only picks up the correction on a + release. No behaviour, export, type, or `dist` byte changes. +- 43ca9d5: `SimpleObjectForm`: consult a declared `submitHandler` before the inline-fields carve-out + + `ObjectFormSchema.submitHandler` is documented as handing the collected values to the host INSTEAD of calling `dataSource.create` / `dataSource.update`, so a form that declares it has a submit target with or without an adapter. `SimpleObjectForm.handleSubmit` nevertheless opened with the inline-fields carve-out (`hasInlineFields && !dataSource`), which returned before the persistence chain: a host that had declared it owns the write was never asked, and `onSuccess` confirmed a write that never happened (measured `onSuccess 1 / submitHandler 0`). + + The carve-out now fires only when no `submitHandler` is declared, and the "no submit target" refusal moved into the persistence chain after the seam — the shape the five variant renderers already use, reusing their shared refusal from `submitTarget.ts` rather than a private copy. A form with inline fields and no seam is unchanged: its `onSuccess` is still the write. +- ba306e3: Honour the declared `submitHandler` seam in every form variant, not just the simple one. + + `ObjectFormSchema.submitHandler` is documented as the seam a host uses to own persistence: the form validates and hands the collected values over instead of calling `dataSource.create` / `dataSource.update`. `ObjectForm` forwarded the key into every variant it routes to, but only `SimpleObjectForm` read it — `TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` persisted directly. + + **Behaviour change on a persistence path.** A master-detail parent half rendered `tabbed` (or `split`) now commits through the atomic `batchTransaction` together with its child collections, instead of writing the parent independently through `dataSource.create`. Previously the child leg was never attempted on those layouts: the parent was committed alone, the entered line items were silently discarded, no compensation ran, and a success toast confirmed the save. A failing child leg now leaves no committed parent, on every layout that renders the parent half inline. + + `WizardForm` additionally skips its own default success toast / redirect arms when a `submitHandler` is present, matching `ObjectForm`, so a host that owns the write also owns the outcome. + + The `object-master-detail-form.formType` vocabulary is unchanged and stays `simple | tabbed`. +- 26a2238: `navigateOnSuccess` now honours a mounted host, and says so when its destination is refused + + `ObjectForm` and `WizardForm` consume `navigateOnSuccess` through + `resolveSuccessNavigate`, and both arms travelled to an accepted destination with a bare + `window.location.assign`. A rooted path such as `/apps/x/o/record/{id}` assigned that way + resolves against the ORIGIN root, so under a host mounted at a sub-path (the framework CLI + configures one for every embedded deployment) an authored in-app destination left the + application. Both arms now route an app-relative destination through the injected + navigation seam both components already held for `submitBehavior.url`, so a mounted host's + basename is applied. With no host seam the behaviour is byte-for-byte what it was — a host + with no router has no basename, so origin-rooted resolution is already correct there. A + same-origin ABSOLUTE destination also keeps browser-level navigation: the seam's declared + input is an application-relative path, and an author who spelled out a whole address asked + for that address. + + A declared `navigateOnSuccess` whose destination is refused — a mistyped value, or a written + record carrying no usable id — used to produce a success toast identical to the one a form + with no `navigateOnSuccess` produces, so the navigation failed with nobody told. That toast + now carries a note that the declared navigation did not happen, and the template the author + wrote is logged for them. The write genuinely succeeded, so this stays a success rather than + becoming an error state. + + Which destinations are ACCEPTED is unchanged: the same-origin guard, the `{id}` / + `{recordId}` dialect and the unescaped interpolation are the subject of an open contract + question and are deliberately untouched here. +- 5d79faf: Variant forms refuse a submit that has nowhere to go, instead of reporting success + + `TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` each opened + `handleSubmit` with `if (!dataSource) { await schema.onSuccess?.(data); return data; }` + — a success signal emitted without consulting a declared `submitHandler` and without + persisting anything. Through `MasterDetailForm`, whose parent schema declares both + `submitHandler: submitViaBatch` and `onSuccess: handleSaved`, that produced a success + toast and, in create mode, a form reset clearing values nobody wrote. + + All five now answer the question the same way `SimpleObjectForm` and the `object-form` + element gate already do. A form has a submit target when it has a `dataSource` or a + declared `submitHandler`; with neither, the one legitimate shape is inline fields — + a non-empty `customFields`, or `sections` whose fields are all inline runtime + `FormField` objects — whose `onSuccess` is the write. Anything else throws + `DataSource is required for form submission (inline mode not configured)`, which + reaches `schema.onError` and is rethrown. A declared `submitHandler` is consulted + first, so a host that owns the write is never bypassed for want of an adapter it + never needed. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index c3b40d9de3..55d394f0ee 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-form", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Form plugin for Object UI", diff --git a/packages/plugin-gantt/CHANGELOG.md b/packages/plugin-gantt/CHANGELOG.md index 88b03a3154..d025355fae 100644 --- a/packages/plugin-gantt/CHANGELOG.md +++ b/packages/plugin-gantt/CHANGELOG.md @@ -1,5 +1,1327 @@ # @object-ui/plugin-gantt +## 17.7.0 + +### Minor Changes + +- 00f3eb5: `ObjectGanttSchema` declares the ten gantt keys `ObjectGantt` actually reads + (objectui#5903, triage 2026-08-24). Every one is a real, working, documented + feature — `readOnly`, `mobileReadOnly`, `markers`, `navigation`, `skipWeekends`, + `holidays`, `criticalPath`, `showBaselines`, `persistLayout`, `viewName` — and + none of them was discoverable from the published type, because all ten were read + as `(schema as any).K`. The cast was the load-bearing part: it kept the read + invisible to `tsc`, to the zod mirror and to the designer's registry `inputs`. + + Both halves move together. The TS declaration (`packages/types/src/objectql.ts`) + and its zod mirror (`src/zod/objectql.zod.ts`) gain the same ten keys at the same + requiredness — all optional — so the `zod-mirror-parity` ratchet stays at zero + drift for this pair and no `KnownDrift` entry is added. `navigation` is taken + from `@objectstack/spec`'s `NavigationConfigSchema` by reference rather than + restated, matching `ObjectGridSchema.navigation`. + + `ObjectGanttProps.schema` is retyped from `ObjectGridSchema` to + `ObjectGanttSchema`. That is what makes the declaration load-bearing: the ten + keys are not grid keys, so with the old prop type, dropping the casts would have + left the reads landing on `BaseSchema`'s index signature — the same invisibility + in different syntax. The grid-style `{ gantt: { … } }` block is unaffected; + `getGanttConfig` reads it through that index signature exactly as before, and the + registered renderer passes `schema: any`, so no runtime shape is turned away. + + Accept-set change, stated plainly: a **declared** key is now type-validated, so + `readOnly: 'yes'` is refused where it used to parse green — the same narrowing + objectui#5074 landed for `viewMode`. An **undeclared** key is still accepted: + `BaseSchema` is `.passthrough()` and carries an index signature (objectui#5155's + structural ceiling), so declaring these ten did not buy rejection of a + misspelling. `packages/types/src/__tests__/gantt-declared-keys.test.ts` pins both + halves so neither can be misread. + + The eleventh reported key, `label`, needed no declaration — `BaseSchema` already + carries it — so only its cast was dropped. +- 75bd83d: `ObjectGanttSchema` declares the flattened `GanttConfig` face `ObjectGantt` + actually reads (objectui#6051). `getGanttConfig` has two branches: when + `startDateField` and `endDateField` are both present at the TOP level it builds + its config from top-level keys and returns early; otherwise it reads the `gantt` + block. Everything the first branch reads was undeclared — and unlike + objectui#5903's ten, none of it was hidden behind a cast. `BaseSchema` carries + `[key: string]: any` (objectui#5155's structural ceiling) and the helper's + parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as + `any` with no syntax anywhere to grep for. That is also why the census here is an + AST enumeration and not a compile-and-observe: an index signature absorbs every + literal name, so annotating the parameter compiles clean while enforcing nothing. + + **27 keys join the declared surface, each additive and each with a live read + site.** 24 flattened `GanttConfig` members — `colorField`, `borderColorField`, + `dependenciesField`, `parentField`, `typeField`, `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `tooltipFields`, `baselineStartField`, + `baselineEndField`, `groupByField`, `resourceView`, `assigneeField`, + `effortField`, `capacity`, `quickFilters`, `autoZoomToFilter`, `timeSegments`, + `interactions`, `exportFileName`, `timeZone`, `dependencyTypes` — plus the three + query keys the fetch path reads, `staticData`, `filter` and `sort`. Nothing is + declared that the renderer does not consume. + + **`GanttConfig` itself gains nine members and is a published type**, exported by + name from `packages/types/src/index.ts`: `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, + `timeZone`, `exportFileName`, `interactions`. The entry file's diff is empty only + because the export list already named the type — the widening happened at the + declaration. + + **The 28th measured key, `gantt` (the block face), is deliberately NOT declared** + — see the closing section. + + The 24 are DERIVED from `GanttConfig` rather than restated, so the flat spelling + cannot fork from the block spelling, and the invariant is pinned in the type + system: every key of `GanttConfig` must be declared at the node's top level. + Making that derivation possible moved nine members — `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, + `timeZone`, `exportFileName`, `interactions` — out of `plugin-gantt`'s + package-private `GanttConfigEx` and into `@object-ui/types`' `GanttConfig`. They + are a MOVE, not new vocabulary: the `gantt` block already honoured all nine, and + a type private to the plugin could be referenced by neither authoring face. + + Both halves move together, as in objectui#5903: the TS declaration and its zod + mirror gain the same 27 keys at the same requiredness (all optional), the + spec-modelled ones taken from `GanttConfigSchema.shape` by reference, so the + `zod-mirror-parity` ratchet stays at zero drift for this pair and no `KnownDrift` + or `UnmirroredDeclared` entry is added. The mirror builds the flat face and the + `gantt` block from one field map, so they are one schema expressed twice. + + Accept-set change, stated plainly. All 27 keys are additive — every one is + optional, and nothing previously legal loses its slot. What changes is that a + **declared** key is now type-validated, so `capacity: 'one'` and + `summaryExtent: 'parent'` are refused where they used to parse green. An + **undeclared** key is still accepted — `BaseSchema` is `.passthrough()`, so this + bought no rejection of misspellings. There is no narrowing anywhere in this + change. + + **`gantt` is severed on purpose (objectui#6475), not overlooked.** It is the 28th + key of the measured residue and a genuine read — `getGanttConfig`'s second branch + honours it in full — but it is the one key whose declaration would NOT have been + additive. It has no mirror entry today, so a block rides through `.passthrough()` + unvalidated; declaring it as `GanttConfig` means it gets parsed against the spec's + `GanttConfigSchema`, which REQUIRES `startDateField`, `endDateField` and + `titleField`, and `ObjectGanttSchema` reaches the CLI's `validate` / `check` + through `AnyComponentSchema`. A published CLI's refusal behaviour is decided on + its own card, where reviewers can see what they are approving; objectui#6475 + carries the full measurement, including the case FOR enforcing it (the renderer + already feeds that block to `GanttConfigSchema.safeParse` and warns, so enforcing + restores declared = enforced rather than inventing a contract). Today's behaviour + is pinned in the test file so the omission is a measured state, not a silent gap. + `packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts` pins both + halves so neither can be misread. + + Which face WINS is unchanged and was not decided here: the flat branch is checked + first and returns early, so a node carrying both spellings still renders the flat + one. (`plugin-map` had the opposite precedence ruled on in objectui#5018; no + equivalent ruling exists for gantt.) +- d7acad6: **plugin-gantt: the `gantt` block now outranks the flat top-level spelling, and + the losing face's keys are named instead of dropped.** + + `getGanttConfig` checked the flattened top-level spelling first and returned + early, so a node carrying both spellings rendered the flat one and every key + inside an authored `gantt` block was discarded with **no diagnostic** — not even + the `GanttConfigSchema.safeParse` warning, which sat behind that early return. + + `plugin-map` had the identical two-faces shape ruled the other way (maintainer + ruling on objectui#5018, 2026-08-17, landed in PR #5156): the block wins, with a + dev-mode warning naming the ignored top-level keys. objectui#6469 inherits that + ruling, so the two sibling view plugins now answer the same question the same + way. + + What changes: + + - A node carrying **both** spellings now renders the **`gantt` block's** values. + The block is taken **whole** — the flat keys are not merged into it. + - In dev, `[ObjectGantt] … these top-level keys are IGNORED: …` names every + shadowed flat key, once per distinct shadowing. + - Nothing else moves. A node with only the flat spelling, or only a block, is + read exactly as before. + + **Producer-safe:** `ObjectView` (`case 'gantt'`) and `ListView` (`case 'gantt'`) + both flatten `options.gantt` onto top-level keys and emit **no** `gantt` key, so + every gantt reached through either view layer still takes the flat branch, and + the new warning cannot fire on that path. This is the same producer check the + `plugin-map` flip pinned, re-run on today's `main`. + + This **supersedes** the precedence sentence in the objectui#6051 changeset + (`.changeset/6051-gantt-flat-config-declared-keys.md`), which recorded the flat + branch winning — accurate for that change, which deliberately did not touch + precedence, and reversed by this one. +- e3d117a: `ObjectGantt` and `ObjectTree` resolve a relationship target only from the two + spellings a contract carries, dropping the third one no contract declares + (objectui#6837, second slice). + + - `ObjectGantt`'s quick-filter option fetch was + `fd?.reference_to ?? fd?.reference ?? fd?.referenceTo`; it is now + `fd?.reference_to ?? fd?.reference`. + - `ObjectTree`'s `detectParentField` was + `def?.reference || def?.reference_to || def?.referenceTo`; it is now + `def?.reference || def?.reference_to`. + + **Accept-set move — a def carrying ONLY `referenceTo` stops resolving a target + at these two seams.** Concretely: the gantt quick filter for that field falls + back to the distinct values present in the loaded rows instead of fetching the + referenced object's full domain, and the tree stops auto-detecting that field as + its parent pointer, so records render as a flat forest unless `parentField` is + configured explicitly. Nothing else changes; the two surviving arms are + untouched. + + Two things bound that move: + + - Any def that entered through the ingestion choke point is unaffected. + `normalizeSchemaReferenceKeys` reads `reference_to ?? reference ?? + referenceTo` and stamps both snake_case keys, so a `referenceTo`-only def + arriving via `MetadataProvider` or `ObjectStackAdapter.getObjectSchema` + already carries `reference_to` before either component sees it. Only a def + that bypassed that door entirely is affected — and that door is not total: + `getObjectSchema` is a required member of the published `DataSource` + interface, and both components call it on the generic `dataSource`. + - No contract declares the deleted spelling. `@objectstack/spec` 17.2.0's + `FieldSchema` refuses `referenceTo` by name with `unrecognized_keys`, carrying + its own "Did you mean `referenceTo` -> `reference`?" rename, and `referenceTo` + is additionally a tombstone in `RETIRED_FIELD_KEY_TOMBSTONES` (objectui#6041) + at all three strip sites, so the designer read door removes it before a draft + round-trips. + + A repo-wide structure-walk producer census found **0** emitters of `referenceTo` + reaching either seam, measured in the cell these components read (a value inside + an object schema's `fields` container) against controls `reference` (92 hits / 36 + files) and `reference_to` (52 / 36) hot in the same pass over the same cells; + the only two in-cell hits are negative fixtures of the retirement machinery, + asserting the read door strips the key. Neither `plugin-gantt` nor `plugin-tree` + emits `referenceTo` anywhere, while both packages' own fixtures are hot on the + surviving spellings. + + Pinned by `ObjectGantt.referenceArms-6837.test.tsx` and + `ObjectTree.referenceArms-6837.test.tsx`, which keep the live arms green beside a + named refusal for the deleted key. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- bf244f4: Size the gantt task list from its container, and give a row's dates ONE predicate + (objectui#7204, objectui#7224; maintainer ruling 2026-09-02, option Y). + + **A row could show no dates at all.** Two gates decided whether a row's dates were + painted, and they read two different widths. The Start/End columns were gated on the + container-derived task-list width; the `8/26 → 9/2` sublabel under the title was gated + by the component's own `@media (min-width: 640px) { .gantt-sm-hidden { display: none } }` + rule, which reads the viewport. Between a 640px and a 1023px container both were shut, + so the row's dates existed in the DOM twice and were painted zero times. The same hole + opened at any width once the splitter was dragged under the threshold. + + The sublabel now renders on exactly the complement of the Start/End columns, both from + the same container-derived width, and the media rule is gone. A row always carries its + dates one way or the other. + + **And the task list no longer caps at 320px.** From a 1024px container up, the pane + takes 3/8 of the container clamped to `[320, 560]` instead of a flat 320. At 1440 that + is 540px, which leaves the title 287px with the Start/End columns still painted — a + 40-character title measures 262px in the row's font, so real-world task names stop + truncating to about seven characters while several hundred pixels of chart sit empty. + Measured in Chromium at a 1440px container: title 53px before, 287px after. + + The Start/End threshold moves from an estimated 280 to a derived **412** — 32 row + padding + 160 for the two columns + 28 for the open-details slot + 32 of title + furniture + 160 minimum title, each term traced to the markup that spends it. Below it + the sublabel carries the dates. One consequence worth stating: between a 1024px and a + 1097px container the columns are now off and the dates ride the sublabel, which trades + two 80px date cells for a title that grows from 67px to 291px. + + Not fixed here, and unchanged: a row's `depth * 14` indent is unbounded, so no single + default keeps a deeply nested row legible. +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- 199d31b: **`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both + gantt renderer branches honour it** (objectui#5074, maintainer ruling + 2026-08-19: declare-and-wire; the spec half landed first upstream). + + - `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, + DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` + enum by reference, so the member list cannot drift. Deliberately no + default: an omitted `viewMode` keeps letting a persisted layout + (`persistLayoutKey`) seed the timeline granularity before the renderer's + `'day'` fallback. + - The timeline branch (`GanttView`) now receives an authored `viewMode`. + Previously only the resource-workload branch (`resourceView` + + `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt + view was silently ignored. + - The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both + branches read the declared `ganttConfig.viewMode`, which also honours the + key when authored inside the spec's `gantt` config block. + - Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value + (e.g. `viewMode: 'hour'`) becomes a zod validation error where it + previously passed through unvalidated. Values on the published spec enum + are unaffected. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 9b61cf1: The package README stops documenting a `navigation` key the spec refuses, and the corrected example is now parsed by the schema that validates it. + + `README.md`'s record-navigation override read + `{ mode: 'page', basePath: '/console/apps/.../campaign' }`. `basePath` is not a + member of the spec's `NavigationConfig`, and nothing consumes it: + `useNavigationOverlay` — where a gantt's `navigation` lands — builds no URL out + of the config, and `ObjectGantt` calls the hook with no `onNavigate`, so a + page-mode click falls through to the host's `onRowClick`. The destination route + is owned by the host and was never authorable through this key, under any + spelling. + + That made the snippet worse than inert. `NavigationConfigSchema` is a strict + object with no passthrough, so the undeclared key did not fall away quietly — it + rejected the **whole** config with `unrecognized_keys`, taking down the + `mode: 'page'` the sentence was actually teaching. An author who copied the + documented snippet got a rejected navigation config and no page navigation, which + is the copy-the-snippet-get-rejected shape objectui#5057 / #5012 named on other + keys. + + The example is corrected to `{ "navigation": { "mode": "page" } }` — the shape + the sentence demonstrates — and the prose now says who owns the destination route + and points at `@objectstack/spec`'s `NavigationConfigSchema` for the member list + instead of restating it, matching the derivation `ObjectGanttSchema.navigation`'s + doc comment (objectui#5903) adopted for the same concept. + + `view` is **not** substituted for `basePath`. It is a declared member, but it + names a form view (the spec: *"Name of the form view to use for details"*) and is + forwarded to `onNavigate` as the action argument — it is not a route, so putting + it where `basePath` stood would have replaced an invented key with a wrong one. + It is documented for what it does. + + No gate in this repo could have caught the original defect, and that is why the + fix ships with a measurement rather than a re-reading: `check-doc-snippet-types` + compiles `ts`/`tsx` fences and `check-doc-component-types` reads `type` literals, + and both are structurally blind to a metadata key in a README — the former's own + header records schema-key validity as "a different question … left unruled on + purpose". `src/readme-navigation-example.test.ts` closes that hole for this + example by EXTRACTING the fence from the README on every run and parsing it + against `NavigationConfigSchema`, with a control asserting the same parse still + rejects an undeclared key by name, so the green cannot come from a schema that + accepts everything. + + `tsconfig.test.json` names `node` in `types` for that test to compile, and its + comment — which had recorded that no test in this package touches a Node global + — is corrected rather than left standing. +- cbc8839: `ObjectGantt`'s export filename resolves a locale-map `label` instead of stringifying it + (objectui#6052). A gantt authored as + `{ "type": "object-gantt", "objectName": "task", "label": { "en": "Shift Plan", "zh-CN": "排班计划" } }` + exported its PNG/PDF as `[object Object]-20260825-1030.png`. + + `BaseSchema.label` is `string | I18nLabel` since #4580's revised Q1-A ruling — `I18nLabel` + being the spec's INLINE locale MAP — and the `exportFileName` chain handed that value + straight to `String(...)`. It now goes through `resolveI18nLabel` from `@objectstack/spec/ui`, + the producer's own resolver for that vocabulary, against the display locale the file already + reads via `useDisplayLocale()`. A zh-CN audience gets `排班计划-<stamp>.png`, an en audience + `Shift Plan-<stamp>.png`, and a plain-string label is unchanged. + + The next link in the same chain, `objectSchema?.label`, is deliberately left alone: that is + the DATA object's label, declared `z.string().optional()` on the spec's `ObjectSchemaBase`, + which is a `strictObject` — a locale map there is rejected by the producer rather than + resolved by the consumer, and wrapping it would be accepting a second vocabulary at a read + site. No filename sanitisation is added either; `GanttView` already strips + filesystem-hostile characters downstream, and a resolved map entry goes through the same + strip a plain string does. +- 45a9aeb: `ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published + declaration faces, naming `dependenciesField` as the canonical spelling + (objectui#6470). Nothing is removed and nothing is rejected that was accepted + before. + + **What the two spellings were.** `ObjectGanttSchema` declares both, and + `getGanttConfig`'s flat branch reads them with a `||`: + `dependenciesField: schema.dependenciesField || schema.dependencyField`. + `dependenciesField` is the spec's key (`@objectstack/spec` + `GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO + spec counterpart — zero occurrences across `packages/spec/src`, measured against + a live positive control on the plural. Until objectui#6051 declared the plural, + the singular was the ONLY dependencies spelling this interface carried, so for + the whole time the alias existed the published type taught the non-spec key and + hid the canonical one. + + **What was missing was the ranking, not the behaviour.** The two were declared as + equals: nothing on either face said which one to author, so a reader — including + an AI writing metadata, which is the reader this project optimises for — had a + coin flip between a spec key and pre-spec vocabulary. The marker turns that coin + flip into a fact the type itself carries, and the zod mirror's description makes + it readable at runtime as well as in an editor. + + This adopts the idiom already ruled for this exact shape rather than inventing a + second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry + `/** @deprecated legacy alias for the spec's X */` plus + `.describe('Deprecated alias for X')`, and `dependencyField` now reads the same + way. + + **⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the + renderer's `||` — would break every author who wrote the singular and narrow the + accept set of a published surface. That is a maintainer decision on a future + enforce-or-remove card once the deprecation has sat a release, and it is + explicitly excluded here. Two pins hold the line in both directions: + `packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts` + fails if the marker goes missing AND if the alias stops being declared or + accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx` + fails if the `||` limb is dropped — the two spellings must keep resolving to the + same config, with the canonical one winning when both carry a value. + + `packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the + singular; it was the only in-repo site that did, and it now authors the plural + with the alias named as legacy. No runtime code, fixture, example app or catalog + schema authored it. +- f9653ae: Re-key the load-bearing fetch effects in `ObjectMap`, `ObjectCalendar` and + `ObjectGantt` onto the primitive fields they actually read off `dataConfig` + (`provider` / `object` / `items`) instead of the whole memoised `dataConfig` + object (objectui#6592, the deferred half of objectui#6270/PR #6591). + `ObjectTree` is a census member too but is deferred out of this change — see + the PR body — because its own fetch effects are the surface of PR #6696 + (objectui#6481), open at the same time. + + `useMemo` carries no semantic guarantee — React is permitted to discard a + memo cache and recompute even when its dependency array compares equal to + the previous render, and the local `getDataConfig(schema)` helper each of + these renderers carries builds a fresh `{ provider, object }` / + `{ provider, items }` wrapper object on every call. So a fetch effect keyed + on `dataConfig` itself was correct only for as long as that identity + happened to survive a discard: a recompute alone (no author or caller + action) was enough to re-run the effect and issue an extra + `dataSource.find` / `dataSource.getObjectSchema` call. Keying the effects + on the primitives instead makes a cache discard a no-op, restoring + `useMemo` to a pure optimisation. + + `ObjectGantt`'s `effectiveDataSource` memo deliberately keeps `dataConfig` + as a dependency (`resolveDataSource` needs the whole provider-shaped + value — the `api` provider's `read`/`write` request config cannot be + flattened to a fixed primitive list the way `object`/`value` can), so its + `reload()` fetch is decoupled from the redundant direct `dataConfig` + dependency but not from `effectiveDataSource`'s own; for the `object`/`value` + providers `resolveDataSource` returns its `fallback`/a fresh + `ValueDataSource` respectively rather than reading further into the config, + which is enough for the two fetch effects to observe no extra call under a + recomputed-but-equivalent `dataConfig` in the common case. + + No behaviour change for a schema whose `useMemo` caches survive normally; + the effects are unaffected by React discarding one. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 231d1b9: Gantt toolbar: the period label names the visible window, and the prev/next + buttons step it (objectui#7203). + + The label formatted `timelineRange.start` — the memo spanning the whole dataset + — so it named the first unit of the entire result set and could not change while + the chart was scrolled, because it was not derived from scroll position at all. + On a dataset running January to December it therefore read "January 2026" at + every scroll position, four pixels above a band header correctly reading + "Aug 2026". Measured on the demo fixture in Chromium at 1440x900: on first paint, + after the chart auto-scrolls to Today, the label read `December 2025` over + columns `28F 29S 30S 31M 1T 2W 3T` with the band beneath them reading `Aug 2026`. + Two month labels four pixels apart, disagreeing — and the wrong one is the + prominent one, so the chart reads as if the columns were mislabelled. + + The label now names the period at the left edge of the viewport, snapped to the + same tier `headerGroups` bands the timeline by: a month under day and week view, + a year under month and quarter view, a decade under year view, the shift-day + under shift-segmented day view. The toolbar and the band header therefore agree + by construction rather than by two derivations that can drift. Wording is + unchanged for the month tier — the toolbar still spells the month out + ("August 2026" beside the header's "Aug 2026"). + + The `‹` / `›` buttons rendered an `aria-label` and an icon and carried no + `onClick`. They now scroll the visible window one period backwards/forwards at + that same tier, clamped to the ends of the timeline (ADR-0049 enforce-or-remove: + wiring is the branch the label change makes available). They step the label's + tier rather than one column, so a click always changes what the label says. + + The band header is untouched. It was already correct; it is the reference here. +- 7c3df8f: The settled-schema convergence, and the gantt's duplicate query gated + (objectui#7225, maintainer ruling B, 2026-09-02). + + `useSettledSchema` was extracted and published in PR #6690 with exactly **one** + non-test adopter (`ObjectTree`, the component that had an actual defect — + objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and + `ObjectCalendar` kept their own hand copies of the same shape, so a published + export was owed compatibility forever **and** the duplication it was named for + stayed. All three now call the hook. + + The migration is a pure deduplication with no behaviour delta — the hook was + extracted *from* these three shapes, so each becomes a one-line call. + `ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the + recipe the hook's own doc comment prescribes for it by name: pass the data + source as `undefined` for a render that must not read metadata + (`hasInlineData ? undefined : dataSource`), so "inline value data set" is + expressed as "there is no source to read from" rather than as a second enable + flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and + what made the calendar's obstacle a non-obstacle: it was about the gate half. + + **One observable change:** `ObjectKanban`'s rejected definition read now logs + on `console.error` with a `[useSettledSchema]` prefix instead of + `console.warn`. Its test spy moves with it, and now asserts on the channel + rather than merely silencing it. + + **The gantt's duplicate query is gated** (ask 2 of the card; #6482's + undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s + dependency list, so every load issued two unbounded queries — the first with no + `$expand` at all. Measured on this component across three latency profiles, the + cost is not the mild "round trip bought and thrown away": when the metadata + read is the slower of the two, which is the common case on a cold + `MetadataCache`, the user sees the full three-step paint — raw foreign-key ids, + back to the loading placeholder, then the expanded rows. It now issues one + query, already expanded. + + Gating the gantt required its schema resolution to settle on EVERY exit + (objectui#7232): the hand-rolled effect returned without settling on + `!effectiveDataSource`, on `!resource` and in its `catch` — harmless while + nothing waited on it, and a chart that never loads once something does. + `useSettledSchema` settles on all three by construction, which is what makes + the gate safe; both exits are pinned. + + ⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's + separate ruling, in its own commit on the same branch. +- 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230). + + objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope + (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This + closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, + `DetailView`, and `ObjectDataTable` (which builds its own whitelist in + `computeLookupExpand` rather than calling `buildExpandFields`). + + **Three of them pass no column list at all**, which makes them the sharp ones: + `buildExpandFields` reads an absent column list as "no column restriction" and falls + back to **every declared relation on the object**, denied ones included. So a standalone + calendar, a gantt, and every record page in the console asked the server to resolve the + object's full relation set by default rather than by configuration. + + **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column + list is already FLS-filtered field by field, which is exactly the route PR #7229 measured + as unsound: an emptied column list reads as "no column restriction", so a detail view + whose authored fields are all denied had its `$expand` **widened** from the relations it + asked for to every relation the object declares. The principal who may read least was + asking for the most. + + **Reproduced before it was fixed**, as a failing test per site. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip, and the + client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT, + which contains only the object's declared reference-bearing fields, so the "`checkField` + answers false for an undeclared key" trap cannot be reached and derived / host-joined + columns are untouched. An unanswered permission policy filters nothing. Neither + `buildExpandFields` nor `computeLookupExpand` is changed. +- 92c65aa: `ObjectGantt` no longer blanks the chart when one reload supersedes another + + `reload()` already sequenced concurrent runs with `reloadSeqRef` and guarded every + result write with `isCurrent()`, but its `finally` was unguarded — so a **superseded** + reload still flipped `loading` / `refreshing` off. The stale run only had to finish + first, which is the ordinary case whenever a second reload is issued while the first + is still in flight: the placeholder was released, no rows had arrived, and the user + saw an empty chart until the fresh response landed. + + The `finally` now clears the flags only when the run reaching it is still the current + one. It clears **both** flags rather than only the one its own `silent` mode set: + being current at that point means nothing is in flight any more, so clearing only its + own mode would strand the other flag whenever the superseded run used the other mode + — a silent toolbar refresh overtaken by a filter-change reload would have left the + refresh button busy for the life of the component. + + This is the reload guard alone. Nothing about which queries are issued, how they are + projected or how they page changes. +- e75f4c9: `colorField` now means the same thing in the gantt, the calendar and the timeline + (objectui#7243). + + **The inversion this fixes.** `gantt.colorField` is documented as "field that drives the + bar color", and the renderer passed the stored value straight into the bar's + `backgroundColor`. Pointing the key at a select field therefore emitted + `backgroundColor: "open"` — not a colour, so the browser dropped the declaration and + every bar rendered identically. OMITTING the key was strictly better: the absent-key + branch derived a real colour per status. Declaring the documented key was worse than not + declaring it, with no error, warning or console message either way. + + The same key also meant three different things across the three lenses: the timeline + resolved the field's authored option `color`, the calendar hashed the raw value onto a + fixed palette, and the gantt emitted the raw value. An author colouring three views by + one field got three unrelated results, one of which was no colour at all. + + **The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's + resolver, lifted so all three call it: + + 1. the field's own option `color` for the record's value; + 2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`, + `#rrggbbaa`, `rgb(...)`, `hsl(...)`); + 3. else each renderer's own last rung, which is deliberately NOT shared — the gantt + derives a semantic-token hex (a bar must be painted), the calendar keeps its + theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its + default marker. + + **What changes for authors.** A gantt or calendar whose `colorField` points at a select + field with authored option colours now paints those colours. A gantt value that is + neither an option colour nor a colour literal now derives a colour instead of emitting + an invalid CSS value — including a palette NAME (`red`), which now resolves to that + palette's hex, the behaviour the key's own contract has always promised ("hex or + semantic name") and the one `borderColorField` already had. `gantt.borderColorField` + takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps + today's behaviour otherwise and deliberately gains no derivation rung, since the stroke + is opt-in and deriving one for every record would draw an alert on records that have + none. + + Calendars whose `colorField` points at a plain categorical field are unchanged: that + value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline + is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar + already accepted. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [f66072d] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [1117414] +- Updated dependencies [6d63cd0] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [7c96c94] +- Updated dependencies [4da5109] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [63d54dd] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [b4393e5] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [d3005f7] +- Updated dependencies [1e7fe0a] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3777538] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [9409eb9] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [eeb6c2f] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [854cba3] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [6411def] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [b61d7d8] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [fe8f451] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [d06fba8] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [0b12a33] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [6c1b105] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [17fbbaf] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0d1e702] +- Updated dependencies [b03ba3a] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index bfe2a7592e..37ac5c9998 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-gantt", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Gantt chart plugin for Object UI", diff --git a/packages/plugin-grid/CHANGELOG.md b/packages/plugin-grid/CHANGELOG.md index 06a5f2e8d0..370b46920f 100644 --- a/packages/plugin-grid/CHANGELOG.md +++ b/packages/plugin-grid/CHANGELOG.md @@ -1,5 +1,1893 @@ # @object-ui/plugin-grid +## 17.7.0 + +### Minor Changes + +- 67a87d9: Retire the dead per-column `wrap` forward from `ObjectGrid`'s data-table emit (objectui#5453, + ADR-0049 enforce-or-remove). + + `generateColumns()` copied a per-column `wrap` onto every column object handed to + `data-table`, and `packages/components`' `data-table.tsx` never read it. Measured on the + current ref rather than inherited from the card, with comments stripped so prose mentions + cannot be counted as reads: a column-level `wrap` scores **0**, against `accessorKey` 34, + `align` 5, `header` 4, `className` 4, `width` 8 and `fitContent` 2 in the same query shape. + Those sibling counts are the positive control — the search style does find the keys that are + genuinely consumed, so the zero is a measurement and not a mis-aimed grep. The raw string + `wrap` does occur in that file; every occurrence is `flex-wrap`, `whitespace-nowrap`, or a + variable named `wrapper`. + + The implement leg was ruled out by the same measurement rather than by preference. `wrap: true` + would have to drop a `truncate`, which presupposes the renderer has somewhere to put a second + line — and it does not. `data-table`'s cell wrapper is a two-way switch, + `isFit ? 'w-full whitespace-nowrap' : 'truncate w-full'`, with a native `title` tooltip as the + only concession to overflow; the file does not read `density` or `rowHeight` at all. There is + no clamp, no expand, no line-clamp and no multi-line affordance for a per-column `wrap` to turn + on, so the enforce-or-remove default applies. + + Removing the forward rather than declaring the key follows from `wrap` having **no second road + to a consumer**. That check is what separates this verdict from `pinned`, which `data-table` + also never reads and which is nonetheless kept: `ObjectGrid`'s own reorder pass consumes + `pinned` before the array reaches the slot and re-expresses it as the sticky `className` the + renderer does read. `wrap` had no such pass anywhere. + + **Retired, not merely deleted.** `wrap` is no longer carved out of `RetiredListColumnKey`'s + `Exclude`, so the derived tombstone band that objectui#6461 installed on this producer now types + it `never`. Re-adding `...(col.wrap !== undefined && { wrap: col.wrap })` is a compile error + naming `wrap` — otherwise "retired" is just a deleted line the next edit can put back for free. + Deriving the band from the authored `ListColumn` is also why this needed no hand-maintained + list: taking the carve-out out was the whole edit. + + **Breaking for TypeScript consumers of `@object-ui/plugin-grid`'s exported emit types only** + (`ObjectGridColumnHolds` loses its `wrap` member; `ObjectGridColumnDraft['wrap']` becomes + `never`). Marked `minor` per this repo's version-alignment rule, which reserves `major` for + following `@objectstack` across a major — the same classification the `MobileOverrides` + retirement used. **Runtime behaviour is unchanged**: an authored `wrap` did nothing before and + does nothing now. What changed is that the code no longer implies `data-table` might consume it. + + ⚠️ The **authorable** spelling is untouched and still declared: `@objectstack/spec`'s + `ListColumn.wrap`, which `packages/react`'s spec-bridge still forwards into the grid schema. + Whether that spec property should keep being declared with no renderer anywhere is a spec-side + enforce-or-remove question, filed separately rather than settled from inside this renderer. +- beccf1c: fix(plugin-grid): type ObjectGrid's column emit against the `TableColumn[]` slot it fills + + `generateColumns()` had no return annotation and all four of its call sites cast + to `any`, so nothing checked what this producer wrote into + `DataTableSchema.columns: TableColumn[]`. + + Annotating it is not enough, and that is the substance of the change. Measured on + this program: `generateColumns(): TableColumn[]` raises **zero** diagnostics — the + emit literals reach the annotation through `.map()`, which strips the freshness + that excess-property checking depends on, so even an undeclared key written out + longhand is accepted. Underneath that sits the reason the annotation could not + bite at all: `objectSchema` is `useState<any>`, and an `any` spread into an object + literal collapses the **entire** literal to `any`. + + So the fix has three parts: name the four inference locals so `any` stops at the + boundary, carry ADR-0049 `?: never` tombstones **derived** from + `keyof ListColumn` (never hand-listed, so a future spec key is refused by + default), and drop the `any` at every call site — including a fourth the card's + census missed and a fifth (`const generatedColumns: any[]`) inside the producer. + + Key verdicts: `headerIcon`, `pinned` and `wrap` are HELD and now declared at the + seam; `options` is RETIRED — nothing on either side of the seam read it, and + every value it carried still reaches its consumer through the field metadata the + cell closure captures and the object schema the inline editor reads. `type` stays + objectui#5853's and `name` is not emitted here at all. + + No rendering change. +- f55d666: Retire four undeclared authored column reads in `ObjectGrid.generateColumns()` + (objectui#6458). + + `ListColumnSchema` is a strict object, so an author who wrote `format`, + `options`, `appearance` or `essential` on a list column was **refused at + publish** with `unrecognized_keys` while the grid happily honoured the key at + runtime. That is the `declared != enforced` split AGENTS.md #0.1 exists to + stop, and it was reachable only through `(col as any)`. All four reads are + removed. Each was re-measured on this branch before deletion: **zero authored + occurrences on a column across `examples/` and `apps/`**, per key, with `field` + as the positive control in the same query shape — so no real author's metadata + changes behaviour today. + + What each retirement leaves as the only road: + + - `format` and `appearance` — the object-field fallback (`objectDefField?.format` + / `?.appearance`), which is already the road every measured author uses. + - `options` — the object schema's select options. The column-level override was + exactly the shape that let AI-authored metadata drift from the schema it is + supposed to obey; one source for options beats two. + - `essential` — mobile visibility stays positional (`colIndex === 0`). + + **Retired for want of authors, not forbidden forever.** If a real request for + semantic mobile-column control arrives, the declare route reopens: declare the + key on `@objectstack/spec` and read it without a cast. What stays ruled out is + the third road — a renderer-side tolerance for a key the schema refuses. + + `columnReadBoundary-6458.test.ts` moves with the change: its bound on undeclared + cast reads in that branch is now the **empty set**, so a new one goes red on + arrival rather than accreting quietly. +- f241a4d: feat(plugin-grid): type the data-table schema slot ObjectGrid fills + + `const dataTableSchema: any` becomes `ObjectGridDataTableSchema`, and + `buildGroupTableSchema`'s return carries the same annotation, so the ~46 keys + this grid writes into the `data-table` slot are finally checked against the + slot's declaration — the receiver half of the seam whose producer half #6004 + typed. The `(dataTableSchema.columns as any[])` cast in the grouped writer + drops with it. + + The annotation is not a bare `DataTableSchema`, and that is the substance: + measured on this program, a bare annotation with an undeclared bogus key + written longhand in the fresh literal compiles with **zero** diagnostics, + because `BaseSchema`'s `[key: string]: any` index signature makes every key a + member — excess-property checking never has a non-member to refuse. + `ObjectGridDataTableSchema` derives from `DataTableSchema` by stripping the + index signature (never by hand-listing members), which makes the same probe go + red (TS2353) at both writer literals — shown able to fail before being claimed + as coverage, per the #6004 rule. + + Two schema-level keys the grid passes are undeclared on `DataTableSchema` and + HELD at the seam with measured live readers in `data-table`: + `renderCellEditor` and `cellClassName`. Whether `DataTableSchema` should + declare them is filed for a ruling — nothing is declared on or retired from + `@object-ui/types` here. +- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one + spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). + + **BREAKING, deliberately, with no deprecation window.** + + Two published contracts declared OPPOSITE dialects for the same four lookup keys, and + `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object + contract *refuses* outranked the one it *declares*: + + | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) | + |---|---|---| + | camelCase | **declared** | compile error (`TS2561`) | + | snake_case | refused (`unrecognized_keys`) | **declared** | + + `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the + snake members are **removed**: + + | before (removed) | after | + |---|---| + | `display_field` | `displayField` | + | `description_field` | `descriptionField` | + | `lookup_filters` | `lookupFilters` | + | `id_field` | `idField` | + + **Migration.** Rename those four keys wherever you author lookup or user field metadata + — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema` + that returns them. The old spellings are no longer read: a def still carrying + `display_field` falls back to the referenced record's generic name heuristic rather than + the field you named. + + `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name + hydration — committing a record field other than the id as the lookup's stored value — + which is picker behaviour with no `FieldSchema` twin, and none owed. + + **Not renamed** (outside this ruling's four keys, still snake on the widget bag): + `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`, + `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's + `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def. + + Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose + snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the + inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s + `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict + class; its gate is restated to assert the class no longer exists rather than passing + vacuously. +- 8952395: A grouped grid now says, where the group counts are, that it grouped a **page** + (objectui#7189). + + `useGroupedData` buckets the rows the browser already holds and computes every + per-group aggregate from that same array, so both the set of groups and every + number in a group header are properties of the fetched page, not of the query. + That is a correct implementation of client-side grouping and is **unchanged** + here — what was missing is any statement that client-side grouping is what you + are looking at. Measured on a 186-record store distributed 86 / 61 / 31 / 7 / 1 + across five business units with a 100-row page: with contiguous rows the grid + rendered **two** group headers (`86`, `14`) and three units were absent from + the screen entirely; with interleaved rows all five resolved but every count + was a page slice (31 / 31 / 30 / 1 / 7). Nothing on screen said either. + + The paging footer is not a statement about what was grouped, and it + demonstrably did not prevent the wrong reading — a wrong number invites a + second look, an absent row invites none. So the disclosure goes where the + authoritative-looking number is: + + - a short `Partial` marker beside **every** group count, at every nesting + depth, carrying the full sentence as its `title` and its accessible name; + - one line directly above the group list, inside the grouped region rather + than in the footer: *"Grouped over the first 100 of 186 records. Group counts + are page-scoped, and a group whose records all fall beyond the loaded rows is + missing here."* + + The trigger is the strongest thing the component can actually know, and the + wording never outruns it. With a real match total to compare against + (`resolvedTotalMatching` — the one derived value the pager and both bulk-bar + sites already read, reached either from the grid's own fetch or from a host's + `rowCount`) it states the fact with both numbers. With no total but a window + that came back full it may only say *"more may match"* — the same inference + `plugin-list`'s own footer draws when no total is known. Rows handed in inline + are not a page and are never marked, and **a grouped grid whose result set fits + in one page shows nothing at all**: the marker is conditional, which is what + makes it worth reading. + + Server-side grouping — the durable fix — is deliberately NOT part of this. It + is an API-surface decision still open on objectui#5560, and nothing here builds + toward it or changes the fetch. + + `@object-ui/i18n` carries the three new `grid.grouping.*` strings across all + ten locale packs; `GroupRow` gains two optional props (`partialLabel`, + `partialTitle`) and is otherwise unchanged. No metadata schema key was added: + the condition is derived from data the grid already has. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- cf1d29e: `ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one + (objectui#8067). + + A registration's `type: 'array'` said a value was a list and stopped there, so a member + that drifted from `@objectstack/spec` was invisible to every layer that reads a + declaration. `page:header.actions` is the measured cost: the contract declares + `z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` + objects, and the repo-wide parity gate in + `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the + whole life of the drift because both sides carried the key and neither could say what + was inside it. What settled it was a maintainer ruling, not a test — and even after the + fix, "these are ids" survived only as English in the registration's `description`. + + **What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind + vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an + `object` used as a map. One kind, or an array of them for a member contract that is a + union, with `type`'s semantics — a member passes when any declared arm accepts it. The + manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per + input instead of six. + + **Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 + is the precedent: five `ComponentInput` keys declared and read by nothing). The + repo-wide parity gate compares every declared `of` against the member kind + `ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; + `sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new + `member-type-mismatch` diagnostic naming the offending positions; and the generated + `sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is + `string[]` where it used to be `unknown[]`. + + **Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: + every container key's member position was probed with one value of each coarse kind and + a declaration written only where exactly one kind was accepted. A member contract that + admits several kinds — `record:highlights.fields` takes a field name or an inline field + object — is deliberately left undeclared and pinned with its reason, because picking one + arm there is a narrowing this repo leaves un-gated and picking all of them would + advertise shapes only a per-block pin can vouch for. + + **The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer + ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` + is the publication face's expression ceiling, and spec is the sole judge of values — + stands exactly as written. `of: 'object'` says the members are objects; which keys they + carry is still `description`'s job and `os validate`'s. + + **Nothing published before this changes.** An input that declares no `of` validates, + serializes and types byte-identically: `validateTree` checks no member, the serializer + emits no key, and the codegen emits the same `unknown[]`. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 9801765: The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen` + per selected record (objectui#4420). It used to read that key as a bare boolean — the + object-level verdict only — so ticking a record the author's predicate excludes still + offered the red Delete, and pressing it deleted the record the predicate was written to + protect. The row kebab on the same screen hid its Delete correctly, so one declared key + meant two different things on two surfaces. + + Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the + operation and report the skipped**. The bar evaluates the predicate once per selected + record, the delete runs over the allowed subset, and the excluded records are reported + rather than silently dropped. The button itself is never hidden or disabled by the + predicate — a mixed selection is not punished for one stray tick — and a selection where + every row is excluded is a legible refusal rather than an unexplained absence. + + - `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of + `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once. + A bulk gate evaluates N records in a loop, which is why it can never be a hook. + - `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through + `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped + count; a selection with nothing excluded keeps the consumer's own delete flow + untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` — + the bulk half of the same predicates, gated on the object verdict rather than on the + row `onDelete` wiring. The dialog declines to run over zero records. + - `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters + the built-in `delete` to the eligible subset and states the skipped count inline. + + Custom bulk action ids are untouched: they route through the action runner carrying + their own gates. This is a UI affordance — server enforcement was never the leak. +- 062943f: `@object-ui/plugin-grid` and `@object-ui/plugin-kanban` now publish a stylesheet — + `"./style.css"`, mapped to `dist/index.css` and compiled at build time from the package's + own sources (objectui#4929, maintainer ruling 2026-08-17, Direction 1). + + **What was broken.** Only `@object-ui/components` and `@object-ui/fields` shipped CSS, and + each scans its own `src` only, so a class used exclusively by a plugin could not appear in + either sheet BY CONSTRUCTION. A published-state Vite app that installed one of these two + plugins and followed the quick-start rendered the grid or the board with **25 themed + utilities that had no source anywhere in the world** — `bg-muted/10`, `bg-card/60`, + `text-muted-foreground/60`, `ring-primary/40` and friends, ordinary appearance classes — + plus ~103 plain ones. Re-measured on the merged tree: the 21 the card listed all still hold, + and four more (`[&>h3]:text-foreground/80`, `border-l-primary/40`, `border-primary/30`, + `hover:text-primary`) that its literal-grep method could not see. + + The plain utilities a consumer could in principle regenerate by pointing `@source` at the + package's `dist`. The themed ones they cannot, at all: they resolve `@theme` tokens declared + in `packages/components/src/index.css`, which that package does not publish. A build inside + this monorepo is their only possible producer — which is why the fix is a stylesheet we + ship, not documentation teaching consumers to hand-declare the theme and scan + `node_modules` (the advice objectui#4858 had just retired from the guides). + + **The shape**, inherited from `@object-ui/fields` (objectui#4059): each package gains + `src/index.css` that `@reference`s the components entry — theme tokens, the class-based + `dark` variant and the animate plugin become available for resolution while emitting + nothing — plus `scripts/build-css.mjs`, which subtracts every rule components' published + sheet already ships. So these are **supplements, imported after** the components sheet, and + they are 16.30 kB and 11.41 kB rather than another ~170 kB each: + + ```css + @import 'tailwindcss'; + @import '@object-ui/components/style.css'; + @import '@object-ui/fields/style.css'; + @import '@object-ui/plugin-grid/style.css'; + @import '@object-ui/plugin-kanban/style.css'; + ``` + + Add a line only for the plugins you install; no other `@object-ui/plugin-*` package + publishes a stylesheet yet. The build step is shared + (`scripts/build-plugin-stylesheet.mjs`) so it is the pattern the next one inherits rather + than a file to copy, and it refuses to write a sheet that fails any of four assertions — no + rule may vanish, the subtraction must have removed something, the class count may not pass + a leak ceiling, and named themed utilities only this build can produce must still be + present. + + Nothing is removed and no existing import changes: a consumer who does not import the new + sheets is exactly where they were, and the guides' "do not scan `node_modules`" advice + stays correct — it is now correct for plugins too. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 490d9a9: Grid headers offer a sort click only on columns the PLATFORM says it will order by + (objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23: + the platform serves an explicit per-column sortability signal and the grid reads it, + rather than re-deriving "virtual ⇒ unsortable" from field type). + + `GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its + ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve + time from the platform's own storage predicates, deliberately beside `item` rather than + inside it so the key stays un-authorable. The signal was reaching the browser and being + discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps + the envelope to `item`, so every UI reader saw a document with no signal on it. It now + survives that unwrap, carried on the schema under a symbol key — invisible to + `JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata + write endpoint can never take it into a body the server parses strictly. + + `@object-ui/core` gains the one spelling of the consumer contract: + `isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and + says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown + name, a dotted path and an unprovisioned audit column, all three of which the runtime + doors reject — so the `!== false` spelling every other optional flag in this repo uses + would get exactly that family backwards. A projection that is absent ALTOGETHER is a + different question with a different answer (`undefined`: no signal was served) and is + typed apart from an empty one, so a deployment older than the upstream change keeps the + behaviour it had rather than being told, falsely, that nothing on the object is sortable. + + Three things follow in the grid. The header click on a refused column ceases to exist, so + neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is + reachable from it. A sort PERSISTED before the signal existed is filtered out of both what + the grid renders and what it emits, so a restored personalization cannot ride back into + the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the + PUT still fires. And the relational carve-out is untouched and deliberately not delegated + to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored + foreign key and both runtime doors accept ordering by it), while the grid withholds that + header for a different reason — a column of names ordered by an invisible id. + + Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts + those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced + drift in mirror image. +- fc62bb4: `TableColumn.type` now has ONE canonical value set across all three ends that disagreed + (objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is + canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | + 'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted + anything; the renderer branched on a third set and could only read the key through an + `as any` cast. + + ## ⚠️ Accept-set narrowing — these spellings stop validating + + `TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now + `z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse + time with `type` named in the error path. Spellings that validated before and are **refused + now**, grouped by why they were being written: + + - **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, + `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is + the card's headline case: it validated, matched no renderer branch, and the column fell + through to plain text rendering with nothing reported. That silent fall-through is the + lenient-validation face that lets AI-authored metadata errors through, and it is now a + loud parse failure. + - **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, + `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of + `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, + not on the column: a column gets its dedicated widget from the field definition behind its + `accessorKey`, never from `type`. + + **Authored metadata in this repo needs no migration.** Measured before tightening, across + `examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files + plus the docs and playground sources): **zero** authored `TableColumn.type` values outside + the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column + position anywhere in the repository. If you author `type` on a table column, check it + against the eight; if the value describes the FIELD rather than the column, remove it. + + ## The renderer's undeclared vocabulary disappears instead of being declared + + `int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` + and `datetime-local` had its own editor branch, none of them declared. They arrived because + column-inference producers forwarded an object schema's field type **verbatim** into + `TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred + value onto the declared vocabulary at their emit seam via the new + `normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, + `datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the + column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) + and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote + the raw field type into the same slot. + + Dropping the annotation is behaviour-preserving at the only consumer that reads the key. + `data-table`'s inline editor branches on `date`, `datetime` and the numeric set and + otherwise falls through to a text input — which is exactly the `undefined` path. The + dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, + which resolves the field through `column.accessorKey` and never reads `type`. + + ## New public API + + `@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single + declaration the zod mirror builds its enum from, so the two cannot drift), the + `TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast + in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared + spelling is a tsc error rather than a silent widening. + + A value-level parity pin covers all three ends + (`packages/types/src/__tests__/table-column-type-canonical.test.ts` and + `packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). + objectui#5684's guard is key-set only and cannot see value drift — `type` was present on + both sides the whole time — which is how this instance survived while its siblings were + caught. A future inference value turning that pin red is by design; the note at the pin says + so, and names the two correct repairs. +- 129bcc5: Column width and order that a user drags in `ObjectGrid` now actually persist + (objectui#6175). Both halves of `saveColumnState`'s only two call sites were dead, so a + drag was written nowhere — not to `localStorage`, not through `onColumnStateChange` to the + host's `dataSource.updateViewConfig`. The saved state was read back correctly forever; it + was simply never written. + + Two independent breaks, one per package: + + - **`@object-ui/components`** — `DataTableSchema` has declared + `onColumnResize?: (columnKey, width) => void` all along, and `data-table.tsx` invoked it + **nowhere**: the resize drag updated the table's local `columnWidths` state and stopped + there. It now reports the settled width once, at `mouseup`. Once, deliberately — the host + turns this callback into a write to shared view config, so a per-`mousemove` callback + would be a write storm. + - **`@object-ui/plugin-grid`** — `ObjectGrid` emitted `onColumnReorder` (singular) while the + renderer invokes the near-duplicate `onColumnsReorder` (with the `s`), a different declared + key with a different signature. The producer now emits the spelling the renderer actually + invokes, mapping the reported `TableColumn[]` to the `accessorKey` order `columnState` + stores. + + **Nothing is retired.** Both spellings remain declared on `DataTableSchema`; + `onColumnReorder` stays declared and stays unwired, exactly as the `RuntimeOnlyDeclared` + ledger in `zod-mirror-parity.test.ts` records it. Which of the two survives is a + declared-surface ruling that stays open and is deliberately not settled here. + + ⚠️ Behavioural note for hosts: `onColumnStateChange` now fires where it previously never + did, which means `dataSource.updateViewConfig` is now reached on a column drag. That call + was unreachable by this path before, so any permission gate on that write now sees traffic + it never saw. + + The renderer's resize/reorder gestures, the inbound seeding of `columnState`, and the + declared surface are all unchanged. +- c38162d: The batch-explain cap the row-verdict hook paginates under is now imported from + `@objectstack/spec/security` instead of re-declared locally (objectui#6286). + `useRecordCrudVerdicts` carried `const EXPLAIN_BATCH_MAX_RECORD_IDS = 200`, a hand copy of + a SERVER contract constant, under a doc comment that named its own expiry condition: the + pinned `@objectstack/spec@17.0.0-rc.6` predated the batch form, and the pin bump would + supersede the declaration. It has. + + **No value changes and no behaviour changes.** The spec exports `200`, which is what the + local copy said, verified by resolving the installed package and reading the export — both + statically (`dist/security/index.d.mts`) and at runtime through the same specifier the + source now uses. What changes is reference identity: if the server relaxes or tightens the + cap and the spec follows, the client follows too, instead of paginating at the old boundary + with no signal anywhere. The cap's whole point is that an over-cap request is refused with + `400 VALIDATION_FAILED` rather than truncated, so a client that silently disagrees with it + is exactly the drift `scripts/check-spec-symbol-derivation.mjs` argues about — and could + not catch here, because both of its scanners skip non-exported declarations and this const + was module-local (objectui#5899). + + The declared floor already carries the symbol, so no range moves: `@objectstack/spec@17.0.0` + — the minimum `^17.0.0` admits — exports `EXPLAIN_BATCH_MAX_RECORD_IDS = 200` from + `./security`. Measured against the published tarballs of `17.0.0-rc.6`, `17.0.0`, `17.1.0` + and `17.2.0`: only the rc lacks it. The declaration was therefore expired one release + earlier than the card that found it assumed. + + The half of the comment that explains *why* the cap exists and what the server does with an + over-cap request is kept and now sits on the import; only the half explaining why it was + declared LOCALLY is gone, since that is the part that stopped being true. + + Covered by a new reference-identity test rather than a value assertion. Every assertion on + `200` passes on both sides of this change — a ghost — so + `useRecordCrudVerdicts.batchCap.test.tsx` stands the spec module in at a cap no hand copy + could produce and asserts the request chunking follows it, with a control case proving the + stand-in installed and differs from the shipped value. The pre-existing cap assertion in + `rowRecordCrudVerdict.test.tsx` now derives its fixture and its bound from the same export + instead of re-typing `200`, which removes the last hand copy on this surface without + pretending to be a two-world test. +- 6e88630: One authority per exported type name, batch 3 of objectui#6349: `ComboboxOption`, + `NamedActionDef`, `OrgTranslate`. + + **`@object-ui/components` — `ComboboxOption` now IS `@object-ui/types`' declaration.** + The component declared its own `{ value, label }`, a strict subset of the + `ComboboxOption` that `@object-ui/types` declares for `ComboboxSchema.options` and + mirrors in `form.zod.ts` (`{ value, label, disabled? }`). The component now re-exports + the types declaration (through the `@object-ui/types/form` subpath — the root barrel + does not publish the name), so the name `ComboboxOption` exported from + `@object-ui/components` gains the optional `disabled?: boolean` member. Every value + that type-checked before still does — nothing narrows and no key changes type; the + one thing that moves is `keyof ComboboxOption`, so a consumer that EXHAUSTS the type + (a `Record` over its keys) will need the new key. Note that the `Combobox` component + itself does not read `option.disabled` — that member was already declared on the + `@object-ui/types` face and is now visible on this one too; it is recorded as a + separate finding, not changed here. + + **`@object-ui/plugin-grid` / `@object-ui/app-shell` — internal, surface unchanged.** + `NamedActionDef` was declared identically in `resolveBulkActions.ts` and + `resolveLegacyRowActions.ts`; the latter is now the one authority and the former + re-exports it. `OrgTranslate` was declared identically in `orgErrorMessage.ts` and + `orgRoleLabel.ts`; the former is now the one authority and the latter re-exports it. + Neither name is on its package's public entry, and every deep-`dist` module still + exports the same name with the same shape. + + `FilterBuilderCondition` / `FilterGroup` (the other two names this batch was sized + with) are deliberately NOT converged: their shapes disagree on `id`, `value` and on + `operator`, and the only dependency-legal re-point would retype `operator` — the + vocabulary objectui#7561 is asking a maintainer to rule on. +- 38a123c: Land objectui#6425's per-key ruling for `ObjectDataTable`'s authored column + override keys (maintainer, 2026-08-27): + + - **Declare `format`, `options`, `currency`** on `TableColumn` and its + `TableColumnSchema` zod mirror, in the same stroke. All three are honoured + by `object-data-table`'s cell pipeline — `format` / `options` were + documented author overrides the published types refused (a typed author got + a compile error and the zod parse silently stripped the key); `currency` + shipped in production but was never promised. The zod mirror now passes the + keys through instead of stripping them; `StaticTableColumn` and its mirror + tombstone all three under the #5474 lockstep rule (the static renderer + reads no field-meta overrides). + - **Retire `decimals`**, immediately: zero readers measured anywhere + (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads + `precision`), so no authored `decimals` could reach a render. The authored + read is removed and the key falls into `AuthoredColumnOverrides`' derived + refusal band — render output is pinned unchanged. + - **Re-arm plugin-grid's #6004 `options` retirement with an explicit + tombstone**: that refusal rested on the key's NON-membership (excess-property + freshness), which declaring `options` on `TableColumn` silently ended. + `ObjectGridRetiredOptionsTombstone` (`?: never`, intersected into both + `ObjectGridColumnDraft` and `ObjectGridColumn`) restores the refusal by + assignability; #6004's verdict itself is unchanged. + - **`referenceTo` is deliberately NOT declared as spelled** — it stays held, + owned by objectui#6597 (fix the spelling chain or withdraw the README + line). The remaining hold is that card's scope, not unfinished work here. +- c4987fb: ObjectGrid now restores a persisted column width on the ungrouped path — the stamp key was + `size`, which nothing downstream reads (objectui#6457). + + Resize a grid column and reload: the width came back. It was written to `localStorage` + (and reported to the host, which persists it through `dataSource.updateViewConfig`), read + back into `columnState.widths`, and stamped onto the column — as `size`. `TableColumn` + declares `width`, and `data-table` resolves a column's width at all four of its sites as + `columnWidths[accessorKey] || col.width || autoSizedWidths[accessorKey]`; it reads no + column-level `size` anywhere, and ObjectGrid never passes a `columnWidths` prop down. So + the round trip completed and was discarded at the last hop, and the column fell back to + the char-estimate auto width. The `persistedColumns` map now stamps `width`. + + The correct key was not a judgement call: the **grouped** path in the same component reads + the same `columnState.widths` and has always stamped `width`, and it worked. One path was + out of step with its sibling — so this restores a convention rather than teaching + `data-table` a second spelling. `TableColumn` is not edited: the consumer's declaration + was the correct one. Precedence is unchanged and needs no change — a persisted width still + loses to an in-session resize and still beats auto-sizing. + + Two things stop it recurring. The map's callback is no longer `(col: any)`: typed as + `ObjectGridColumn` (`TableColumn & …`, declared since objectui#6004), a stray `size` here + is now a compile error instead of a silent, user-visible drop — the `any` was what let the + wrong key cross a boundary that had already declared the right one. And the new pin is the + **inbound** half — a persisted width seeded through both channels, asserted at the rendered + header cell. The pre-existing suite asserted only the outbound half, which passes on the + broken code, because the write is exactly what was wrong; that is the measured reason this + shipped. +- d6ceb8d: Implement `ListColumn.wrap` — a column that says it wraps now actually wraps + (objectui#6650, maintainer ruling 2026-09-02, Option B). + + `@objectstack/spec` declares `ListColumn.wrap` and describes it to authors as + "Allow text wrapping", and `packages/plugin-grid/README.md` shows it in its + authored-column example. No renderer anywhere implemented it. Long cell text + stayed clipped to one line, with no error, no warning and no feedback of any + kind — a promise made at authoring time and silently broken at render time. + + **What changes.** A `data-table` column with `wrap: true` renders its cell body + `whitespace-normal break-words` instead of the default `truncate`, so long text + flows onto further lines and the row grows to fit. `ObjectGrid.generateColumns()` + forwards the authored key into the column slot, and `TableColumn` declares it, so + the key is honoured whether it is authored on a spec list view or directly on a + `data-table` node. `ObjectGrid`'s own `LinkCell` — the record link that column one + of almost every grid renders through — honours it too, because its own `truncate` + would otherwise clamp the text back to one line inside a cell body that was + willing to wrap. `@object-ui/types`' zod mirror carries the key as well; without + that the non-strict mirror would silently strip an authored `wrap` on the parse + road, which is the same "renderer honours what the declaration refuses" gap + objectui#6424 and objectui#6425 closed for their keys. + + **Nothing changes for anyone not authoring the key.** `wrap` absent or `false` + renders exactly what shipped before, pinned as a control rather than assumed, and + the link cell's default markup is byte-identical to what it was. + + **Precedence, where the two keys conflict.** `fitContent` WINS over `wrap`. A fit + column is `width:1%` with no `minWidth`/`maxWidth` clamp, so the auto table layout + sizes it from its content alone, and `whitespace-nowrap` is what holds that + content's min-content width at its max-content width — one line. Drop nowrap and + min-content falls back to the longest word, so honouring `wrap` there does not + wrap the column, it collapses it: measured in Chromium with the cell shape + reproduced exactly, 463.9px wide on one line with nowrap against 70.9px wide over + ten lines without it — 6.5x narrower and 5.9x taller. The keys do not compose, and + the one that yields is the one whose outcome nobody asked for. + + The static `table` renderer does not gain the key: `StaticTableColumn` tombstones + it, so an author who writes `wrap` there is refused loudly at parse time with the + remedy named, rather than having it silently stripped. +- 4ac3769: An object-bound grid whose rows arrive from a **host** now renders the object + schema's default-columns policy instead of the row payload's keys + (objectui#6677). + + `ObjectGrid.generateColumns()` checks three default paths in order: authored + `columns` → the inline-data path → the object-schema path. The inline-data path + is gated on `hasInlineData` (`dataConfig.provider === 'value'`), and + `dataConfig` is built as `provider: 'value'` from the `data` **prop** before + anything else — so it is taken by every grid whose rows were handed down rather + than fetched. It returned unconditionally whenever rows were present, and its + projection is `schemaFields || Object.keys(inlineData[0])`, the first row's + keys. That made the object-schema path — the one carrying the documented policy + (`highlightFields` first; else drop `hidden`, drop readonly system-managed, push + the remaining system/ownership columns to the end) — **unreachable for every + object-bound grid reached through a fetching host** (`ListView`, `ObjectView`, + …). The branch that knows the object was the one that never ran. + + Measured on the same page, source and object with one variable — who fetches: + `<object-grid objectName="opportunity" />` rendered the policy's **5** columns + (Opportunity Name / Stage / Amount / Close Date / Owner); the same object behind + `<list-view>` rendered **10**, adding `Id` (`hidden: true`) and the four audit + columns (`system`). Those are exactly what the policy exists to keep off a + default list, and the extra key set was whatever the query happened to return. + + **The yield is as narrow as the defect, and the two boundaries are the change.** + Only the row-key *fallback* is wrong for an object-bound grid, so only that is + given up, and only once there is a policy to give it up to + (`!schemaFields && !!objectName && !!objectSchema`): + + - **An authored `fields` projection still wins.** The schema path drops a name + the object does not declare (`if (!field) return;`), and a host may + legitimately join or derive keys, so an explicit projection is not overridden + — including when it names an audit column on purpose. `!schemaFields` is + exactly the condition under which the `||` reaches for the row keys, so the + gate cannot drift from the fallback it guards. + - **Gating on `objectName` alone would have been a worse defect.** The schema + arrives from an async fetch, so `objectSchema` is `null` on first paint; that + gate falls through to `if (!objectSchema) return []` and paints an empty + header row before flipping. Requiring the *loaded* schema keeps the row-key + columns on screen until the object is actually known, and is also the + graceful fallback when the schema fetch fails or the data source has no + `getObjectSchema` — the grid degrades to heuristic columns rather than going + blank. + + Inline data with no object behind it is untouched: the "Legacy support" path is + reordered, never deleted, and is still the right answer there. + + Scored **patch**, deliberately. No public API moves — no prop, type, export or + signature changes — and this restores the default-columns policy the component + already documents and already applied whenever the grid fetched its own rows; + the host-fed divergence was the defect, not a contract. `minor` was considered, + because the visible column set changes on existing screens, and rejected: the + lost columns were never *declared* by any author, only leaked by the branch + order, and this repo scores behaviour-correcting fixes as patch and reserves + `minor` for new capability (a `major` is never authored here — the fixed group + tracks `@objectstack`). +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 19f3637: `ObjectGrid` no longer copies `reference_to_field` onto a relational column's `fieldMeta` + (objectui#6711). + + `RELATIONAL_META_KEYS` listed nine keys that `applyRelationalMeta` copies off the + object-schema field def onto the built `fieldMeta`, at all three of `generateColumns`'s + column-building call sites. `reference_to_field` had **zero member reads**: swept across + `packages/` and `apps/` (and again across the producer repo), the only occurrences of the + identifier anywhere were the array literal itself — the write — and prose recording that + nothing reads it. No member access, no destructuring, no bracket read. + + The control that makes that zero a reading rather than an artefact of how the sweep was + written: the same sweep over its list-mates finds real readers for each of them — + `reference_to` / `reference` / `display_field` in `LookupCellRenderer`, and `id_field` / + `description_field` / `lookup_filters` / `lookupFilters` in `LookupField` / `UserField`, + which is what the grid's editable cells need. + + Nothing renders differently. The key is not a member of any declared type on either end: + `applyRelationalMeta` writes into a `Record<string, any>`, the bag reaches cell renderers + through an `as any` cast, and the declared `FieldMetadata` union it is cast to does not + declare it (nor does `BaseFieldMetadata` carry an index signature). `@objectstack/spec` + 17.2.0's `FieldSchema` does not declare it either — it is in none of that schema's 64 + props — so nothing authorable produces it. This is the same defect class the sibling + producer retired twice: objectui#6625 (`FieldMeta.decimals`) and objectui#6597 + (`FieldMeta.referenceTo`). + + ⚠️ **What the measurement bounds.** The sweep covers this repo and the producer repo. A + host application outside them could still be reading `reference_to_field` off the + `fieldMeta` a cell renderer receives; that was never a declared promise this renderer made, + and this repo's own contract is what the retirement is about — but the world was not + measured, and a host reading the key gets `undefined` after this change. + + Because the key had no readers, the suite stays green whether or not the removal is + correct, so the absence is pinned directly instead + (`__tests__/relationalMetaCopySet-6711.test.tsx`): all three call sites, each with a + presence assertion on the eight surviving keys as the control against a fixture that + passes by never reaching the copy path. +- 9bd08fe: `ObjectGrid` re-applies field-level security on its inline-data column path too, + so whether an object-bound grid re-checks FLS no longer depends on who fetched + the rows (objectui#6723, maintainer ruling 2026-08-29). + + `generateColumns()` re-applied FLS at exactly one place — the object-schema + path. The inline-data path, taken when a host hands rows down as `data` **and** + the author declared a `fields` projection, had no equivalent check. Both paths + serve object-bound grids, so the same object with the same authored projection + did or did not go through the field gate purely according to provenance: + + | rows from | `fields` declared | path taken | FLS re-applied | + | --- | --- | --- | --- | + | grid fetches | no | object-schema | yes | + | grid fetches | yes | object-schema | yes | + | host passes `data` | no | object-schema (since objectui#6677) | yes | + | host passes `data` | yes | inline-data | **no, until now** | + + The inline-data path now filters each column through + `perms.checkField(objectName, fieldName, 'read')` when `perms.isLoaded && + schema.objectName`, the same gate and the same deferral condition the + object-schema path has always used. + + ⚠️ **Only keys the OBJECT DECLARES are judged, and that limit is load-bearing + rather than an optimisation.** Host-joined and derived keys pass through + untouched, because keeping them is this path's whole reason to exist — the + object-schema path drops them outright (`if (!field) return;`). A field policy + that enumerates readable fields answers "no" for a key it has never heard of, so + judging derived keys would silently drop them, which is the failure the issue's + own analysis warned about. Declaration is read with `hasOwnProperty`, so an + inherited name (`constructor`) is not mistaken for a declared field. + + **Defence in depth, not a reachable exploit through the shipped hosts.** + `ListView` — the dominant host — already filters its own `effectiveFields` + through this same gate before forwarding, and that redundancy is the point: the + invariant must not rest on every future host having read the docs. The exposure + this closes is a direct + `<ObjectGrid schema={{ objectName, fields }} data={rows} />` composition, or a + future host that forwards an authored projection unfiltered. + + Deliberately unchanged, and refused by name in the ruling: the two paths' other + differences stay as they are — the schema path's `resolveFieldLabel` (i18n) vs + the inline path's local humanisation, and the schema path's drop of names the + object does not declare. Converging those is a separate decision. + + Pinned in `packages/plugin-grid/src/__tests__/inlineDataFls-6723.test.tsx` (a + readable declared field renders; an unreadable declared field does not, even + with host data for it; a derived key is unaffected; plus the perms-not-loaded, + no-`objectName` and schema-in-flight boundaries and a case through the real + `PermissionProvider`) and, as a measured no-op on the `ListView` path, in + `packages/plugin-list/src/__tests__/ListView.inlineFlsNoop-6723.test.tsx`. +- fab4802: `ObjectGrid` re-applies field-level security on its authored `columns` path too, + so all three of `generateColumns()`'s default paths now go through the field + gate (objectui#6799, maintainer ruling 2026-08-30). + + objectui#6723 closed the inline-data path and left this one. It was the worst of + the three to leave, because it is the **most reachable**: the inline-data path + needs a host to hand rows down, while the authored `columns` path runs whether + the grid fetches its own rows or not. + + | path | reached when | FLS re-applied | + | --- | --- | --- | + | authored `columns` (`ListColumn[]` and `string[]` arms) | `schema.columns` present and non-empty | **no, until now** | + | inline-data | host passes `data` and `fields` is declared | yes (objectui#6723) | + | object-schema | everything else | yes | + + Both arms now filter through `perms.checkField(objectName, fieldName, 'read')` + when `perms.isLoaded && schema.objectName` — the same gate and the same deferral + condition the other two paths use. + + **What a consumer will feel.** A grid that composes `ObjectGrid` directly with + an authored `columns` projection will now render *fewer* columns for a principal + whose field policy denies them: a column naming a declared field the user may + not read disappears, where it previously rendered with its values. If your host + already filters its projection through `checkField` before forwarding — as + `ListView` does — nothing changes at all; this is a measured no-op on that path. + Hosts that did **not** filter first will see the difference, and that is the + point of the change rather than a side effect of it. + + ⚠️ **Only keys the OBJECT DECLARES are judged, and that limit is load-bearing + rather than an optimisation.** Host-joined and derived columns pass through + untouched. It matters more here than on the inline-data path: a `ListColumn` + carries `label` / `link` / `action` / `prefix` / `width`, so a column whose + `field` the object does not declare is not a mistake but a legitimate authored + derived column, and dropping it would destroy authoring work. A field policy + that enumerates readable fields answers "no" for a key it has never heard of, so + judging derived keys would silently delete them. Declaration is read with + `hasOwnProperty`, so an inherited name (`constructor`) is not mistaken for a + declared field. + + **The judged key is read through `columnIdentity`, never off a bare string.** It + folds the three authored identity spellings — `'salary'`, `{ field: 'salary' }` + and the legacy `{ name: 'salary' }` — which is why one predicate serves both + arms. A gate reading `col.field` directly would find no identity on the legacy + spelling and wave a denied declared field straight through. + `resolvesToDataColumn` still owns its own decisions and runs first, so the gate + narrows what survives and never resurrects a hidden or unresolvable column. + + **Defence in depth, not a reachable exploit through `ListView`.** Measured in + this repo: three shipped compositions reach this path without filtering first — + `ObjectView`, and the designer's `ObjectManager` and `FieldDesigner` — plus two + dev/demo harnesses. `ListView` filters its own `effectiveFields` through the + same gate before forwarding, and that redundancy is the point: the invariant + must not rest on every future host having read the docs. + + objectui#6598's `hasAuthoredColumns` predicate is unchanged and its rationale is + rewritten in the same change: it used to rest on "the grid would not re-check", + which is no longer true, and it now rests on the half that never depended on the + grid — an empty projection is the author's projection after filtering, and the + object's default columns are not what was authored whether or not they are + FLS-checked on the way out. + + Pinned in `packages/plugin-grid/src/__tests__/authoredColumnsFls-6799.test.tsx`. +- f9984c0: `ObjectGrid` no longer copies `titleFormat` onto a relational column's `fieldMeta` + (objectui#6874). + + `RELATIONAL_META_KEYS` listed eight keys that `applyRelationalMeta` copies off the + object-schema field def onto the built `fieldMeta`, at all three of `generateColumns`'s + column-building call sites. `titleFormat` was one of them and had **zero FIELD-meta + readers**. + + This is a zero of a different kind from objectui#6711's, and a stronger one: `titleFormat` + is a real, live key with plenty of readers — it just has none on a field meta. The sweep + did not fail to find readers. It found every member read of the identifier across + `packages/` and `apps/` (tests included) and classified each by its receiver: + + - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`, + `components/renderers/layout/containers.tsx`, `plugin-detail/DetailView.tsx`, + `plugin-kanban/ObjectKanban.tsx`, `plugin-calendar/ObjectCalendar.tsx`, + `react/hooks/useRecordSearch.ts`. An OBJECT schema, every one. + - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the REFERENCED + object's schema, fetched by `getSchema(referenceTo)`. Also an OBJECT schema, and the one + that decides this case — it is what the grid's own inline picker reads. + - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved `ActionParamDef`. + The field-def read beside it is `field.title_format`, a different spelling on a different + surface. + + `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the repo's single + `titleFormat=` pass is `titleFormat={refTitleFormat}` — object-schema sourced. So copying + `reference_to` is what makes `titleFormat` work on this path, and copying `titleFormat` + onto the meta reached nothing. + + Nothing renders differently, and the argument does not rest on the member sweep alone: the + only computed access to the meta bag anywhere in `@object-ui/fields` or `plugin-grid` is + `applyRelationalMeta`'s own write, so no consumer can pick the key up dynamically. The key + is also not a member of any declared type on this path — `applyRelationalMeta` writes into + a `Record<string, any>` and the bag reaches cell renderers through an `as any` cast. + + `plugin-dashboard/src/recordFields.tsx` had already recorded this exact measurement as its + reason for not copying the key into that seam, so it was a measured no-op in two seams and + retired from only one. Same defect class as objectui#6625 (`FieldMeta.decimals`), + objectui#6597 (`FieldMeta.referenceTo`) and objectui#6711 (`reference_to_field`), and the + same disposition as objectui#6711 on this very list. + + ⚠️ **What the measurement bounds.** The sweep covers this repo and the producer repo. A + host application outside them could still be reading `titleFormat` off the `fieldMeta` a + cell renderer receives; that was never a declared promise this renderer made, and this + repo's own contract is what the retirement is about — but the world was not measured, and a + host reading the key off a field meta gets `undefined` after this change. The supported + source is unchanged and unaffected: the referenced object's schema. + + Because the key had no readers on this path, the suite stays green whether or not the + removal is correct, so the absence is pinned directly instead + (`__tests__/relationalMetaCopySet-6874.test.tsx`): all three call sites, each with a + presence assertion on the seven surviving keys as the control against a fixture that passes + by never reaching the copy path. +- 9700dd9: A lookup cell in `ObjectGrid` now honours the author's `displayField` + (objectui#6875). + + `ObjectGrid` copies a set of relational keys off the object-schema field def + onto each column's `fieldMeta`, and that bag is what the lookup cell renderer + and the inline picker receive. The set was hand-kept and had become a strict + SUBSET of what those two consumers read — `displayField`, `descriptionField` + and `lookupColumns` were read on the grid's own path and never copied. + + They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema` + is strict and declares `displayField` / `descriptionField` / `lookupColumns` / + `lookupFilters` / `reference`, and none of the snake_case twins the copy set + mostly carried — those parse to `unrecognized_keys`, so a spec-compliant + producer cannot emit them. Nothing renames anything on the way in either: the + adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄ + `reference_to` pair. So an author who declared `displayField: 'project_code'` + got a grid cell showing the referenced record's generic `.name` instead. + + - The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from + a table that classifies every key the consumers read off this bag. A gate + re-extracts that read set from the consumer sources on each run and fails on + any unclassified spelling or orphan, so the two cannot drift apart again. + - `reference_field` and `lookup_columns` — the other two never-copied keys — + stay out on purpose: `FieldSchema` declares neither, so no producer can fill + them. The gate proves that against the installed spec rather than asserting it + in prose. + - `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the + same fallback chain and gains `displayField` too. +- 1349400: ObjectGrid: field-level security on the server `$select` projection + (objectui#6898) — the FETCH half of the gap objectui#6799 closed on the RENDER + half. + + `getSelectFields()` built the projection from the authored `columns` / `fields` + with no FLS gate, so after objectui#6799 hid the column the field name was still + being ASKED for. `perms.checkField(object, field, 'read')` now gates the + projection, on both authored arms and on the predicate-operand harvest. + + Measured, because the grade depended on it: ObjectStack's own server enforces + FLS on the RECORD, not on the projection — `plugin-security`'s read middleware + deletes an unreadable key from every returned row, and its `predicate-guard` + says in terms that the projection is deliberately unguarded because the masker + strips the value anyway (pinned over real HTTP by objectstack's + `showcase-fls-read-mask-strip.dogfood.test.ts`, where `?select=name,<denied>` + answers 200 with the key absent). So against ObjectStack this is + defence-in-depth; it becomes load-bearing for any backend that does not strip. + + Two limits are deliberate and pinned: + + - Only keys the object DECLARES are judged. `checkField` answers `false` for a + field no policy mentions, so judging an undeclared key would strip a host's + derived or joined column out of its own query. + - `id` survives even a policy that denies it, structurally — `ensureId` composes + after the gate — so row navigation cannot break. Readable predicate operands + are untouched, so objectui#3501 does not regress. + + The fetch effect now also depends on `perms.isLoaded`: `/me/permissions` + resolves asynchronously, so without it nothing would rebuild the projection + after the policy answered and the gate would never run on the only fetch most + grids make. +- 84ffdbc: Fix: a `dependsOn` lookup column is no longer permanently uneditable in an + editable `ObjectGrid`. + + `LookupField` resolves the record it gates on as + `dependentValues ?? ctx.formValues ?? ctx.data ?? {}`, and the grid's inline + cell editor supplied **none** of the three — `renderCellEditor` rendered + `FieldEditWidget` with `field` / `value` / `onChange` only, `SchemaRendererContext` + has no `formValues`, and the grid sets no `ctx.data` for a row. The resolved + record was therefore `{}` for every row, so a column declaring `dependsOn` + rendered a disabled trigger reading "Select region first" **even when the row + carried the parent value**. The field could never be filled and nothing said + why. + + PR #2216 closed #2215 in two halves: the form renderer injects its live watched + record as `dependentValues`, and every picker takes the `dependsOn` chain as a + hard `baseFilter`. The second half is host-independent and was already live on + the grid path — which is why the gate fired at all. The first half is per-host + and the grid never got it. `renderCellEditor` now passes + `dependentValues={ctx.row}`, supplying that missing input; no cascade is + re-implemented. + + ⚠️ Interim, and deliberately labelled as such in the code (#7165): `ctx.row` is + the **saved** record, so a parent edited but not yet saved in the same row does + not re-scope the child — it stays scoped by the persisted value. Matching the + form's live-record semantics needs a new member on `renderCellEditor`'s + published context type and is tracked as #7188. +- a276480: `ObjectGrid` no longer copies `descriptionField`, `lookupColumns` or `lookupFilters` onto a + relational column's `fieldMeta` (objectui#7166). No behaviour change — all three still reach + the inline lookup picker, by a different seam. + + `applyRelationalMeta` writes the copy set onto the `fieldMeta` that `generateColumns` hands + to `<CellRenderer>` as the `field` prop — six JSX passes across the three column-building + paths, and nowhere else. For a relational column that resolves to `LookupCellRenderer`, + which reads exactly `reference_to`, `reference`, `display_field`, `displayField`, + `reference_field` and `options`; a `user` column resolves to `UserCellRenderer`, which + destructures `{ value }` and reads no field meta at all. Measured by receiver rather than by + count: `packages/fields/src/index.tsx`, the file holding **every** cell renderer, contains + **zero** occurrences of the three retired keys, against a control of 22 occurrences of the + `display_field` / `displayField` / `reference_to` spellings the cell does read. + + Their only readers off a field meta are `LookupField` and `UserField` — the two **editor** + widgets — and the grid's inline editor does not receive this bag. `renderCellEditor` looks + the field up in the object schema and spreads the whole def into the widget + (`{ name: ctx.column.accessorKey, ...fieldDef }`), so every key a def carries reaches + `LookupField` whether or not it is copied. The copies were dead writes: the objectui#6711 + (`reference_to_field`) and objectui#6874 (`titleFormat`) class, arriving from the opposite + direction — those keys had no *declaration*, these have no *reader on this path*. + + Two of the three, `descriptionField` and `lookupColumns`, were **added** by objectui#6875 as + its fix, classified from a read-set derivation alone. Its third key, `displayField`, is + genuinely delivered and stays copied — and it is the one that arrived with a rendering test. + ⭐ The generalisable lesson, now recorded in both docblocks: **a derivation establishes that + a consumer READS a key; it does not establish that a given BAG is how the consumer gets it.** + + - New `__tests__/relationalMetaCopySet-7166.test.tsx` renders both directions. The **cell**: + four lookup columns over one referenced record, differing only in the key under test, all + resolve the same text, while the `displayField` control column resolves something else — + the control is what makes the three zeros readings rather than a fixture that never reached + the lookup path. The **editor**: each retired key still takes effect in the inline picker + with the copy set no longer carrying it — `descriptionField` drives the secondary line, + `lookupColumns` shapes the picker columns, `lookupFilters` scopes the candidates — each + against a sibling control column that declares nothing. + - The three snake_case `legacy-alias` spellings — `description_field`, `lookup_filters`, + `id_field` — have the same reader-side verdict and are **deliberately kept**. They are + recorded as legacy aliases precisely because a host `DataSource` outside these repos may + hand-feed them; that is a producer-side argument, untouched by this reader-side + measurement. Their verdict is now recorded on the table with the open producer question + stated, so the next pass inherits a measurement instead of a silence. + - ⛔ The derivation gate cannot enforce this retirement, and now says so. Its read set is a + union over three consumers, two of which are not fed this bag, so all three retired keys + remain in it and every derived assertion passes whichever verdict they carry. Their absence + is pinned by an explicit hand-written assertion plus the rendering test. Re-scoping the + derivation around the cell alone is a design change to objectui#6875's mechanism and is + filed, not made here. + - The gate is strengthened, not weakened: every `deferred` verdict is now mechanically proved + spec-declared against the installed `FieldSchema`, and the sibling pins + (`relationalMetaCopySet-6711` / `-6874`) turn the retired `lookupFilters` into a live + negative assertion on a fixture that still declares it. +- a6d8b8d: Fix: a grid grouped by a field it does not also show as a column no longer collapses + every row into one `(empty)` group (objectui#7179). + + `$select` was built from the view's `columns` and nothing else, so a view declaring + `grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns + never asked the server for that field. It was `undefined` on every row by the time + grouping ran, and the grouping label builder — correctly, for a genuinely empty value — + answered `(empty)` for all of them. The result was one collapsible group holding every + record, with no error, no warning and no empty state: a grid that looked like it grouped + and did not, reading as "these records have no value for this field". + + The grouping fields are now unioned into the projection, at both places it is built — + `ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the + rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that + fetches a bare foreign key without populating it buckets by raw id instead of by name, + which is a different wrong answer rather than a fix. + + Authors do not need to mirror a grouping field in `columns` any more. That was never + required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of + it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their + `groupByField` with no column needed. Refusing the configuration at author time was + considered and rejected: it would make the grid the odd one out and reject working + intent that the schema explicitly allows. + + The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]` + entry carries a bare string that has never been through column validation, and some + backends answer an unknown `$select` key with an empty result set rather than ignoring + it. Unioned unguarded, a grouping field naming something the object does not declare + would have turned this bug into a strictly worse one — no rows at all, equally silently. + Grouping fields are therefore intersected with the object's declared fields and passed + through the same field-level-security gate as columns and predicate operands before they + reach the query. +- adbda1b: Fix: a `dependsOn` lookup edited inline follows a parent edited in the same row **before** + it is saved (#7188, finishing #7165). + + #7165 shipped an explicitly-labelled interim: the grid's inline editor scoped a + `dependsOn` picker by `ctx.row`, the **saved** record. Edit the parent cell, do not save, + open the child — and the child still listed candidates for the parent's persisted value + (or stayed gated if that value was empty). The grid now scopes by `ctx.pendingRow ?? ctx.row`, + where `pendingRow` is the row merged with its staged, unsaved edits, so picking a parent + re-scopes the child immediately — the form's live-record semantics from #2216. The interim + marker that named #7188 is gone with the interim. +- 67dadd6: FLS-gate the `$expand` projection at both build sites (objectui#7215). + + objectui#6898 closed field-level security on `$select`. `$expand` was left ungated at + both projection sites — `ObjectGrid`'s own fetch and `ListView`'s `expandFields` memo — + so a `lookup` / `master_detail` / `user` / `tree` field the current principal cannot + read was still handed to the server for expansion. `$select` on a denied lookup asks for + its bare foreign key; `$expand` on the same field asks the server to resolve it and + return the related record, so the larger of the two disclosures was the ungated one. + + **Reproduced before it was fixed**, as failing tests at both sites, and the same leak + reaches further on the `ListView` path: that builder's `$select` gate drops the denied + column and then adds the expand roots back unconditionally, so the denied field walked + back into `$select` as well. Gating the expansion closes both halves. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 is: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row, and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip. + + **Nothing a permitted view did stops working.** The gate judges the OUTPUT of + `buildExpandFields`, which is already a subset of the object's declared + reference-bearing fields, so the "`checkField` answers false for an undeclared key" + trap cannot be reached and derived / host-joined columns are untouched. An unanswered + permission policy filters nothing. `buildExpandFields` itself is unchanged. +- e21308e: Grid: a malformed entry in `grouping.fields[]` no longer crashes the whole grid. + + A `null` (or `undefined`) hole in the array was dereferenced with no guard at + two places — `ObjectGrid`'s `groupValueFormatter` memo and `useGroupedData`'s + `buildLevel` — throwing `TypeError: Cannot read properties of null (reading + 'field')` during render, before any projection was built. Both sites now read + one normalized entry list, admitting exactly the entries `collectGroupingFieldRefs` + harvests into the query projection, so the usable grouping levels still group + and an unusable entry is simply dropped rather than taking the view down. +- 19f1639: A synthesized default list view now always leads with the object's name field + (objectui#7245). + + **The defect.** An object that declares no list view gets its default grid columns + synthesized from `highlightFields`, taken verbatim. But `highlightFields` is ADR-0085's + *"most important fields"* role, not a column list — and its first consumer, the + detail-page highlight strip, **deliberately removes the title field**, because the page + H1 directly above it already shows one. So metadata that is entirely correct routinely + omits the record's name from `highlightFields`. The showcase `showcase_account` declares + `nameField: "name"` and `highlightFields: ["status", "industry", "annual_revenue"]`, and + its default `所有记录` grid rendered 14 rows whose columns were `#` / Lifecycle / Industry + / Annual Revenue / actions — no name column, and no way to tell one account from another. + + A list has no H1 to lean on, so the same declaration needs the opposite treatment here. + This is not a new convention: `deriveLookupColumns` in `@object-ui/fields` already leads + its record-picker columns with the display field and filters it out of the declared list. + The list faces now agree with it. + + **What changed.** `@object-ui/core` gains two exports on the ADR-0079 title ladder: + + - `resolveNameField(objectDef)` — *which field* titles an object: the declared + `nameField` (then its deprecated `displayNameField` / `NAME_FIELD_KEY` aliases), else + the type-aware derivation. The name-space twin of `getRecordDisplayName`, which answers + what that field *says* on one record. Both now read one spelling of the declared + pointer, so they cannot drift into naming different fields. + - `leadWithNameField(objectDef, columns)` — moves that field to the front of a + **synthesized** column list. + + All three faces that synthesize default list columns call it: `ObjectView` + (`defaultListColumnsFromObject`), `InterfaceListPage` (`defaultColumnsFromObject`) and + `ObjectGrid`'s own derivation. The name field is **moved**, not merely appended, so an + author who lists it third still gets it first — "the column that identifies the row" + means first. On the two capped faces the lead is applied *before* the 5 / 6-column slice, + so an object declaring its name field late no longer loses it off the end. + + **Scope, deliberately narrow.** Author-declared column lists are untouched — a view or + grid that declares `columns` / `fields` said what it wants, and reordering it would be + renderer-side second-guessing of metadata. Three cases also decline to lead: a name field + the object carries no field def for (never fabricate a column), one marked + `hidden: true` (the author said don't show it), and a *derived* pick that lands on a + system-managed column — `deriveTitleField` filters by type only, and leading a default + list with a raw id is the regression objectui#2702 / #2777 fixed. A *declared* + `nameField` pointing at a system field still leads: `sys_migration` really does point at + `id`, and an explicit designation is not a heuristic misfire. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- 9e22085: `ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of + byte-copying it onto `$filter` (objectui#4082). + + The query assembly already lowered the canonical `filter` key through `toFilterNode` — + the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch + beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the + one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter` + and `plugin-view`'s non-grid fetch both already route the same value through + `toFilterNode` / `mergeFilterNodes`. + + Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is + declared `Record<string, any>` (the MongoDB-style shape) and `isFilterAST` is false for a + plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one + answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. + + `toFilterNode` handles both without new logic: objects route through + `convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot + passes through untouched, so nothing is lowered twice. It also folds an absent or empty + source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` + used to send `$filter: {}`, asking the server a question with no content in a shape it + refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. + + **Grade — this is less dormant than the card assumed.** objectui#4082 was filed + observation-class on "no measured producer", reasoning that `defaultFilters` is not in + `object-grid`'s registered `inputs` so an author writing it only draws a save-gate + warning. That reasoning covers authors, but not the framework: `plugin-view`'s + `ObjectView` writes the slot itself, forwarding an active named view's `filter` as + `defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — + and `plugin-view`'s own README documents `listViews.<name>.filter` as + `[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as + `400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no + `renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, + and `ListView`'s lowering does not cover it. So a schema-registration host — the + documented authoring path — reached the raw assignment whenever an active named view + carried a filter. Not asserted here: a failing request captured against a running + deployment. `app-shell` is unaffected either way; it supplies `renderListView` and + delegates to `ListView`, which lowers. + + Not in scope, and deliberately not done: retiring `defaultFilters`. This is + consumer-side only — the key the schema admits is unchanged, and its precedence behind + the canonical `filter` is unchanged. + + The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the + measurements. +- c574dfb: `ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence. + + objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so + `ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer + reads. That was right — the spec refuses `accessorKey` and `header` by name, and the + census found zero authored usages. But it relocated a failure mode instead of removing + it: a column authored in a spelling the renderer does not read contributed nothing, and + nothing said so. No error, no warning, no empty state — the author got a grid with its + row-number column and no data columns, which is a success receipt for a disagreement + between the renderer and the author. + + An authored column that can never resolve now emits one `console.warn` naming the + address rather than the symptom: which block (`object-grid` or the `view:grid` alias), + which object and label, which `columns[i]`, the keys that entry actually carries, and the + rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }` + the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped + it" (the export-format warning), rather than adding a second differently-shaped one. + + Rendering is unchanged in every case: this is additive. The diagnostic reads the + `columns` input and nothing else — it never asks whether the grid found rows, because + `object-grid` legitimately draws them from five different places (a bare `data` array, + `data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and + passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView` + does). All five are pinned by test, in both directions. A `hidden: true` column is + authored intent and is never reported, and so are the arms that legitimately produce no + columns of their own: no `columns` key, an empty `columns` array, and the `string[]` + spelling. + + A throw was rejected: a grid that renders nothing today would become a page that renders + nothing. +- 02f48b6: `object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. + + `userActions` names two different blocks. On a **view** it is toolbar policy — + the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, + `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by + name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / + `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is + the only shape `listViewPredicates` can read, since its loop skips every + non-object value. + + `ObjectGrid` read the key view-first when building the `$select` projection + (`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a + perfectly legal toolbar block therefore shadowed the object's CRUD predicates, + the harvest found none, and the predicate's operand left the projection. CEL then + faults on the absent key, fails closed, and the row Edit/Delete button disappears + for everyone with nothing pointing at the projection — objectui#3501's failure, + reached with a success receipt at every step. + + The view-level block is not hypothetical: `SpecBridge.transformListView` copies + it onto the `object-grid` node the renderer receives, and `app-shell`'s + `ObjectView` builds one unconditionally. + + The harvest now reads the resolved object block only. Both `userActions` read + sites carry a comment naming the collision, and + `__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes, + the producer that writes the view one, the harvest's blindness to it, and the + projection that must keep the object's operand with a toolbar block present. + + Toolbar policy itself is untouched — it was never read through this path. +- 591bf27: Guard `useRowColor`'s two object-literal lookups with `Object.prototype.hasOwnProperty.call`. + + Both `config.colors` (the authored map) and the module's `COLOR_TO_CLASS` literal inherit + `Object.prototype`, and both were reached with a bare index. A record whose colour field + held `constructor`, `toString`, `valueOf` or `hasOwnProperty` resolved to an inherited + function: the `if (!color)` guard passed it (functions are truthy) and `colorToClass` then + called `.startsWith` on it, throwing a `TypeError` inside the row-className resolver during + render — a grid crash triggered by record data rather than by metadata. The same shape one + call deeper in `colorToClass` did not throw; it handed an `Object.prototype` member back as + the row's `className`, which reached React as a class attribute. Both now resolve to + `undefined`, as an undeclared value always did. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-grid/package.json b/packages/plugin-grid/package.json index 4cb92b5682..665a489883 100644 --- a/packages/plugin-grid/package.json +++ b/packages/plugin-grid/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-grid", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Grid plugin for Object UI", diff --git a/packages/plugin-kanban/CHANGELOG.md b/packages/plugin-kanban/CHANGELOG.md index 055d641658..62efa60579 100644 --- a/packages/plugin-kanban/CHANGELOG.md +++ b/packages/plugin-kanban/CHANGELOG.md @@ -1,5 +1,1021 @@ # @object-ui/plugin-kanban +## 17.7.0 + +### Minor Changes + +- b3d562c: One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn` + inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155). + + The 2026-08-25 family ruling: every exported schema name has exactly one + authority. Two of this card's names are discharged here. + + **`MarkdownSchema` — converged onto `@object-ui/types`.** + `@object-ui/plugin-markdown` declared a second copy of the name. The two + differed on exactly one member — `content`, required in `@object-ui/types` and + optional in the plugin — and that was measured to be drift rather than a real + semantic difference: the plugin's own registration declares the `content` input + `required: true` (pinned by its own test), `MarkdownImplProps.content` is a + non-optional `string`, the Zod mirror spells `z.string()`, and every authored + `type: 'markdown'` node in the repository supplies `content`. The plugin now + re-exports the one authority. + + ⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown` + consumers**: `MarkdownSchema['content']` goes from optional to **required**. A + value annotated `MarkdownSchema` that omitted `content` no longer type-checks. + Measured against this repository: zero authored markdown nodes omit it, so + nothing in-tree changed. (`type: 'markdown'` literals that carry no `content` + are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.) + The plugin's face also gains the optional `sanitize` and `components` members + the canonical declaration carries; both are additive, and neither is read by + this renderer, which sanitizes unconditionally. + + `className` is unaffected — it comes from `BaseSchema`, which both copies + extended, so it was always inherited rather than added by the plugin. + + **`KanbanCard` / `KanbanColumn` — the three in-package copies converged to + one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A + TypeScript-AST comparison found them strict-SUBSET copies of `./types` with + nothing typed differently, so their extra members moved onto the one + declaration and both files now re-point at it. + + Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and + `coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every + value that type-checked before still does. Both modules keep their previous + export surface via re-export, so no import path changes. + + The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision + between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here + and is escalated on objectui#6172 — those are two different dialects (`items` + vs `cards`, `labels` vs `badges`), and collapsing them renames a published + name, which needs an authority ruling. +- 6ca6e12: `ObjectKanbanComponentProps.schema` names both node types the component is registered for + (objectui#7322 item ②, following the objectui#5903 / #5018 land shape). + + `ObjectKanbanRenderer` is registered under two keys — `'object-kanban'` and `'kanban'` — + and the two keys have different declared node types: `ObjectKanbanSchema` (`type: + 'object-kanban'`, `objectName` and `groupBy` required) and `KanbanSchema` (`type: + 'kanban'`, both optional). The prop named `KanbanSchema` alone, so **no `object-kanban` + node was assignable to the component that renders it**, and the discriminants are disjoint + string literals, so no cast-free annotation existed for half the boards this component + serves. It is now the union of the two. + + ## What settled it: the read set + + `ObjectKanban` reads thirteen keys off `schema`. Neither declaration covers them; the two + TOGETHER cover twelve, and each arm is load-bearing: + + - `objectName`, `groupBy`, `limit`, `cardFields` — declared on both; + - `columns`, `cardTitle`, `swimlaneField`, `grouping` — `KanbanSchema` only; + - `titleField` — `ObjectKanbanSchema` only (which is why that read was spelled + `(schema as any).titleField`); + - `data`, `bind`, `className` — `BaseSchema`; + - `filter` — declared by **neither** face, still riding `BaseSchema`'s index signature. + Measured and reported, **not** changed here: this card moves the prop, not the two + published schema faces. + + So naming `ObjectKanbanSchema` alone — the remedy the original card implied — would have + been wrong in the other direction: it drops four declared reads and the `'kanban'` + registration. + + ## Not affected + + Widening a member of an exported prop type is additive: every caller that passed a + `KanbanSchema` still compiles, and the union claims exactly the accept set the registry + dispatches to this component — a third node type is still turned away. The runtime is + untouched; `ObjectKanbanRenderer` still takes `schema: any`, so no shape is turned away + there either (the objectui#5903 disposition, restated). The view-level `kanban.groupField` + alias, `BaseSchema`'s index signature, and `@object-ui/types` are all untouched. + + ## Casts this removes + + Inside `ObjectKanban.tsx`, three schema-key reads drop their `as any`: `titleField` (two + sites, now honest because the `object-kanban` arm declares it) and `cardFields` / + `cardTitle` (already declared; the casts were redundant). `(schema as any).navigation` + **stays** — `navigation` is declared on neither face, so removing the cast would change + nothing but the spelling of an index-signature read. + + Four of the six in-package fixtures that mount an `object-kanban` board drop their + `as never` escape for a real `satisfies ObjectKanbanSchema`. The other two are static + boards (`columns` + inline `data`, no fetch) that author no `objectName`, which + `ObjectKanbanSchema` declares required — objectui#7780's subject; their casts stay, now + carrying the reason and the card number. +- 0ead1f6: Declare the `filter` input on every `object-kanban` / `object-calendar` + registration (objectui#7712) — the html tier stops reporting `unknown-prop` on a + key the spec declares and both renderers read. + + `ObjectKanban.tsx` sends the authored key to the query as `$filter: schema.filter` + and `ObjectCalendar.tsx` does the same, and `@objectstack/spec`'s + `ComponentPropsMap` declares `filter` on both blocks (measured: `safeParse` + accepts it, and refuses an undeclared key by name on the same call). But none of + the four registrations that publish those two renderers listed `filter` in + `inputs`, and `sdui-parser`'s `validateTree` reports `unknown-prop` for every key + no `inputs` entry claims. So an author writing the one spelling that WORKS was + told it was unknown — objectui#6678's shape, where a correct write draws the same + diagnostic as a write that does nothing. That is worse than an inert key: it + actively punishes the correct behaviour, and the honest response to it is to + delete working metadata. + + ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has + live readers on both ends, so the registrations were the side that was wrong. The + declaration is `type: 'array'` on all four, matching the `filter` that + `object-grid`'s `GRID_QUERY_INPUTS` and `object-metric` already publish, and it is + writable as one shape only because objectui#7711 landed first and retired the + object-shaped `filter.calendar` spelling — `filter` is the query filter and + nothing else. + + Declared per key against the spec rather than derived from the + `ElementDataSourceMapping` sitting beside these registrations, even though that + mapping already asserts `filter` is a live query key. Measured, that derivation + would be wrong: the kanban mapping also carries `limit`, and the spec's strict + `ComponentPropsMap['object-kanban']` **rejects** `limit` by name, so emitting it + would publish a key the save gate refuses. + + ⚠️ Note for whoever meets this class next: `check:react-blocks-declaration-parity` + runs manifest → spec, one direction. A key the SPEC declares and the manifest + omits is structurally outside what that ratchet measures, so fixing these four + registrations does **not** make the next omission loud. Making that ratchet + bidirectional is its own card. + +### Patch Changes + +- 062943f: `@object-ui/plugin-grid` and `@object-ui/plugin-kanban` now publish a stylesheet — + `"./style.css"`, mapped to `dist/index.css` and compiled at build time from the package's + own sources (objectui#4929, maintainer ruling 2026-08-17, Direction 1). + + **What was broken.** Only `@object-ui/components` and `@object-ui/fields` shipped CSS, and + each scans its own `src` only, so a class used exclusively by a plugin could not appear in + either sheet BY CONSTRUCTION. A published-state Vite app that installed one of these two + plugins and followed the quick-start rendered the grid or the board with **25 themed + utilities that had no source anywhere in the world** — `bg-muted/10`, `bg-card/60`, + `text-muted-foreground/60`, `ring-primary/40` and friends, ordinary appearance classes — + plus ~103 plain ones. Re-measured on the merged tree: the 21 the card listed all still hold, + and four more (`[&>h3]:text-foreground/80`, `border-l-primary/40`, `border-primary/30`, + `hover:text-primary`) that its literal-grep method could not see. + + The plain utilities a consumer could in principle regenerate by pointing `@source` at the + package's `dist`. The themed ones they cannot, at all: they resolve `@theme` tokens declared + in `packages/components/src/index.css`, which that package does not publish. A build inside + this monorepo is their only possible producer — which is why the fix is a stylesheet we + ship, not documentation teaching consumers to hand-declare the theme and scan + `node_modules` (the advice objectui#4858 had just retired from the guides). + + **The shape**, inherited from `@object-ui/fields` (objectui#4059): each package gains + `src/index.css` that `@reference`s the components entry — theme tokens, the class-based + `dark` variant and the animate plugin become available for resolution while emitting + nothing — plus `scripts/build-css.mjs`, which subtracts every rule components' published + sheet already ships. So these are **supplements, imported after** the components sheet, and + they are 16.30 kB and 11.41 kB rather than another ~170 kB each: + + ```css + @import 'tailwindcss'; + @import '@object-ui/components/style.css'; + @import '@object-ui/fields/style.css'; + @import '@object-ui/plugin-grid/style.css'; + @import '@object-ui/plugin-kanban/style.css'; + ``` + + Add a line only for the plugins you install; no other `@object-ui/plugin-*` package + publishes a stylesheet yet. The build step is shared + (`scripts/build-plugin-stylesheet.mjs`) so it is the pattern the next one inherits rather + than a file to copy, and it refuses to write a sheet that fails any of four assertions — no + rule may vanish, the subtraction must have removed something, the class count may not pass + a leak ceiling, and named themed utilities only this build can produce must still be + present. + + Nothing is removed and no existing import changes: a consumer who does not import the new + sheets is exactly where they were, and the guides' "do not scan `node_modules`" advice + stays correct — it is now correct for plugins too. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 7c96c94: Four more private copies of the reference-bearing field family converge onto + `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`, and the "fourth and last private copy" claim + that `paramToField` still stated is corrected (objectui#5874, objectui#5875). + + Each copy diverged from the published family in BOTH directions, so this is a behaviour + change on every face and not a refactor: + + - **`user` and `tree` are now treated as relations.** Both carry the same foreign-key + storage as `lookup` and resolve through the same expand path (objectui#2032), and each + face's own stated reason for special-casing `lookup` applied to them verbatim — so + gaining them restores the rule each face already meant. A `user` / `tree` field is now + read-only in the quick-look drawer (`RecordDetailDrawer`), where the drawer wires no + relation picker and a plain text input let a user overwrite the relation with a + free-form string; it gets the wide layout basis in the record header's highlights strip + (`HeaderHighlight`), whose inline editor is a record picker; and a field-backed action + param over one now inherits the picker config it needs (`resolveActionParams`). + - **`master_detail` is now treated as a relation by `resolveActionParams` too** — it was + the only face missing that member as well, so a field-backed `master_detail` action param + inherited no `referenceTo` at all and degraded to the unexplained "paste a record id" + text input that objectui#3405 exists to prevent. + - **The undeclarable `reference` spelling is gone from the three field-type faces.** + Measured against `@objectstack/spec`'s closed `FieldType` vocabulary with live controls + (`lookup` / `master_detail` / `user` / `tree`) and dead ones (the retired `owner`, plus a + nonsense spelling): `reference` is absent, so no spec-compliant object schema could + declare a field that reached those branches. It sat exactly where `owner` sat before + objectui#4814 retired it — dead weight that read as live capability. + + `resolveActionParams` keeps answering for `reference`, deliberately and by a different + route: it is refused by the spec's `ActionParamSchema` too, but the dialog still accepts it + from params already authored with it, and that acceptance belongs to the one alias table in + `paramToField` rather than to a hand-copied membership test. This face now asks the shared + family over the widget key that table produces — the same expression `paramToField` + evaluates one step later, so the half that populates a param's picker config and the half + that forwards it can no longer disagree. + + No face copies the set: each calls `.has()` on the object `@object-ui/core` exports, and + each carries an identity pin (a spy on that `has`) so a member-identical private copy fails + instead of quietly re-forking the table. +- 3e853c9: Let a producer-marked refusal reach the drag-write surfaces (objectui#5902). + + The kanban card-move toast, the calendar drag-to-reschedule toast and the OCC + conflict dialog each substituted a generic string for a refusal the producer had + marked as user-facing (`userMessage`), so a user was told "Save failed" where the + application author had written a sentence addressed to them. All three now read + the marking through the shared `declaredUserMessage` reader, which covers both + places the adapter boundary parks it — the typed member on + `ConcurrentUpdateError` and the details bag on `DataApiValidationError`. + + Nothing unmarked changes: the reader answers `null` for it, so every existing + generic substitution — including the localized "not authorized" message that + keeps raw server diagnostics away from end users — still governs unmarked + refusals exactly as before. + + The two toasts substitute; the conflict dialog augments. Its description also + explains what the destructive "Overwrite" button does, which is affordance copy + that surface owns rather than a refusal message, so the marking leads and that + paragraph stays. +- a76b18c: `ObjectKanban`'s `resolveDisplay` drops an unreachable relation-type guard, and the rule + that survives it is pinned (objectui#6063). + + The helper that builds card descriptions ended: + + ```ts + const isLookup = isExpandableFieldType(def); + if (isLookup && isOpaqueId(raw)) return undefined; + if (isOpaqueId(raw)) return undefined; + return raw; + ``` + + The second line subsumes the first for every input — same `raw` (a `const`, unmodified + between the two lines), same predicate, and `OPAQUE_ID_RE` carries no `g`/`y` flag, so + repeated `.test()` on it is stateless. `isLookup` was computed, branched on and discarded. + + **No behaviour changes on any board.** The card named a second reading — that the + unconditional line was the mistake and only relation columns were meant to suppress + id-shaped values — and it was rejected on evidence rather than by tidying first: the + helper's own docblock declares both clauses, the same predicate is already applied with no + type gate to the incoming `description` a few lines down, and `objectDef` is optional at + that read, so a type gate would suppress nothing on exactly the boards whose object schema + is thin or absent. That reading is now a red test, not a comment. + + Deleting the branch also deleted this path's read of `@object-ui/core`'s + `EXPANDABLE_FIELD_TYPES`, so objectui#5874's identity pin for this face is re-anchored onto + the read that is live — `buildExpandFields`, on every fetch — where the membership delta is + observable on the wire (`$expand`) as well. That pairs the identity pin with the + behavioural counter-probe #5874 had to record as missing. +- 7d2a689: `ObjectKanban` no longer queries twice on mount (objectui#6271). A standalone board issued + its first `find` before the object definition resolved — so `buildExpandFields` saw no + fields and that query carried no `$expand` at all — then issued a second, expanded one once + the definition landed: + + ``` + ['deal', { $top: 100 }] + ['deal', { $top: 100, $expand: ['owner'] }] + ``` + + The definition now GATES the query instead of refining it afterwards: one query per mount, + carrying the expansion the first time. + + Decided on measurement rather than on the two shapes' relative appeal. The first response + never reached the screen in the regimes that matter: with the schema resolving no slower + than the row query (profiles schema/find = 30/30, 30/60, 5/30 ms), the definition lands + first, the effect re-runs, its cleanup flips `isMounted` false, and the unexpanded rows are + discarded on arrival — a DOM probe polling every 2ms for a title only that response carried + never fired once. What the gate costs is one schema resolution ahead of the query, and that + read is cheap and shared: one small GET behind the same discovery call `find` already + awaits, served thereafter from `MetadataCache` (5-minute TTL, concurrent readers coalesced + onto one request). Measured against the real `ObjectStackAdapter` over loopback HTTP, 22 + reads of one object produced exactly one metadata request and every read after the first + returned in 0.01ms. End to end the board is not slower for it — same harness, before → + after, time to the fully populated board: 156.9 → 145.2ms (30/30), 119.8 → 110.6ms (30/60), + 54.7 → 52.4ms (5/30). + + The gate is on the definition read having **settled**, not on the definition being truthy: + an adapter that exposes no `getObjectSchema`, and a read that throws, both settle with + nothing to report and the board falls through to an unexpanded query rather than waiting + forever. Boards fed rows by a parent (`data`, `bind`, inline `schema.data`) are untouched — + they never ran this effect, and they still read the definition for lane titles and labels. + + The `isOpaqueId` suppression in the card-description path is unchanged and keeps its + comment beside it: part of what it hid was this fetch ordering, but unexpanded rows still + reach it from parents that pass rows they fetched without `$expand`, from author-supplied + data, and from backends that decline an expansion. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- f08bcd9: `FieldEditWidget` now delivers the NON-DOM half of the contract it declares (objectui#7008). + + objectui#7009 made the factory forward its declared DOM pass-through block. The rest of + `FieldWidgetComponentProps` was still dropped: `error`, `onUploadingChange`, and the whole + "Host plumbing" block (`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`, + `emptyHint`, `onSelectRecord`, `onCreateNew`). A host could pass any of them with no type + error and the widget never received it — the "declared but not delivered" class this + package treats as first-class. + + `error` was the live one. `InlineFieldInput` has passed `error` into this factory since + PR #7109 and the factory dropped it, so an inline-edit control that had failed validation + never reported `aria-invalid`: a sighted user saw the red hint, a screen-reader user was + told nothing. The kanban `RequiredFieldsDialog` had the same hole from the other side — it + computes the validation state and could not hand it over — and now passes `error`, so its + controls are marked. Delivering `error` buys the a11y MARKING only; the message text stays + with the host, per the objectui#3222 contract. + + The keys travel through a new sibling executor, `toHostProps` (exported alongside + `toDomProps`), never through the DOM whitelist — none of them is DOM-legal, and routing a + `dataSource` adapter there is the `[object Object]` leak that whitelist exists to stop. + Three compile-time assertions make the two executors partition the contract, so a future + declared key cannot go undelivered silently. + + `dataSource` precedence is stated rather than left to emerge: a host's explicit + `dataSource` prop WINS over `SchemaRendererContext`. That is the order `LookupField` + already implements; the factory is a conduit and resolves nothing. A host that passes no + `dataSource` keeps reading the context exactly as before, so no in-repo host changes + behaviour. +- 7c3df8f: The settled-schema convergence, and the gantt's duplicate query gated + (objectui#7225, maintainer ruling B, 2026-09-02). + + `useSettledSchema` was extracted and published in PR #6690 with exactly **one** + non-test adopter (`ObjectTree`, the component that had an actual defect — + objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and + `ObjectCalendar` kept their own hand copies of the same shape, so a published + export was owed compatibility forever **and** the duplication it was named for + stayed. All three now call the hook. + + The migration is a pure deduplication with no behaviour delta — the hook was + extracted *from* these three shapes, so each becomes a one-line call. + `ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the + recipe the hook's own doc comment prescribes for it by name: pass the data + source as `undefined` for a render that must not read metadata + (`hasInlineData ? undefined : dataSource`), so "inline value data set" is + expressed as "there is no source to read from" rather than as a second enable + flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and + what made the calendar's obstacle a non-obstacle: it was about the gate half. + + **One observable change:** `ObjectKanban`'s rejected definition read now logs + on `console.error` with a `[useSettledSchema]` prefix instead of + `console.warn`. Its test spy moves with it, and now asserts on the channel + rather than merely silencing it. + + **The gantt's duplicate query is gated** (ask 2 of the card; #6482's + undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s + dependency list, so every load issued two unbounded queries — the first with no + `$expand` at all. Measured on this component across three latency profiles, the + cost is not the mild "round trip bought and thrown away": when the metadata + read is the slower of the two, which is the common case on a cold + `MetadataCache`, the user sees the full three-step paint — raw foreign-key ids, + back to the loading placeholder, then the expanded rows. It now issues one + query, already expanded. + + Gating the gantt required its schema resolution to settle on EVERY exit + (objectui#7232): the hand-rolled effect returned without settling on + `!effectiveDataSource`, on `!resource` and in its `catch` — harmless while + nothing waited on it, and a chart that never loads once something does. + `useSettledSchema` settles on all three by construction, which is what makes + the gate safe; both exits are pinned. + + ⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's + separate ruling, in its own commit on the same branch. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 2af1fa7: `KanbanSchema` / `KanbanColumn` / `KanbanCard` / `CardTemplate` / + `ColumnWidthConfig` are now the `@object-ui/types` declarations, re-exported + from this package rather than declared in it (objectui#7664, maintainer ruling + (a)). Nothing this package renders changed and every existing import keeps + resolving; what changed is that `safeValidateSchema` in `@object-ui/types` now + validates an authored `type: 'kanban'` document against this very shape, so a + board that validates is a board these renderers draw. + + **The shape is not member-for-member what this package declared — it is that + shape plus four members**, counted off `origin/main`'s + `plugin-kanban/src/types.ts` (19 members on `KanbanSchema`, 6 on + `KanbanColumn`, 7 on `KanbanCard`) against the `@object-ui/types` + declarations: + + - **`onCardClick` is DECLARED for the first time.** This package's dialect + never had the member, while `KanbanRenderer` has always forwarded + `onCardClick={schema.onCardClick}` — an undeclared read (objectui#7742). It + is declared here as a `#6124` RUNTIME SLOT: callable on the TypeScript face, + refused by name on the mirror, like the `onCardMove` and `onQuickAdd` beside + it in the same forward block. + - **Four `?: never` tombstones** carry the retired declarative face's keys + under the same `'kanban'` key so those spellings keep being refused by name: + `draggable`, `onColumnAdd` and `onCardAdd` on `KanbanSchema`, and `color` on + `KanbanColumn`. None of the four was ever a member of this package's dialect; + each is refused, not silently accepted, because the retired face taught it. + The full accept-set statement is on the sibling `@object-ui/types` entry. +- 2d80456: `object-kanban` / `view:kanban` and `object-calendar` / `view:calendar` now DECLARE seven + spec-carried keys their renderers already honoured, so the html tier stops reporting working + metadata as `unknown-prop` (objectui#8201, the backlog objectui#8176 exposed). + + Board: `groupBy`, `cardTitle`, `titleField`, `swimlaneField`, `coverImageField`. + Calendar: `defaultView` (the spec's three-member enum) and `locale`. + + No renderer behaviour changes — every one of these keys was already read and already acted on; + what changes is that authoring tools can now discover them and the save gate agrees with the + validator. Both tags of each block now spread ONE shared `inputs` list, so the two published + surfaces cannot drift apart by hand-copy. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [f66072d] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [1117414] +- Updated dependencies [6d63cd0] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [7c96c94] +- Updated dependencies [4da5109] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [63d54dd] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [b4393e5] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [d3005f7] +- Updated dependencies [1e7fe0a] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3777538] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [9409eb9] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [eeb6c2f] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [854cba3] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [6411def] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [b61d7d8] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [fe8f451] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [d06fba8] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [0b12a33] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [6c1b105] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [17fbbaf] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0d1e702] +- Updated dependencies [b03ba3a] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json index f22f7bff86..12c2dd0a95 100644 --- a/packages/plugin-kanban/package.json +++ b/packages/plugin-kanban/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-kanban", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Kanban board plugin for Object UI, powered by dnd-kit", diff --git a/packages/plugin-list/CHANGELOG.md b/packages/plugin-list/CHANGELOG.md index b539389c42..3b74b2bf9e 100644 --- a/packages/plugin-list/CHANGELOG.md +++ b/packages/plugin-list/CHANGELOG.md @@ -1,5 +1,1771 @@ # @object-ui/plugin-list +## 17.7.0 + +### Minor Changes + +- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). + + Seven modules resolved authored icon names into lucide's runtime `icons` record, each + with its own copy of the logic: **three different tokenisers** (`split('-')` on five of + them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` + rename on only **four** of the seven. The same authored name therefore rendered on one + surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened + with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, + and the other six call it. + + **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it + was measured rather than chosen.** Its regression set is empty three independent ways: + against the authored population, against a maximally-pessimistic every-authored-name x + every-surface cross-product, and against a bound-free differential over 8,298 spellings + derived from all 1,767 live record keys — each with a discrimination control that fired + in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface + pairs in that last reading, stripping two surfaces of every snake_case and + space-separated spelling they resolve today. + + **What changes for you — all of it widening, none of it removal.** No name that resolved + before stops resolving: no key of lucide's record contains `_`, whitespace or `-` + (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the + wider one produces the same key. Sixteen name-surface pairs start resolving where they + rendered a fallback or nothing before: + + - `layout_dashboard` and `building_2` (and every other snake_case or space-separated + spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, + `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and + the related list. + - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried + no rename map. `Home` is not a live record key, so this could only ever be a widening. + + **What does NOT change: what each surface draws when a name does not resolve.** The seam + answers `name -> component`, returning `null`, and decides nothing else (maintainer + ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at + the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning + (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, + `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` + and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and + then dropped once the tree was measured to have four such behaviours rather than two: a + lookup function is the wrong place to publish a presentation decision. + + `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface + this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: + its census goes from seven sites to one, and its normalisation stops being a + widest-common approximation of three disagreeing resolvers — so the under-reporting that + gate disclosed at objectui#5932 is closed rather than merely bounded. +- 4ca30d0: Two widget prop types anchor their `schema` to exported schema types that extend + `BaseSchema`, instead of hand-rolled inline literals with no `BaseSchema` in + their ancestry (objectui#6576, maintainer ruling 2026-08-31 option A; folds + objectui#6914). + + - `@object-ui/types` exports `ObjectGallerySchema` (`type: 'object-gallery'`) + and `ObjectDataTableSchema` (`type: 'object-data-table'`), each `extends + BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors + of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema` + declares the two keys the widget was reading behind casts — `drillDown` + (`DrillDownConfig`) and `onRowClick` — which no declaration carried before. + - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is + `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is + writable (`visibleWhen`, a real base member, was a compile error on the + literal) — and NARROWS in one place: `type` is now required and pinned to + `'object-gallery'`. `data` stays `Record<string, unknown>[]`. + - `@object-ui/plugin-dashboard`: `ObjectDataTableProps.schema` (not exported + from the plugin index) is `ObjectDataTableSchema`. The literal's own + `[key: string]: any` is gone, so a wrong-typed base member (`visible: 42`) and + a wrong-shaped `drillDown` are refused, and `type` is pinned to + `'object-data-table'` instead of bare `string`. + + Unchanged on both, stated plainly: an UNKNOWN key still compiles, because + `BaseSchema`'s index signature is inherited (objectui#5155, open). No runtime + behaviour changes; the widgets render exactly as before. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 2a7ac32: Calendar views no longer render on invented field names (objectui#7029; ruled on + objectstack#13748, director batch #19, option A). + + A view that carried no `calendar:` block used to have a complete-looking calendar + configuration synthesized for it. `ObjectCalendar` has always decided whether it + has a usable configuration by asking whether a start-date binding is PRESENT, so + the fabrication short-circuited its own refusal screen — "Calendar configuration + required. Please specify startDateField and titleField." — which existed all + along and was simply unreachable. Measured on a leave-request object whose real + fields are `start_date` / `end_date`: every record piled onto today's cell under + titles resolved through the display-name chain. A plausible, fully wrong screen, + with zero signal to the author. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/ObjectView` emitted `startDateField: 'due_date'` and + `titleField: 'name'` into `options.calendar` for every object view; + - `plugin-list/ListView`'s calendar branch floored the same two bindings at + `'start_date'` / `'end_date'` one layer down; + - `plugin-view/ObjectView.generateViewSchema` — the authored `object-view` + element route, which bypasses `ListView` entirely — carried its own copy. + + All three now forward only what the author declared. This converges the calendar + on the shape its siblings already had: `timelineViewOptions` (objectui#3129 + retired this very literal from the timeline axis), the kanban lane detector + (ADR-0085, "never invents a field the object doesn't have"), and + `defaultCalendarFromObject` (a binding, or nothing). + + **Behaviour change, loud over silent.** With no binding to forward, ADR-0047's + capability gate stops offering the Calendar toggle to views that configured + none, and a view forced onto the calendar renderer reaches the refusal screen + instead of a wrong one. A view that happened to sit on an object carrying a real + `due_date` field was rendering by luck; it now refuses until its `calendar:` + block is written. Correctly configured calendars are unaffected — same fields, + same render. The same deletion also stops the fabricated name from answering for + the Timeline switcher, which accepts a calendar binding as a legitimate axis. + + The spec half — cross-field validation rejecting a half-written declaration at + authoring time — is objectstack#13817. This half makes the runtime honest + independent of which spec version the host pins. +- 5f4514f: Gantt views no longer render on invented date field names (objectui#7070). + + The half PR #7062 fenced out and reported separately. A view that carried no + `gantt:` block used to have a complete-looking date axis synthesized for it: + all three faces floored `startDateField` at `'start_date'` and `endDateField` + at `'end_date'` — field names no view had written and most objects do not + carry. + + `ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props + are present, so the fabricated pair short-circuited the renderer's own refusal + screen — "Gantt configuration required. Please specify startDateField, + endDateField, and titleField." — which existed all along and was simply + unreachable from every route. The same fabrication answered ADR-0047's + capability gate in `ListView.availableViews`, so the Gantt toggle was live on + every object view in the product. + + ⚠️ The premise was MEASURED before anything was deleted, because #7029's + mechanic is only correct where a refusal path exists and that had never been + established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an + absent binding — it does not render empty, and it does not throw. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/src/views/ObjectView.tsx` — the console object page. The inline + branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and + `timelineViewOptions`: the declared block spread whole, title floored at + `'name'`, no date field invented. + - `plugin-list/src/ListView.tsx` — the render branch AND the capability gate. + - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored + `object-view` element route, which bypasses `ListView` entirely. + + **What changes for an author.** A view that declared no gantt configuration is + no longer offered the Gantt toggle, and one forced onto the renderer reaches + the refusal screen instead of a plausible, fully wrong chart. A view that + declared a binding is unaffected — the declared block is forwarded exactly as + before, every spec key included. + + Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx` + certified the gantt branch below it as already using the safe two-rung shape. + It did not. The note now states each sibling branch as measured, and says + explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the + two plugin faces, which objectui#7070 routes to a ruling rather than settling + per-face. + + Deliberately out of scope, and left in place: `progressField` / `dependenciesField` + (not date axes, different absent-value semantics) and the timeline `'created_at'` + posture conflict. +- 04a67b9: Retire the `'created_at'` timeline date-axis floors at both plugin faces + (objectui#7070 step ③, maintainer ruling 2026-09-01, 总监批 #28). + + **Breaking, deliberately.** A timeline view that declares **no** date axis anywhere no + longer renders. `ListView`'s and `ObjectView`'s timeline branches used to hand + `ObjectTimeline` a `startDateField` of `'created_at'` for such a view; both now forward + a declared axis or no key at all, and the renderer shows its "declare a date axis" + refusal instead. + + House posture, entered with the ruling: **日期轴永不虚构** — a date axis is never + fabricated. This is the third and last step of a sequence the ruling ordered and forbade + reordering: `ObjectTimeline` gained the refusal screen and lost its own internal + `|| 'date'` floor first (objectui#7459), which by its own measurement changed nothing a + user could see — precisely because these two faces still supplied a name. They are the + supply. + + The floor was not a harmless default. `'created_at'` is a column nearly every object + carries, so downstream it was indistinguishable from a real binding and could never + resolve to nothing — while the `$select` projection is collected from the **declared** + `timeline` / `options.timeline` blocks and never from this prop. An undeclared view was + therefore given a timeline bound to a column the query had not requested, and every + record bucketed into "No date": a screen that looks built, is wrong, and gives the + author no signal. The ruling also explicitly replaced the written decision that stood on + the deleted `ListView` line ("`created_at` stays the last resort for a view that + declares no date axis anywhere") — it was a second, de-facto contract held at one face, + on the very literal objectui#3129 had retired at the app-shell face. + + **Migration.** Declare the axis on the view: `timeline.startDateField` (spec-canonical), + `timeline.dateField` (legacy alias), or a `calendar.startDateField` — objectui#3129 + established that a calendar binding is a legitimate timeline axis, and it still is. All + three keep rendering exactly as before; only the *undeclared* case changes. A view that + really did want records laid out by creation time says so in one key: + `timeline: { startDateField: 'created_at' }`. The refusal names the accepted keys on + screen, so an affected view reports its own fix. + + `titleField` is unaffected and keeps its `'name'` floor at both faces — it is not a date + axis. So do gantt's `progressField` / `dependenciesField`, which the ruling scoped out + for separate evaluation. +- 78ca238: `DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s + load-failure panel is now rendered by the error state instead of the empty state + (objectui#7143; maintainer ruling 2026-09-01, director decision batch #27). + + `ListView` rendered its load FAILURE through `DataEmptyState` — the component named for + the *empty* case — passing it a destructive icon, error copy and a retry action, while + `DataErrorState`, in the same file and with the same layout, had no consumer anywhere in + the repo. objectui#7132 closed the accessibility half of that collision (the panel now + declares `role="alert"` over the empty state's `role="status"` default) and deliberately + left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a + props-surface question plus a visual change rather than a rename. + + **`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored + from `DataEmptyState` in the same file rather than spelled a second way: + + - `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle` + glyph the component has always drawn. + - `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely. + - `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than + merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??` + against its own default and this does the same, against + `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive + square `DataErrorState` already drew. + + Same names, same types, same default semantics as the empty state's; nothing existing on + `DataErrorState` changed, and a call site that passes none of the three renders exactly + what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the + ruling pins three props, and this component's retry affordance is already spelled + `onRetry` / `retryLabel` (plus `children` for a call site that needs its own control). + + One non-prop addition rides along, called out rather than folded in: the icon wrapper now + carries `data-slot="data-error-state-icon"`, mirroring the empty state's + `data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no + name — untestable and unstylable — and migrating a call site off `DataEmptyState` would + DROP that identifier rather than rename it. + + **`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call + site passes the same custom icon through the new `icon` prop, the same + `iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the + error state's spelling of `description`); its retry `<Button>` moves from `action` to + `children`, which renders at the identical position. `role="alert"`, the + `data-testid="list-error-state"` hook and `data-error-kind` are untouched. The whole + rendered delta is two attributes: + + - the panel root's `data-slot` becomes `data-error-state` (was `data-empty-state`); + - the icon wrapper's becomes `data-error-state-icon` (was `data-empty-state-icon`). + + Both are renames, not removals. Nothing in this repo styles or selects on either — no CSS + rule and no test read them — so a stylesheet in a host app targeting + `[data-slot="data-empty-state"]` to reach *this* panel is the only way to notice, and it + should be reading `data-error-state` now. Every class on every node, and the glyphs + themselves, are byte-identical: this is a visual no-op, deliberately, so the review the + ruling asks for has a small thing to look at rather than a redesign. +- d80447d: Stop inventing a gallery cover binding, and offer the view switcher only when it + can switch (objectui#7547). + + **The object page no longer floors `gallery.imageField` at `'image'`.** It used + to supply that key for every object view, declared or not. The screen damage was + small — `ObjectGallery` collapses the cover area when no record yields a cover — + but the key fed `ListView`'s ADR-0047 capability gate, which reads + `options.gallery.imageField`, so **Gallery was offered on every object view that + whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now + forwards the view's own declared block (both legacy cover spellings still + cross-fill each other, and `titleField` keeps its `'name'` display floor) and + emits no cover key when the view declared none. Same class and same route as + objectui#7029 (calendar) and objectui#7070 (gantt dates). + + ⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery + toggle. That is the ADR-0047 rule working: it was only ever offered because this + relay answered the gate on the author's behalf. + + **The visualization switcher is drawn for the resolved list, not the whitelist.** + `showViewSwitcher` was computed from the LENGTH of + `appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects + it with the capability gate — so a view whitelisting `['grid', 'timeline']` with + no timeline block drew switcher chrome around a single Grid entry. The predicate + now lives at the one site that holds both halves. Views whose whitelisted types + all resolve are unaffected. +- 6712930: The two kanban adapters stop writing the retired `groupField` onto the + `object-kanban` node they generate (objectui#7773). `groupBy` — the key the + renderer actually reads — is unchanged and is now the only lane key emitted. + + **What was measured, on this branch's base (`a915064e`).** Both adapters emitted + the key twice: + + ``` + packages/plugin-view/src/ObjectView.tsx:1362 groupField: groupBy, + packages/plugin-list/src/ListView.tsx:2500 groupField: laneField, + ``` + + The `object-kanban` renderer never read it: `groupField` has ZERO hits anywhere + under `packages/plugin-kanban/`, against a control of thirteen `schema.groupBy` + read sites in `ObjectKanban.tsx` from the same query — so the zero is a reading, + not a blind grep. The write was inert; the board grouped by `groupBy` and + `groupField` rode along unread. + + **Why it is removed rather than tolerated.** objectui#7322 RETIRED + `groupField` on this node on both published faces — `groupField?: never` on the + TypeScript interface and a `retirementTombstone()` in the Zod mirror, which + refuses an authored value BY NAME. So the adapters were producers emitting a + node their own published contract rejects. That was harmless only because a + generated node never reaches the mirror at runtime (`SchemaRenderer` runs the + structural `validateSchema`, not `safeValidateSchema`) — but the CLI's + `os check` / `os validate` DO run the mirror, so the identical node was already + refused when authored by hand and admitted when generated. This closes that + split. + + **What changes for a consumer.** Nothing on any documented path: the renderer's + behaviour is byte-identical, because it never read the key. A host that + registers its own `object-kanban` component and reads `props.schema.groupField` + off the generated node now reads `undefined` — read `groupBy` instead, which + carries the same value and always did. Graded `minor` rather than `patch` for + exactly that narrowing, following the repo convention that objectui's own + breaking changes ship as `minor` (AGENTS.md 版本号策略, mechanically enforced by + `scripts/check-changeset-no-major.mjs`). + + **Who is NOT affected — the boundary is node-local.** Every VIEW-LEVEL + `groupField` read is untouched and still live: it is a legacy alias of the + spec's `groupByField` on the kanban *view config*, mapped by + `normalize-list-view.ts`, and both adapters still resolve lanes through it + (`ObjectView.tsx`'s `kanbanCfg.groupField ||`, `ListView.tsx`'s + `groupByField || groupField`). Authoring `options.kanban.groupField` on a + `list-view` or `object-view` keeps working exactly as documented in + `packages/plugin-list/README.md`. `groupField` is dead only on the generated + `object-kanban` NODE. + + The two tests that pinned the duplicate write are TURNED, not deleted — they now + assert the key is absent, so restoring the write reddens them instead of being + silently re-blessed by a missing assertion. +- 31a0d05: Emit the spec-canonical kanban lane key from the object page, and teach the + capability gate to recognize it (objectui#8193). + + `ObjectView` built the view-level kanban config it hands to `list-view` and wrote + the deprecated alias `groupField` — never `groupByField`, the key + `@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it + already READ the canonical key first. Its sibling producer in the same package, + `defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and + left the reasoning next to itself ("that read-site now prefers the spec key, so + one key is enough"); the twin was not carried along, so one producer surface + spoke two vocabularies for one concept depending on which entry point you + arrived through. `ObjectView` writes into `options.kanban`, which + `normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing + corrected it downstream and the legacy spelling was what drove the lanes. + + The expression is now the exported `kanbanViewOptions`, the fifth member of the + `timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` / + `galleryViewOptions` family it had been the odd one out of. + + **`ListView`'s kanban capability gate changed with it, and had to.** The gate + consulted the `options.kanban` bag for the LEGACY spelling only, so a producer + writing the spec key into the bag was invisible to it while still rendering + correctly — the render branch merges the bag and resolves + `groupByField || groupField`, so the gate recognized strictly less than what + renders. Measured before and after: a bag of `{groupBy, groupField}` offered + Kanban and `{groupBy, groupByField}` did not, so shipping the producer change + alone would have removed the Kanban toggle from every object view. The gate now + reads both spellings out of the bag — the same one-question-two-sites repair + objectui#5042 made for `map` and objectui#7544 for `chart`. + + **No alias READ was removed, and the alias is not retired.** Stored metadata + still authors `groupField`, and every read site still resolves it; `ListView` + already preferring the canonical key is precisely why the sibling producer could + drop the alias write. What changed is only what this one face WRITES, plus one + added rung on the gate. + + **Migration.** Nothing authored has to change. If you read + `options.kanban.groupField` off the schema `ObjectView` produces, read + `groupByField` (or both) instead — that bag now carries the spec spelling. + + The view-level `groupBy` in the same bag is untouched. It is not a spec key + either — measured against the strict `KanbanConfigSchema`, which refuses it by + name — but `ListView`'s projection collectors read it, so retiring it needs its + own producer census and is filed as objectui#8213. +- e2e8e68: **Behaviour change:** the spec's view-level `map` block on a list view is now read at + runtime. `ListMapConfigSchema` (objectstack#9340) has been authorable and validated since + the `@objectstack/spec` 17.1.0 pin — it flows into this repo's own `ListViewSchema` by + reference — but nothing consumed it: `ListView`'s `case 'map'` forwarded only the legacy + `schema.options.map` bag, so declaring `map: { titleField: 'title', locationField: + 'location' }` on a view changed nothing and marker titles fell back to the renderer's + placeholder. + + The block now reaches `plugin-map` and drives every one of its seven reads — coordinate + extraction, marker title and description, and the initial camera. Precedence follows the + convention the sibling visualization blocks in the same file already set: the view-level + block wins over `options.map`, per key, exactly as `kanban` / `calendar` / `gallery` / + `timeline` / `gantt` each merge their spec config over the legacy bag. Both sources go + through the existing objectui#5177 key whitelist, and the branch still emits the flat + form, so `getMapConfig`'s objectui#5018 precedence rule ("neither flattener emits a `map` + key at all") stays true. + + The visualization switcher had the same gap with a sharper consequence: the capability + gate that decides which visualizations are offered also read `options.map` alone, so a + view binding its coordinates in the spec block was filtered out of its own + `appearance.allowedVisualizations` and fell back to `['grid']`. The gate now asks the same + merged config the render seam forwards, so the two cannot disagree — including for a + binding split across the two sources. + + `InterfaceListPage` (ADR-0047 interface pages) forwards the referenced view's `map` block + for the same reason. It is passed alongside the auto-derived `options.map` rather than + replacing it, so a partial authored block — `map: { titleField: 'title' }` — keeps the + derived coordinate binding instead of dropping it. + + No defaults are introduced for `zoom` / `center`: an undeclared camera stays undeclared, + so the fit-to-queried-records behaviour ruled in objectui#5000 is unchanged. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 9801765: The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen` + per selected record (objectui#4420). It used to read that key as a bare boolean — the + object-level verdict only — so ticking a record the author's predicate excludes still + offered the red Delete, and pressing it deleted the record the predicate was written to + protect. The row kebab on the same screen hid its Delete correctly, so one declared key + meant two different things on two surfaces. + + Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the + operation and report the skipped**. The bar evaluates the predicate once per selected + record, the delete runs over the allowed subset, and the excluded records are reported + rather than silently dropped. The button itself is never hidden or disabled by the + predicate — a mixed selection is not punished for one stray tick — and a selection where + every row is excluded is a legible refusal rather than an unexplained absence. + + - `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of + `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once. + A bulk gate evaluates N records in a loop, which is why it can never be a hook. + - `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through + `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped + count; a selection with nothing excluded keeps the consumer's own delete flow + untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` — + the bulk half of the same predicates, gated on the object verdict rather than on the + row `onDelete` wiring. The dialog declines to run over zero records. + - `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters + the built-in `delete` to the eligible subset and states the skipped count inline. + + Custom bulk action ids are untouched: they route through the action runner carrying + their own gates. This is a UI affordance — server enforcement was never the leak. +- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's + ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a + list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user + dragging a column or cycling density was re-styling that view for the entire organization. Nothing + in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, + v18) and is deliberately not built here — which is precisely why the write has to be gated rather + than narrowed: there is no second, private store for it to fall back to. + + `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability + set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` + (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the + `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — + before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. + It is on the write rather than on the toolbar button on purpose: withholding the affordance would + leave the method still accepting the call from anything else holding the adapter, whereas a gate on + the write is inherited by every caller, present and future. + + `manage_metadata` is not a newly minted name. It is the capability this repo already treats as + metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself + refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very + same ADR-0005 metadata door, so this applies the authority the server is already applying instead of + inventing a parallel one. + + **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating + ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a + client-side refusal on missing data cannot protect anything and can only break a permitted user. A + *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new + `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. + + The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure + into `console.error`, which for a debounced toggle whose UI has already moved would have left the + operator looking at a density they did not get; a denied write now raises a toast. And the "View + settings" popover — where density and field visibility are actually changed — now states the scope + before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who + uses this view."*, translated in all ten packs. +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- b4393e5: The last three sort-axis consumers read the platform's per-column sortability signal instead + of re-deriving it from the field's type (objectui#6108, inheriting objectstack#10235 ruling A + through objectui#5729's landed contract). ListView's toolbar sort picker and both of + RelatedList's sort entry points — the embedded table's column headers and the `data-list` + sort-button row — now go through `isPlatformSortableField`, the same spelling the grid header + adopted; their `UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType` re-derivations are + deleted. + + The re-derivation was not wrong about `formula`: the platform computes its own projection from + the same `@objectstack/spec` storage fact, which is why the drift went unnoticed across two + cards. It parts company on everything the projection encodes as ABSENCE — an unknown name, a + dotted path a caller can put in a related list's `columns`, an unprovisioned audit column — + where a type read finds no field definition, answers "sortable", and offers a control the + runtime meets with `400 INVALID_SORT`. It parts company again on any refusal that carries no + `reason: virtual-type`, and it cannot follow the platform in the other direction either: a + field the platform now DOES order by stays withheld forever on its type alone. + + Two behaviours are deliberately unchanged. The relational carve-out stays separate from the + signal — the projection answers `sortable: true` for a `lookup` because the platform can order + by the stored foreign key, while the UI withholds because that order means nothing beside a + column of names — so a relational column does not get its sort back. And ListView's picker + still lists a field the CURRENT sort already names, which is the only way to remove a sort the + server refuses outright; that exception now covers platform-refused fields, not just formulas. + + A deployment that served no `sortability` key at all is a different case from "nothing is + sortable": that branch keeps the type read as a compatibility floor, so behaviour on a backend + older than objectstack#10235 (or an inline/mock data source) is byte-identical to before. +- bd2f56a: `ListView`'s toolbar sort picker no longer persists a sort the platform refuses to order by. + + The picker keeps a platform-refused field listed while the CURRENT sort names it + (#6108). That exception is deliberate and stays: it is the only way a user can + REMOVE a sort the server answers `400 INVALID_SORT` for — withholding the option + unconditionally renders a blank row nobody can delete, and drops the sort silently + on the next edit. + + What was wrong is that the picker rendered and emitted from the same array. Editing + anything ELSE in that popover — adding a second sort key, resetting to the view's + default — re-emitted the whole array with the refused entry still in it, and the + host's `onSortChange` turned that into `persistViewPatch({ sort })`: a + personalization PUT storing a refused column, written by a user who never touched + that row. A view stored before the sortability signal existed therefore kept + re-persisting its refused `$orderby` indefinitely. + + Every `onSortChange` this component emits — the builder, the column-header sort and + "reset to default" — now crosses one boundary that drops what the served projection + refuses, while `currentSort` keeps the array whole. So what the picker LISTS and + what it PERSISTS are separate: the refused entry stays visible and removable, + removing it persists the removal, and no write carries it. This is the separation + #5729 already made at the grid seam (`ObjectGrid`'s `manualSort` / + `manualOnSortChange` pair); the picker was the second door onto the same stored + view state. + + Only under a served sortability projection (objectstack#10235 ruling A). `undefined` + means NO SIGNAL SERVED — an older deployment, an inline/mock data source — not + "nothing is sortable", and that branch is byte-identical in behaviour to before. +- babe956: `list-view` stops spelling "the author declared no columns" as an explicit empty + projection (objectui#6598). + + A production `kind:'html'` page carried `<list-view objectName="opportunity">` + with no `columns` and rendered the row count, the filter/group/sort toolbar and + the index column — and **not one data column**, with no diagnostic anywhere. + `ObjectGrid` derives default columns for exactly that case ("Default columns + priority (when schema doesn't specify columns)"), and it never ran: the + derivation is gated on `schema.fields` being ABSENT, `ListView` sent + `fields: []`, and an empty array is truthy. `normalizeColumns` had already read + the empty `columns` as unauthored, so the two keys disagreed about the same fact + and the stricter reading won. + + `ListView` now asks whether the AUTHOR declared a projection — `columns` present + and non-empty, after the legacy `fields` fold — and hands the child grid nothing + at all when they did not, so the grid's own defaults apply. + + ⚠️ The predicate reads the authored value and never what survived filtering, and + that distinction is load-bearing: when the author DID declare columns and the + field gate removed every one of them, the empty projection is still sent. + `ObjectGrid` re-applies FLS on its derived column path only, never on the + explicit-columns path, so falling through to the derivation there would put + fields on screen that the author never asked for and the principal may not read. + + Measured single-variable on the html tier: a bare + `<object-grid objectName="opportunity" />` renders the object's default columns; + the same object behind `<list-view>` rendered none. Pinned at the handoff + (`ListView.unauthoredColumnProjection-6598.test.tsx`) and end to end over the + real grid on a real html-kind page + (`htmlTierListViewDefaultColumns-6598.test.tsx`). + + This is one half of the reported symptom. Which columns the defaults resolve to + still depends on who owns the fetch — with a host like `ListView` fetching, the + grid takes its inline-data branch and derives from the row payload's keys rather + than from the object schema's policy (hidden and readonly system-managed fields + dropped, `highlightFields` honoured). That precedence sits in + `packages/plugin-grid` and is filed separately. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 06b8c42: Re-key three more renderer effects onto the primitives they actually read, + instead of the memoised object identity that produced them (objectui#6697 — + the three census members from objectui#6592 that sit outside its + `getDataConfig(schema)` family): + + - `RelatedList`'s collection fetch now depends on `defaultSortKey` / + `filterKey` (the `JSON.stringify`-derived content strings the two memos are + already keyed on) rather than on `defaultSortSpec` / `listFilterNode`. + - `page:tabs`' related-count probe now depends on a serialised `probeKey` + rather than on the `probeTargets` `Map`. + - `ListView`'s data fetch now depends on the `expandFields` memo's own INPUTS + — `schema.columns`, the alternate views' binding blocks and + `objectDef?.fields`, all props and state a discard cannot move — rather than + on the `expandFields` array the memo returns. + + `useMemo` carries no semantic guarantee — React is permitted to discard a memo + cache and recompute even when its dependency array compares equal to the + previous render — and all three factories return a FRESH value on every call + (`normalizeSortSpec`/`toFilterNode` build a new array / a freshly lowered AST, + the probe factory builds a new `Map`, `buildExpandFields` returns a new array + in every branch). So each effect re-ran on a discard alone, with nothing an + author or a caller controls having changed: an extra `dataSource.find` for the + related collection, an extra `dataSource.find` for the list window, and a + redundant re-probe of every tab's count. Keying on the primitives makes a + cache discard a no-op and returns `useMemo` to being a pure optimisation. + + Severity is low and the fix is deliberately narrow: the observable was a + redundant round trip, never incorrect data, so only the re-run condition + moves — each effect body still reads the memoised value, and a genuine change + still refetches exactly as before. + + The three take two routes on purpose — key on the nearest DISCARD-IMMUNE + thing. `RelatedList`'s memos are keyed on exactly one primitive each, and + `page:tabs`' probe memo is keyed on another MEMO's output (`items`), which is + not discard-immune, so both take a content string. `ListView`'s memo is keyed + on props and state, so it names those directly: a value key over + `expandFields` would NOT have been content-equivalent there — `buildExpandFields` + collapses the collected set down to the relation roots, while the effect body + also builds `$select` from `schema.columns` and the view bindings — and it + would have defeated objectui#4567's live-dependency pin, which ruled that + "ListView's by-identity dependency is correct for a real column change" and + put the identity stabilisation at the PRODUCER. + + One correction to the census card's account, measured while pinning it: for + `page:tabs` the redundant probe costs nothing on the wire. + `RelatedCountStore.fetch` returns the cached count as its first act and dedupes + concurrent probes, so the extra work is the effect re-running, not an extra + request. +- 4dfdcc3: `ListView` reads `exportOptions.streaming` without a cast (objectui#6956). The + two `as any` reads — the `exportableFormats` server-availability check and + `handleExport`'s server-eligibility gate — and the `'pdf'` in the bare-array + fold's cast are gone: the `ListViewSchema` type now carries `streaming` and not + `'pdf'`, because `@object-ui/types`' zod mirror binds the spec's `exportOptions` + field by reference. No behaviour change: the same formats are offered, + `streaming: false` still forces the client-side path, and the bare-array fold + (`resolvedExportOptions`, a stored `['csv', 'xlsx']` folded to `{ formats }`) + STAYS — nothing on the render path parses and `ObjectView` forwards a stored + value verbatim, so the spec's parse-time lift never runs before this renderer + and the fold is load-bearing rather than legacy. A `'pdf'` stored before the + retirement still arrives as data and is still dropped from the export menu with + the existing one-time warning. +- d1842ab: `DataEmptyState` now declares `role="status"` by default, so an empty result is + distinguishable from a failed one on every surface that renders it + (objectui#7132). + + This is the convergence half of the two rulings that landed as objectui#7063 and + objectui#7064, both resting on objectstack#13848: uniform behaviour belongs to + the platform, and per-surface compensation is the per-app tax being ruled + against. Those two fixed their own surfaces deliberately and locally; this card + measured whether the shared primitive should carry the property. It did not. + + **Measured, not assumed.** All the surfaces were rendered and their empty boxes + read directly: + + | surface | `role` before | + |---|---| + | `DataEmptyState` bare default | *none* | + | `plugin-list` empty list | *none* | + | `plugin-list` load-error panel | *none* | + | `plugin-detail` activity timelines | *none* | + | `ui:empty` schema renderer | *none* | + | `plugin-dashboard` `WidgetEmptyState` (#7063) | `status`, typed at the call site | + | `plugin-kanban` empty board | `status`, typed at the call site | + + The sibling states in the same file had always declared themselves — + `DataLoadingState` is `role="status"`, `DataErrorState` is `role="alert"` — and + the empty state alone declared nothing. So the surfaces were not legitimately + differing: the ones that wanted the property had each hand-typed the same line, + and the ones that had not yet done so were silently missing it. That is one + platform default, copied by hand, at package level. + + **It is a default, not a fixed attribute** — `role` is spread from props, so a + call site keeps the last word. That is what makes this inert for the two ruled + surfaces: both already pass `role="status"` explicitly and receive the identical + attribute with or without it. Neither surface's behaviour changes. + + **One real defect fell out of the measurement.** `plugin-list` renders its load + FAILURE through `DataEmptyState`, borrowing it for layout — so a 403 saying "You + don't have access" and a young object saying "Nothing here yet" were the same + node shape, with no role on either. That panel now declares `role="alert"`, + which both fixes the pre-existing indistinguishability and stops the new default + from announcing an outage as a routine status. + + Metric/KPI widgets are untouched: their carve-out (`rows.length === 0 && + !isMetric`) gates whether an empty state is rendered *at all*, upstream of this + component, so a KPI still reads `0` rather than "no data". +- a6d8b8d: Fix: a grid grouped by a field it does not also show as a column no longer collapses + every row into one `(empty)` group (objectui#7179). + + `$select` was built from the view's `columns` and nothing else, so a view declaring + `grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns + never asked the server for that field. It was `undefined` on every row by the time + grouping ran, and the grouping label builder — correctly, for a genuinely empty value — + answered `(empty)` for all of them. The result was one collapsible group holding every + record, with no error, no warning and no empty state: a grid that looked like it grouped + and did not, reading as "these records have no value for this field". + + The grouping fields are now unioned into the projection, at both places it is built — + `ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the + rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that + fetches a bare foreign key without populating it buckets by raw id instead of by name, + which is a different wrong answer rather than a fix. + + Authors do not need to mirror a grouping field in `columns` any more. That was never + required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of + it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their + `groupByField` with no column needed. Refusing the configuration at author time was + considered and rejected: it would make the grid the odd one out and reject working + intent that the schema explicitly allows. + + The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]` + entry carries a bare string that has never been through column validation, and some + backends answer an unknown `$select` key with an empty result set rather than ignoring + it. Unioned unguarded, a grouping field naming something the object does not declare + would have turned this bug into a strictly worse one — no rows at all, equally silently. + Grouping fields are therefore intersected with the object's declared fields and passed + through the same field-level-security gate as columns and predicate operands before they + reach the query. +- f626808: fix(app-shell,plugin-list): a list view's own `description` now reaches the screen + + A `description` authored on a per-list-view entry (`listViews.<viewName>.description`) + was validated, built and served correctly, then silently never rendered. Two + independent cuts, both fixed here: + + - **app-shell** — `ObjectView`'s `renderListView` relay copied ~46 keys off the + active view onto the schema it hands `ListView` (`label`, `sort`, `filter`, + `hiddenFields`, `inlineEdit`, `color`, `allowExport`, …) but had no rung for + `description`, so the renderer could only ever see the object-level list's + description and a per-view one was unreachable. It is relayed now, with the + same two-rung shape as `label`. This is *not* the object's own + `objectDef.description`, which stays the page header's subtitle. + - **plugin-list** — `ListView` rendered `typeof description === 'string' ? … : ''`, + a type test rather than a resolution. `ListViewSchema.description` is + `I18nLabel`, so an inline locale map (`{ en, 'zh-CN' }`) — metadata the spec + entitles an author to write — rendered a blank strip in every locale. It now + resolves through the same shared helper the sibling `label` uses, and the + visibility guard reads the resolved text, so a map with no usable entry drops + the strip instead of reserving empty space for it. + + `appearance.showDescription: false` still suppresses the description in both arms. +- 5015fcf: A gantt list view no longer shows the record-count bar, because the bar describes a + request that view does not draw (objectui#7210, half 1). + + `ListView` renders one record-count bar for every `viewType`: the row count, the + "Showing first N records. More data may be available." warning that goes with its own + `$top: pageSize` query, and a rows-per-page selector that re-issues that query. On grid, + kanban, calendar, gallery, timeline and map the bar is accurate — those renderers draw + the `data` `ListView` hands down. + + On `gantt` it is not. Measured, not inferred: the registered `object-gantt` renderer + forwards no prop but `schema`, so the `data` prop never reaches the chart and + `ObjectGantt` issues its own query — one that carries no `$top` at all. In a harness of + 18 rows with `pagination.pageSize: 6`, the chart drew all 18 while the bar under it read + "6 records · Showing first 6 records. More data may be available." Because it is the + only paging disclosure on the screen, a reader takes it as describing the chart; that + reading already produced a wrong finding in an application repo, which cost a browser + session to disprove. Authoring `pagination.pageSize` could not have fixed it either — + the chart's request never carried a page size to begin with. + + Scoped to `gantt` alone. Every other surface keeps its bar unchanged, warning included; + a kanban over the same result set still reports its page honestly. + + Not changed here, deliberately: the gantt's query still has no ceiling, and `ListView` + still issues its own paged query for that view (its rows still drive the loading + skeleton and load-error panel, `UserFilters`' option counts, and the client-side CSV / + JSON export). Capping a non-grid view's fetch is an open maintainer decision — adding + one would turn a complete schedule into a quietly truncated one — and this correction is + right whichever way that lands. +- 67dadd6: FLS-gate the `$expand` projection at both build sites (objectui#7215). + + objectui#6898 closed field-level security on `$select`. `$expand` was left ungated at + both projection sites — `ObjectGrid`'s own fetch and `ListView`'s `expandFields` memo — + so a `lookup` / `master_detail` / `user` / `tree` field the current principal cannot + read was still handed to the server for expansion. `$select` on a denied lookup asks for + its bare foreign key; `$expand` on the same field asks the server to resolve it and + return the related record, so the larger of the two disclosures was the ungated one. + + **Reproduced before it was fixed**, as failing tests at both sites, and the same leak + reaches further on the `ListView` path: that builder's `$select` gate drops the denied + column and then adds the expand roots back unconditionally, so the denied field walked + back into `$select` as well. Gating the expansion closes both halves. + + **Grading, measured rather than assumed.** Against ObjectStack's own server this is + defence-in-depth, exactly as objectui#6898 is: `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row, and + objectql's expand path writes the resolved record back under that same key, so one + statement removes the expanded object and the bare id alike; the expansion sub-read is + itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS + treatment). It is load-bearing for any backend that does not strip. + + **Nothing a permitted view did stops working.** The gate judges the OUTPUT of + `buildExpandFields`, which is already a subset of the object's declared + reference-bearing fields, so the "`checkField` answers false for an undeclared key" + trap cannot be reached and derived / host-joined columns are untouched. An unanswered + permission policy filters nothing. `buildExpandFields` itself is unchanged. +- ac257b3: FLS-gate the speculative half of `ListView`'s `$select` projection (objectui#7216). + + `ListView` builds `$select` from two populations and, until now, asked them different + questions. The user-declared `columns` were passed through `perms.checkField(...)` by + objectui#6898. Everything the builder adds ON TOP — the kanban / gantt / timeline / + calendar / gallery field bindings, the timeline's auto-added `status` / `priority` + badge fields, and the operands harvested from row-action and conditional-formatting + predicates (objectui#3501) — went through `addSpeculative`, which intersected them + against the object's declared fields and then added them unconditionally. + + The two gates it needed answer unrelated questions and neither substitutes for the + other. The known-field gate keeps an **unknown** key out, because some backends answer + an unknown `$select` key with an empty result set rather than ignoring it. The FLS gate + keeps a **known but denied** key out, because sending it leaks the value at the server + boundary even though the UI hides it. A field can be perfectly well-declared and still + denied, and that was the case this path did not handle: a kanban grouped by a denied + field, or a gantt bound to a denied date, still named it in the request. + + **Reproduced before it was fixed.** Eight of fourteen new pins fail on the unmodified + tree, each reaching the denied field only through a view binding — a denied *column* has + been dropped since objectui#6898, so a pin naming one would have proved nothing. + + The gate goes inside `addSpeculative` rather than at the five call sites, so every route + into the speculative union is covered at once; gating call sites one at a time is how + the asymmetry arose. It runs **after** the known-field intersection, the ordering + objectui#7179 established: `checkField` answers false for an undeclared key, so asking + it first would drop derived and computed bindings — and would be the reason they were + dropped. The platform record columns (`created_at`, `owner_id`, the audit FKs) are + carved out for the reason they already are elsewhere in this builder: every object + carries them and none declares them, so no field policy mentions them and an FLS answer + about them is always false. Without the carve-out a calendar bound to `created_at` would + go blank for everybody. + + **Nothing a permitted view did stops working.** A permitted binding is still projected, + an unanswered permission policy filters nothing, and the projection is rebuilt when the + policy answers — pinned, because a gate that only runs before `/me/permissions` resolves + is a dead gate that passes almost every test written for it. + + objectui#7179's `addGroupingField` wrapper is removed: its predicate was identical to + the one now inside `addSpeculative`, and two spellings of one gate is the shape that + lets them drift. That path keeps its behaviour and gains the FLS pin it never had. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 245c7b7: The list view's capability gate now resolves `chart` (objectui#7544). + + A `grid` list view that declared a complete `chart:` block and whitelisted + `appearance.allowedVisualizations: ['grid', 'chart']` was never offered the Chart + toggle. `availableViews` builds the resolvable set from each visualization's binding + and intersects it with the whitelist (ADR-0047, whitelist ∩ resolvable); seven + visualizations had a capability check there — kanban, gallery, calendar, timeline, + gantt, map, tree — and `chart` had none, so the author's own whitelist was filtered + down to nothing and the view fell back to `['grid']`. `chart` entered the offered set + only through the "always allow switching back to the schema's own viewType" leg, i.e. + only when the view was already `viewType: 'chart'`. + + Both halves were spec-legal and authorable the whole time: `chart` is a + `VisualizationTypeSchema` switcher target and `chart:` is a list-view key + (`ListChartConfigSchema`). Only the gate never asked. This is `map`'s objectui#5042 one + visualization over, and is fixed the same way. + + **What now resolves.** A chart block that binds to names the author wrote: the ADR-0021 + shape (`dataset` plus at least one measure in `values`), or the legacy inline shape (a + declared category — `xAxisField` / `categoryField` — together with a declared measure — + `yAxisFields[0]` / `valueField`), in the view-level `chart` block or the legacy + `options.chart` bag. A block that declares no binding stays unoffered: reaching the + renderer with nothing declared lands on the legacy branch's invented `'name'` / + `'value'` floor, and the switcher must not offer a route into it. That floor itself is + unchanged here (objectui#7547). + + The gate and the `case 'chart'` render branch now read ONE resolver + (`resolveListChartBinding`) rather than two copies of the condition, so they cannot + drift about what a usable chart block is. `schema.chart` also joins the memo's + dependency array, so a block that arrives on a later render is seen. +- 5a211b2: `ObjectGallery` waits for the object definition instead of querying twice + (objectui#7903). + + It sat outside the set objectui#6482 converged on the shared settled-schema gate + — `ObjectKanban`, `ObjectView`, `ObjectCalendar` and `ObjectTree` were named + there, `ObjectGantt` was ask 2 of objectui#7225 and `ObjectTimeline` was + objectui#7895 — and nothing marked it a deliberate exclusion. It still held the + object definition in a local `useState` fed by its own metadata effect, and + listed that definition in the record-fetch effect's dependency array. + + **User-visible.** Every object-bound gallery load issued **two** `find` calls + instead of one: the first before the definition landed, with `buildExpandFields` + seeing no fields and therefore carrying no `$expand` at all, and a second one + after. The first paint was therefore a grid of cards rendered from raw + foreign-key ids. After this change the gallery paints once, from a query that + already carries its expansion. + + Measured on the component with an instrumented renderer, one mount per hold, + `getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with + `ObjectCalendar` as a positive control in the same run: before, 2 `find` calls + with expand sets `[null, ['owner']]` at every hold, the issue order always + `schema:issued, find(unexpanded), schema:settled, find(expanded)`, two distinct + painted states, 3 late writes into the card grid after the first paint, and a + first-paint time flat at 3-7 ms across the whole sweep; after, 1 `find` carrying + `['owner']`, one painted state, 0 late writes, and a first paint that tracks the + hold (9 ms at +3, 15 ms at +10, 35 ms at +25, 106 ms at +100). The control read + 1 `find` carrying `['owner']` and a hold-tracking first paint both before and + after. + + The cost this component was paying is a **two**-step paint, not the three-step + one `ObjectCalendar` and `ObjectTimeline` each measured: those make `loading` an + unconditional early return, so their re-run drops back to a placeholder in + between, while this component's early return is `loading && !items.length` — the + raw ids were replaced in place. Measured here rather than inherited from the + matching shape, which is objectui#6482's own per-component standard. + + The resolution half is now `useSettledSchema` from `@object-ui/react`, which + settles on **every** exit — no source, no `getObjectSchema`, no object name, and + a read that threw alike. That is what makes the gate safe: the replaced effect + returned without settling on all four, which cost nothing while nothing waited on + it and would have held a gated query open forever. Pinned by + `ObjectGallery.fetchGate-7903.test.tsx`, including a gallery whose adapter + exposes no `getObjectSchema` and one whose definition read rejects — both still + query, unexpanded. + + Two departures, each judged for this component rather than copied. Like + `ObjectTimeline` and unlike `ObjectCalendar` / `ObjectGantt`, the metadata read + is **not** disabled for a gallery whose records were authored inline: this + component reads the definition on every path, for cell semantics and for ADR-0079 + card titles, not only to expand a query. And the gate window now holds the + loading placeholder rather than showing "No items to display", which the two + siblings get from their initial `loading` state and this one did not. +- 15236e0: `list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. + + `userActions` names two different blocks. On a **view** it is toolbar policy — + the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, + `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by + name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / + `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is + the only shape `listViewPredicates` can read, since its loop skips every + non-object value. + + `ListView` read the key view-first when building the `$select` projection + (`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view + carrying a perfectly legal toolbar block therefore shadowed the object's CRUD + predicates, the harvest found none, and the predicate's operand left the + projection. CEL then faults on the absent key, fails closed, and the row + Edit/Delete button disappears for everyone with nothing pointing at the + projection — objectui#3501's failure, reached with a success receipt at every + step. + + This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and + it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level + `userActions` it hands down as an object literal of two spreads, so the left + operand was `{}` at worst — never nullish. The `??` never fell through, and the + object's CRUD predicates were never consumed at all on that path, whether or + not an author wrote any toolbar policy. + + The harvest now reads the object block only. Both `userActions` read sites in + `ListView.tsx` carry a comment naming the collision, and + `__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the + two shapes, a producer that manufactures the view one, the harvest's blindness + to it, and the projection that must keep the object's operand with a toolbar + block — or an empty block — present on the view. + + Toolbar policy itself is untouched — it was never read through this path. +- ec9fdaa: `ListView` now resolves the nested `aria.ariaLabel` against the audience's locale + instead of casting it to a string (objectui#5134). + + `@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain + string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only + read site in this repo spread it into the DOM as + `{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a + conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which + a screen reader announces as the list view's accessible name — in every locale. The + read now goes through the spec's own `resolveI18nLabel` (the resolver four other + in-repo read sites already use) against `useDisplayLocale()`. + + Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a + plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label + predates rc.6, and no measured author writes one today — but map values are legitimate + and arrive via API/import, so an imported list view carrying + `aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong + accessible name. This is the map form working as declared, not a defect users are + currently hitting. + + Behaviour on the string arm is byte-identical, including `''` (falsy before and after, + so no attribute). One edge changes for the better: a map that matches no locale used to + render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an + unnamed region beats a garbage-named one. + + The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different + vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by + `SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's + shape. +- d6613a2: Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime + behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does + change, which is why this is a `patch` rather than an empty frontmatter. + + **objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment + block above the `sortFields` memo still called a formula field "the supported + alternative (… which sorts like any text column)". Since objectui#4294 the + `list.sortRelationalHint` string in this same file says the opposite ("Not a formula + field: it is virtual, so no column is stored for it and the server refuses to sort by + one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the + server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical + now names the remedy the hint, the server's refusal and the README already share — a + stored field that denormalizes the name onto this object, written when the source + changes. This was the last copy of the retired advice in the repo. + + **objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.** + It had drifted two declarations up, so the exported `parseSortConfig` carried two + stacked leading comments and the helper carried none. This one was not cosmetic: because + `parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into + `dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort + parser with a sentence about action labels. Moving the block removes it from the `.d.ts`; + `formatActionLabel` is module-private, so its now-correct docblock does not appear there. + It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the + comment block *attached* to a declaration — a misattributed docblock is the mechanism by + which a claim gets scored against the wrong symbol. This block carries no spec-alignment + phrase, so nothing fired today. + + No tests accompany this change and none could: both edits are comment-only, and there is + no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real + `vite build` before and after, not asserted. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-list/package.json b/packages/plugin-list/package.json index d4b415978e..54e1828dd1 100644 --- a/packages/plugin-list/package.json +++ b/packages/plugin-list/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-list", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "ListView plugin for Object UI - unified view component with view type switching", diff --git a/packages/plugin-map/CHANGELOG.md b/packages/plugin-map/CHANGELOG.md index e03920fb69..a3148cf78c 100644 --- a/packages/plugin-map/CHANGELOG.md +++ b/packages/plugin-map/CHANGELOG.md @@ -1,5 +1,861 @@ # @object-ui/plugin-map +## 17.7.0 + +### Minor Changes + +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 0a2918f: The package README stops documenting a marker-title fallback that objectui#5953 replaced. + + `README.md`'s `map` block table described `titleField` as: "Omitted, markers are + titled `Marker`." That was true of the pre-#5953 read site, which bound the + marker title to a field name directly. objectui#5953 moved the marker title onto + `@object-ui/core`'s `getRecordDisplayName`, and `'Marker'` is now passed only as + that resolver's `fallback` option — a position the resolver reaches **only for a + record carrying no id at all**. A record with an id and no resolvable name reads + `Record #<id>`; a record whose object declares a `nameField` or a `titleFormat`, + or which simply carries a name-ish key, reads that. So "omitted ⇒ `Marker`" was + true in one narrow corner and false in the common case, and an author reading the + row would either under-specify `titleField` for a reason that stopped being true + or over-specify it to avoid a `Marker` that would never have appeared. + + The row now names the precedence an omitted `titleField` hands the decision to: + the declared `nameField`, its deprecated `displayNameField` alias, the legacy + `titleFormat` template, a type-aware pick from the object's fields, then + name-ish keys read straight off the record — with `Record #<id>` as the floor and + `Marker` reached only by an id-less record. + + Two details the row states deliberately: + + - It does **not** describe an object-level `objectDef.titleField` rung. The + resolver consulted one at step 0 as a second `??` leg, but objectui#6531 (PR + #6560) removes it — `@objectstack/spec`'s object schema is a `strictObject` + that rejects the key with `unrecognized_keys`, so no producer can ship it. + What survives is `options.titleField`, which is exactly what `map.titleField` + becomes at `ObjectMap`'s call site, so "a declared `titleField` wins" stays + true either way and the row does not go stale when that lands. + - It names the record-key probe (the resolver's step 4b) as its own rung. That + is not a footnote for this component: `ObjectMap` fetches an object schema + only when `!hasInlineData && dataSource`, so for `staticData` or an inline + `data` array no object definition ever reaches the resolver and the record-key + probe is the only rung that can produce a title — + `ObjectMap.markerTitle.test.tsx` pins exactly that case. + + A second row falsified by the same commit goes with it. Two lines below the table, + the field-name defaults paragraph listed what an unconfigured map falls back to as + "`latitude` / `longitude` / `location` / `name` / `description`". objectui#5953 removed + the title default: `getMapConfig`'s default branch returns coordinate keys and + `descriptionField` only, under a comment that spells out why — "Deliberately NO + `titleField` (objectui#5953)… `getRecordDisplayName` resolves it from the object + definition, and it does so better than any literal here could". The paragraph now + lists the four defaults that exist and says where an unconfigured marker's title + actually comes from. Left alone in the same sentence: `map: { titleField: 'name' }` + still names no coordinate field and still renders an empty map, which is accurate. + + Prose only: no behaviour changes, and the placeholders were already pinned in + order by `ObjectMap.markerTitle.test.tsx`. +- f9653ae: Re-key the load-bearing fetch effects in `ObjectMap`, `ObjectCalendar` and + `ObjectGantt` onto the primitive fields they actually read off `dataConfig` + (`provider` / `object` / `items`) instead of the whole memoised `dataConfig` + object (objectui#6592, the deferred half of objectui#6270/PR #6591). + `ObjectTree` is a census member too but is deferred out of this change — see + the PR body — because its own fetch effects are the surface of PR #6696 + (objectui#6481), open at the same time. + + `useMemo` carries no semantic guarantee — React is permitted to discard a + memo cache and recompute even when its dependency array compares equal to + the previous render, and the local `getDataConfig(schema)` helper each of + these renderers carries builds a fresh `{ provider, object }` / + `{ provider, items }` wrapper object on every call. So a fetch effect keyed + on `dataConfig` itself was correct only for as long as that identity + happened to survive a discard: a recompute alone (no author or caller + action) was enough to re-run the effect and issue an extra + `dataSource.find` / `dataSource.getObjectSchema` call. Keying the effects + on the primitives instead makes a cache discard a no-op, restoring + `useMemo` to a pure optimisation. + + `ObjectGantt`'s `effectiveDataSource` memo deliberately keeps `dataConfig` + as a dependency (`resolveDataSource` needs the whole provider-shaped + value — the `api` provider's `read`/`write` request config cannot be + flattened to a fixed primitive list the way `object`/`value` can), so its + `reload()` fetch is decoupled from the redundant direct `dataConfig` + dependency but not from `effectiveDataSource`'s own; for the `object`/`value` + providers `resolveDataSource` returns its `fallback`/a fresh + `ValueDataSource` respectively rather than reading further into the config, + which is enough for the two fetch effects to observe no extra call under a + recomputed-but-equivalent `dataConfig` in the common case. + + No behaviour change for a schema whose `useMemo` caches survive normally; + the effects are unaffected by React discarding one. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- ff7543c: `ObjectMapProps.clusterRadius`'s JSDoc said "in pixels"; `clusterMarkers` has + always used it as a coordinate-degree grid cell edge (`radius / 2 ** zoom`, + divided into the marker's `[lng, lat]` degrees), not a screen-space radius — + a host tuning clustering granularity by the documented unit would get a + completely different result than intended (objectui#5020). + + No behavior, default, or name changes: clustering, the >100-visible-marker + auto-threshold, and tap-through zoom are unaffected, and `clusterRadius` has + no call sites outside `plugin-map/src` today (re-confirmed repo-wide, + including `apps/`, `examples/`, and the `objectstack` spec/server repo — the + default of `50` is what runs everywhere). This is a doc-comment correction + only, bringing the JSDoc in line with the README's already-correct wording + (post objectui#5002). + + The latitude-anisotropy trade-off (a degree grid distorts east-west as + latitude rises) is a known design trade-off, not part of this fix. +- 2aac61f: `ObjectMap` now resolves marker titles through `@object-ui/core`'s + `getRecordDisplayName` (ADR-0079), instead of reading a hard-coded `'name'` key. + + `getMapConfig` used to fill an absent title binding with the string literal + `'name'`, and the marker transform then did a bare `record[titleField]` read. For + every object whose display field is not literally `name`, that read was + `undefined`, so each marker popup titled itself `undefined`. The literal is gone + from both branches that carried it, and the read site hands the decision to the + same resolver `ObjectKanban`, `ObjectCalendar` and `ObjectGantt` already title + their items through — `ObjectMap` was the fourth renderer and the only one still + outside it. + + An authored `map.titleField` keeps winning outright: it is passed through as the + resolver's explicit `titleField` option, which it checks first. What is new + underneath is everything a static field-name binding cannot express — the + object's declared `nameField`, its deprecated `displayNameField` alias, a legacy + `titleFormat` template, type-aware field derivation, and a name-ish probe over + the record's own keys for the inline-data case where no object definition is + fetched at all. + + Records with no resolvable name now read `Record #<id>` rather than `undefined` + or a uniform `Marker`; the `Marker` placeholder is kept only for a record that + carries no id either. No authoring surface changes and no new map config keys. +- b180a64: `ObjectMap` reads `schema.data` in one place again, so an array-shorthand map stops + making a metadata request it never uses. + + The fetch effect carried a second short-circuit beside the `props.data` one + objectui#5003 fixed: it read `schema.data` directly and tested whether that value was + itself an array. eslint reported it as `missing dependency: schema.data` — the last + `react-hooks/exhaustive-deps` warning on that effect. + + The dependency was never actually missing. `getDataConfig(schema)` already returns + `schema.data` verbatim, and the result is memoized on `JSON.stringify(rawDataConfig)` + into `dataConfig`, which **is** one of the effect's declared dependencies. The authored + rows therefore reached the effect before this change; the direct read was a duplicate of + an already-threaded value, which is why neither adding a dependency nor deleting the + branch was right. + + The array handling moved into `getDataConfig`, where `ObjectGrid`'s own `getDataConfig` + already pins the same normalization (`"Check if data is an array (shorthand format)"`). + Same rows render, and the effect now reads only `dataConfig`. + + One behavioural consequence, and it is the point: an array under `data` now yields + `provider: 'value'`, so `hasInlineData` is true and the sibling effect no longer calls + `dataSource.getObjectSchema()` for it. That request's only read site is + `buildExpandFields()` inside the object-provider fetch branch, which an inline schema + never reaches — so the call was pure waste, and the shorthand now behaves exactly like + the declared `{ provider: 'value', items }` form it is shorthand for. + + Deleting the branch instead was measured, not assumed: with no producer-side handling, + an array-shorthand map renders `Error: DataSource required for object/api providers` + rather than its markers. The shorthand is a live convention in six sibling blocks + (`ObjectGrid`, `ListView`, `ObjectTree`, `ObjectChart`, `ObjectDataTable`, + `calendar-view-renderer`), so `object-map` would have become the one block in the family + that answers it with an error box. +- 538ed92: `ObjectMap`: make the marker `useMemo` actually memoize. + + `getMapConfig(schema)` ran unmemoized in the render body, so `mapConfig` carried + a fresh object identity on every render. The marker transform names `mapConfig` + in its dependency array, so it recomputed on every single render — walking every + record through `extractCoordinates` and the display-name resolver, and re-running + `ObjectMapConfigSchema.safeParse` on each pass — while declaring that it does not. + The invalidation cascaded on into `filteredMarkers`, `clusteredData`, + `markerBounds` and `initialViewState`. + + `mapConfig` is now memoized on `schema`, the one value `getMapConfig` reads. + Behaviour is unchanged; the config is still rebuilt whenever the schema + genuinely changes. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- 2aa2c22: `ObjectMap` no longer serializes its data config on every render. + + `getDataConfig(schema)` was called bare in the render body and its result + re-serialized with `JSON.stringify` on every render, purely to give `dataConfig` + the stable identity its fetch effect depends on. `getDataConfig` is a pure + function of `schema`, so `useMemo(() => getDataConfig(schema), [schema])` gives + the same identity with no serialize and no per-render rebuild. + + This also fixes a crash. `JSON.stringify` throws on a value it cannot serialize, + and the config's passthrough branch returns the author's own `schema.data` + object verbatim — inline rows included. A map handed inline records carrying a + back-reference (an `$expand`-ed lookup) or a `BigInt` id threw from the render + body and took the whole map subtree down with it. Comparing identities never + serializes, so the config no longer has to be serializable at all. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index e01cabb7fe..e4be6a6f5f 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-map", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Map visualization plugin for Object UI", diff --git a/packages/plugin-markdown/CHANGELOG.md b/packages/plugin-markdown/CHANGELOG.md index 71b4108666..f649cdcf6d 100644 --- a/packages/plugin-markdown/CHANGELOG.md +++ b/packages/plugin-markdown/CHANGELOG.md @@ -1,5 +1,679 @@ # @object-ui/plugin-markdown +## 17.7.0 + +### Minor Changes + +- 1ec291c: Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 + enforce-or-remove, maintainer ruling 2026-08-31, option B). + + `inputType` was held back when `min` / `max` / `step` / `placeholder` were retired, because + its defect was a different one. Those four were declared-and-UNREAD. `inputType` was + declared-and-DROPPED: the repository really did author it — `packages/plugin-markdown` + wrote `inputType: 'textarea'` on its `content` input, pinned by that package's own test — + while the manifest serializer dropped it. Retiring it therefore had to decide what that + registration should say instead, which is the fork the card reported and the ruling closed. + + FROM → TO: + + - `inputType?: string` → **tombstoned** (`?: never` on the interface, `retirementTombstone()` + named refusal on the Zod mirror). Put the control hint in `description`, which IS + published. + - `plugin-markdown`'s `inputType: 'textarea'` write → **deleted**, at zero capability cost. + + The write was measured as a no-op before it was deleted, and re-measured on this branch's + base rather than inherited from the card. A structural census over every `inputs:` array in + the repository (211 regions, all tracked TS/TSX/JS sources) scores `inputType` at exactly + ONE authoring site — the `plugin-markdown` registration — against `name` 953, `type` 969, + `label` 966, `description` 194, `enum` 119, `required` 86 and `binding` 4 in the same pass + over the same regions, so the instrument was not blind. The other 192 in-repo `inputType` + hits are a DIFFERENT face: `FormField.inputType` (`zod/form.zod.ts`), the text-input + renderer's prop, and `SchemaBuilder.inputType`, none of which sit on a `ComponentInput`. + The publication path is unchanged and was re-confirmed: `packages/sdui-parser/src/index.ts` + forwards exactly six keys per input — `name`, `type`, `required`, `enum`, `binding`, + `description` — so an authored `inputType` could not reach the published + `sdui.manifest.json` even in principle. + + Option A — teach `sdui-parser` to forward the key — is REFUSED on record. The only thing + that looked like demand for it was a write that had never taken effect, and a write nothing + reads is not demand for a feature. The neighbouring 2026-08-17 expression-ceiling ruling + (quoted on `ComponentInput.type`) is untouched and stays deferred, with its reopen + condition — a measured case of an author shipping a spec-rejected value objectui's silence + let through — unchanged. + + Deleting the member outright was again the option NOT taken, for the reason the four + siblings established: `ComponentInputSchema` is a non-strict `z.object`, so an undeclared + key is silently STRIPPED. The tombstone is what converts a write from OUTSIDE this + repository — the half objectui#5905 could not measure — into a named refusal carrying its + own remedy, with `code: 'invalid_type'` and the key named in the issue `path`. + + Accept-set change, stated plainly for reviewers: a document that sets `ComponentInput.inputType` + used to parse GREEN (the value was then dropped by the serializer) and now parses RED. That + is the intended effect and the reason this carries a contract-review label. + + Three pins were FLIPPED rather than deleted, so the closure stays asserted instead of + becoming a silent absence: `plugin-markdown`'s `index.test.ts` (which asserted the write) + now asserts the key's absence plus a `tsc` refusal at that package's own authoring site, + and the two fork-half controls in + `packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts` — one + type-level, one parse-level — now assert refusal where they asserted liveness. + + Stale wording corrected in the same pass, because this change falsifies it: `base.ts` and + `zod/base.zod.ts` both said the fork was "recorded for a ruling; until then this stays a + live, writable key", and `widget.ts` called it "the open fork". All three now record the + ruling. A reader who greps the source instead of the card thread was meeting an open fork + that no longer existed. +- b3d562c: One authority for `MarkdownSchema`, and for `KanbanCard` / `KanbanColumn` + inside `@object-ui/plugin-kanban` (objectui#6172, folding in objectui#6155). + + The 2026-08-25 family ruling: every exported schema name has exactly one + authority. Two of this card's names are discharged here. + + **`MarkdownSchema` — converged onto `@object-ui/types`.** + `@object-ui/plugin-markdown` declared a second copy of the name. The two + differed on exactly one member — `content`, required in `@object-ui/types` and + optional in the plugin — and that was measured to be drift rather than a real + semantic difference: the plugin's own registration declares the `content` input + `required: true` (pinned by its own test), `MarkdownImplProps.content` is a + non-optional `string`, the Zod mirror spells `z.string()`, and every authored + `type: 'markdown'` node in the repository supplies `content`. The plugin now + re-exports the one authority. + + ⚠️ **Breaking, in the narrowing direction, for `@object-ui/plugin-markdown` + consumers**: `MarkdownSchema['content']` goes from optional to **required**. A + value annotated `MarkdownSchema` that omitted `content` no longer type-checks. + Measured against this repository: zero authored markdown nodes omit it, so + nothing in-tree changed. (`type: 'markdown'` literals that carry no `content` + are rich-text FIELD metadata — `MarkdownFieldMetadata` — a different type.) + The plugin's face also gains the optional `sanitize` and `components` members + the canonical declaration carries; both are additive, and neither is read by + this renderer, which sanitizes unconditionally. + + `className` is unaffected — it comes from `BaseSchema`, which both copies + extended, so it was always inherited rather than added by the plugin. + + **`KanbanCard` / `KanbanColumn` — the three in-package copies converged to + one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A + TypeScript-AST comparison found them strict-SUBSET copies of `./types` with + nothing typed differently, so their extra members moved onto the one + declaration and both files now re-point at it. + + Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and + `coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every + value that type-checked before still does. Both modules keep their previous + export surface via re-export, so no import path changes. + + The cross-package `KanbanCard` / `KanbanColumn` / `KanbanSchema` collision + between `@object-ui/types` and `@object-ui/plugin-kanban` is NOT resolved here + and is escalated on objectui#6172 — those are two different dialects (`items` + vs `cards`, `labels` vs `badges`), and collapsing them renames a published + name, which needs an authority ruling. +- 446d93d: **Breaking for authored metadata:** `MarkdownSchema.sanitize` and + `MarkdownSchema.components` are RETIRED (objectui#6972, ADR-0049 + enforce-or-remove). A `markdown` node that authors either key no longer + validates: the parse fails loudly on that key's path with the explanation in + the message, and both TS members are `?: never` tombstones, so the same document + is refused at compile time. The two keys do not share a disposition — triage + recorded the asymmetry — so each is argued below. + + ## `sanitize` + + **What was measured, on this branch's base.** `sanitize` was declared + `?: boolean` with `@default true` on both published faces — `data-display.ts` + and the Zod mirror — documented, and read by NOTHING. Worse than an ordinary + inert key, it implied a switch that does not exist: sanitization is + **unconditional**. `rehypePlugins` in `plugin-markdown/src/MarkdownImpl.tsx` is + a module-level `const` array whose last link is `[rehypeSanitize, sanitizeSchema]`, + handed to `ReactMarkdown` as-is — no ternary, no `if`, no runtime assembly. + `MarkdownRenderer` forwards exactly `content` and `className`, and + `MarkdownImplProps` accepts only those two. A repo-wide grep for + `schema.sanitize` over `packages/` and `apps/` returns nothing, against a + control of 20 `.tsx` files reading `schema.content` in the same query shape, so + the zero is a reading, not a blind query. An author writing `sanitize: false` + believed they turned XSS filtering off; one writing `sanitize: true` believed + they turned it on. Neither was true. + + **Why remove and not enforce.** The enforce arm of enforce-or-remove for this + key is a switch that DISABLES XSS sanitization, which is not an acceptable + outcome; for `sanitize` the ruling collapses to remove (triage on + objectui#6972). + + **Who is affected — a `sanitize` authored onto a `markdown` node:** + + ```json + { "type": "markdown", + "content": "# Hello", + "sanitize": false } // ← was tolerated, changed nothing + ``` + + now fails validation with: + + > RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a + > fixed last link of the markdown renderer's rehype chain, and no value of this + > key ever switched it. There is no authored spelling that disables XSS + > sanitization; delete the key. + + ## `components` + + **What was measured, on this branch's base.** `components` was declared + `?: Record<string, any>` ("custom components for markdown elements") on both + faces and read by NOTHING: the `components` map `MarkdownImpl` hands to + `ReactMarkdown` is its own module-level `mdComponents` (the mermaid / metadata + fence overrides), never merged with anything off the schema, and + `grep -rn "schema.components"` over `packages/` and `apps/` returns nothing + against the same `schema.content` control. The premise the PM declared + falsifiable — *no host path consumes a `components` map* — was re-measured + before this half was written: `MarkdownImplProps` has no such prop, `LazyMarkdown` + receives only `content` and `className`, and no plugin API, app-shell or runner + site passes one. + + **Why remove and not wire, and why not a runtime slot.** A map of React + component overrides is not a value a JSON document can author — the same shape + as the handler keys objectui#6124 retired ("JSON has no function value"). The + `runtime-slot` disposition keeps a TypeScript twin callable when a host-supplied + value actually reaches a renderer; nothing reaches this one, so there is no twin + to keep and the TS face refuses it outright. This half is the PM's disposition + under a declared veto window on objectui#6972, not a maintainer ruling; the PR + stays draft for contract review. A real override slot must arrive as a proposal + WITH its enforcing reader, not by reviving this key. + + ```json + { "type": "markdown", + "content": "# Hello", + "components": { "h1": "h2" } } // ← was tolerated, changed nothing + ``` + + now fails validation with: + + > RETIRED (objectui#6972) — never read: the markdown renderer forwards only + > `content` and `className`, and a map of React component overrides is not a + > JSON-authorable value. Delete the key; the fenced mermaid / metadata block + > overrides are the renderer's own fixed map, not an authoring surface. + + ## Both keys + + **Two published faces.** `@object-ui/plugin-markdown` re-exports `MarkdownSchema` + from `@object-ui/types` (one authority since objectui#6172) rather than + declaring a copy, so the retirement reaches its consumers through the same + declaration — which is why this changeset names the plugin as well: no plugin + source changes, but the type its published face exposes narrows, and its own + test now pins that the refusal arrives there. + + **Who is NOT affected.** A document that never wrote either key is untouched + (`absent` stays valid), `content` and `className` are unchanged, and the + renderer's behaviour is byte-identical — it sanitized unconditionally before + and does now, and its fenced-block overrides are the same fixed map. One + in-repo fixture authored either key + (`packages/types/examples/data-display-examples.json#examples.markdown`, + `"sanitize": true`); the key is deleted from it and the fixture is now pinned + to parse green. No fixture, catalog entry, doc snippet, skill or app in this + repository authored `components` on a markdown node. + + **Migration:** delete the keys. There is nothing to replace either with — the + behaviour `sanitize` claimed to control is always on, and no authored spelling + overrides markdown elements. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated key. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 90c6d09: Fix `extractToc` deleting tag-shaped text that lives INSIDE an inline code span, + so its `#id` links resolve to the heading they name again (objectui#7658). + + `stripInline()` applied its rules in sequence: the inline-code rule unwrapped + `` `objectui add <component>` `` to `objectui add <component>`, and the raw-HTML + rule that ran next over that same text deleted `<component>` as if it were + markup. The slug became `objectui-add` while `rehype-slug` — which slugs the + RENDERED heading, where a code span's content is a literal text value — put + `objectui-add-component` on the anchor. The TOC entry rendered, was clickable, + and silently went nowhere. The same sequencing let the emphasis rules eat the + underscores out of `` `a_b_c` `` and the link rule rewrite `` `[x](y)` ``. + + Code spans are now lifted out before any other inline rule runs and restored + verbatim at the end, so nothing reaches inside one. The raw-HTML rule is + unchanged and still strips genuine markup — `remark-rehype` runs without + `allowDangerousHtml`, so the renderer likewise drops raw html nodes and keeps the + text they wrapped. Link labels that are code spans still collapse (`` [`getData`](/api) `` + → `getData`), because the placeholder stays ordinary text to the link rule. + + Seven live headings in this repo's own docs were affected + (`content/docs/utilities/cli.mdx`, `content/docs/utilities/runner.mdx`, + `packages/cli/README.md`). Pinned against the real render pipeline rather than a + second derivation of the slug rules: the new test renders each heading through + `MarkdownImpl` and compares `extractToc`'s id to the `id` attribute + `rehype-slug` actually emitted. +- a472b07: Fix `extractToc` eating the underscores out of a `SCREAMING_SNAKE` heading, so + its `#id` links resolve to the heading they name again (objectui#7667). + + `stripInline()` ended with two hand-rolled emphasis rules that gave `*` and `_` + one shared regex — `(\*\*|__)(.*?)\1` and `(\*|_)(.*?)\1`. Neither knew + CommonMark's flanking rule, under which a `_` run INSIDE a word opens nothing: + it is both left- and right-flanking with no adjacent punctuation, so it may + neither open nor close emphasis. The renderer obeys that and keeps the + underscores; the shared rule paired the first two underscores of + `### NON_GRID_ROW_CEILING` and ate `GRID`, then resumed and ate `ROW`. The TOC + said `nongridrow_ceiling` while `rehype-slug` put `non_grid_row_ceiling` on the + anchor, so the entry rendered, was clickable, and silently went nowhere. + + The underscore form is now its own flanking-aware rule and the asterisk rules + are left alone, because only `_` carries the intraword exemption — giving `*` + the same one would break `a*b*c`, which the renderer really does emphasise. + Underscore runs are matched whole (`(?<!_)` / `(?!_)`), which is what keeps + `x__init__y` literal, and a matched pair is dropped at whatever length it has, + since it contributes no characters to the rendered text however it nests. + + The one-underscore case is a REGRESSION pin, not a repair: `## the snake_case + name` was already correct — a lone underscore has nothing to pair with, which + is why this went unnoticed for so long — and it still slugs + `the-snake_case-name`. It takes two or more underscores in one heading for the + old italic rule to find a pair. + + One live heading in this repository's own docs was affected + (`packages/react/README.md:224`). Measured, not derived: the corpus sweep over + `content/docs/**` plus every `packages/*/README.md` — 223 files, 2941 rendered + headings — goes from 5 divergent files to 4, and the 4 that remain are a + different, already-filed defect (objectui#7666, a heading `extractToc` lists + that the renderer never emits under a JSX block). Pinned against the real + render pipeline rather than a second derivation of the flanking rule: each + heading is rendered through `MarkdownImpl` and `extractToc`'s id compared to + the `id` attribute `rehype-slug` actually emitted. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-markdown/package.json b/packages/plugin-markdown/package.json index 3249cdef4f..1df404d129 100644 --- a/packages/plugin-markdown/package.json +++ b/packages/plugin-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-markdown", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Markdown rendering plugin for Object UI, powered by react-markdown", diff --git a/packages/plugin-report/CHANGELOG.md b/packages/plugin-report/CHANGELOG.md index d46584102e..3ec51a8fe1 100644 --- a/packages/plugin-report/CHANGELOG.md +++ b/packages/plugin-report/CHANGELOG.md @@ -1,5 +1,656 @@ # @object-ui/plugin-report +## 17.7.0 + +### Minor Changes + +- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the + dataset captions. + + **The maker's start chips now promise only what ADR-0112 v1 builds + (cloud#1984).** Two of the five asked for automation the first version has no + flows or actions for — the ticket chip said 「状态流转」, the inventory chip said + 「低库存预警」 — and the measured behaviour was not a refusal but a silent + degrade: a status kanban and a low-stock view. The chip promised an alert and + delivered a page. All five are reworded in all ten packs (and in the call-site + `defaultValue` fallbacks, which are a second copy of the same strings) to ask + for objects, fields, views, pages, dashboards and sample data, keeping each a + real business scenario — the ticket chip now asks for a status field and a board + grouped by it, the inventory chip for a view that filters below the reorder + point. A note beside the keys says to revert when v2 re-adds flows. + + **Five newer AI tools get their step labels (objectui#7481).** A zh conversation + read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」: + `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`, + `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are + newer than the pinned spec's tool registry, so they had no `chatbot.tool.*` + entry in any pack and fell through to the English title-caser. + + **The assistant rail follows the thread when you send (objectui#7480).** The + rail and the full-page maker are the same component; what differs is width. A + reply that still ends on screen in the wide column runs two or three times + taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the + user types and the new bubble, the tool steps and the streaming answer all land + below the fold. Every send path now re-arms the lock — including the plan-card + "Build it" and 确认修改 approvals, whose own code comments already named this + miss. Message APPENDS deliberately do not, so a user reading back through the + thread mid-answer is never yanked to the bottom. + + **Console toasts move off the assistant composer (objectui#7482).** 「客户更新 + 成功」 sat on the ChatDock composer's send button and stayed there. One defect, + two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override + that predates ADR-0057 P3a — so a toast both covered the button and, because + sonner pauses a toast's dismiss timer while the pointer is inside the toaster + region, never got to run its 4s timer with a pointer resting on the composer + underneath. The override is gone; the console takes `ConsoleToaster`'s own + documented top-right anchor, and the 4s success duration is now pinned. + + **Built-in aggregate captions follow the locale everywhere (objectui#7534).** + objectui#7258 taught `buildChartSeries()` to resolve a server-minted default + measure through the locale map, so a chart legend read `计数` while the table + beneath it, the KPI caption, the pivot header and the dataset preview still + printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()` + takes the same optional `builtinAggregateLabels`, resolving through the one + `resolveMeasureLabel` order, and the five call sites pass it. Omitting the + argument reproduces the previous output byte for byte, and an author-declared + measure still keeps its own label verbatim (objectui#4106). + + **The activity feed stops asking for an object the environment does not have + (objectui#7476).** A tenant environment has no `sys_activity`, so every page + load issued a request that 404'd. Everything downstream was already correct — + the adapter memoizes the missing collection, its logger demotes the failure, the + feed retires as an ANSWER and the panel renders its earned empty state — so what + is left is the request itself, and `data-objectstack` states the rule for it: + the cure for a doomed request is not issuing it. New `useObjectPresence` reads + the object registry the shell loads for the nav anyway; only a registry that has + ANSWERED and lists other objects without this one skips the read. Every + uncertainty — no provider, empty registry, still loading, errored — reads as + before, because a wrong skip would cost a real deployment its feed. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 854222c: `@object-ui/plugin-report` now registers its three components under namespace + **`plugin-report`**, the spelling its consumers already declare (objectui#6416). + + It used to register `report`, `spec-report` and `report-viewer` under namespace + `report`, while `apps/console` declared the lazy stubs for the same three short + names under `plugin-report` and the CLI's known-type whitelist shipped the + `plugin-report:*` spellings as renderable. Two things followed from the + disagreement: + + - **`plugin-report:report`, `plugin-report:report-viewer` and + `plugin-report:spec-report` could never be satisfied.** `Registry.register` + clears the lazy stub for the type IT registers, and that type was + `report:report`, so those three stubs were never cleared and no component was + ever stored under them: `get('report', 'plugin-report')` returned `undefined` + and `hasLazy('report', 'plugin-report')` stayed `true` forever. A schema + authored with any of the three whitelisted keys resolved to nothing — the + gate handed authors a green light for a key the runtime could not satisfy. + - **The bare `report` key was claimed twice under two different namespaces.** + `Registry.register` and `Registry.registerLazy` share the + `meta?.namespace && !meta?.skipFallback` branch, so what bare `report` + *declared* depended on whether the plugin chunk had loaded yet — the + objectui#6353 shape. + + **No authored metadata changes.** The direction was chosen by measurement: + nothing in this repository, and nothing in the sibling `objectstack` checkout, + authors a `report:*` spelling (0 hits), while the bare spellings are authored in + 48 places. `type: 'report'`, `type: 'spec-report'` and `type: 'report-viewer'` + resolve exactly as before; the three unreachable `report:*` keys are retired and + the three `plugin-report:*` keys now name real components for the first time. + + `packages/cli/src/utils/known-schema-types.ts` is regenerated from the + registrations, dropping `report:report`, `report:report-viewer` and + `report:spec-report`. + + Two pins are the half that outlives the fix: + `packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts` replays + this package's real declared metadata and a console-shaped lazy stub into a + fresh `Registry` in **both** registration orders, checking the bare key's + declared namespace after every step, so order- and phase-independence are + properties under test rather than properties of the file the test imports. + `scripts/__tests__/report-namespace-agreement-6416.test.ts` re-derives both + sites from source and fails if the plugin, the console stubs and the generated + whitelist ever disagree again. +- 47547d0: Localize the server's built-in aggregate measure titles on dataset charts + (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer + ruling B, 2026-09-02). + + A dataset-bound chart's aggregate axis / legend title read the analytics + service's hard-coded English `Count` on a zh console whose category labels were + already Chinese. The renderer was passing `fields[].label` through verbatim — + correctly, for an author-declared measure (objectui#4106) — and had no way to + tell the server's built-in default apart from an author's label. + + The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural + discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | + 'count_distinct'`, populated only on the server-side built-in defaults + (objectstack#14492). This change is the consumer side of that contract: + + - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` — + `ChartResultField` plus the optional `builtinAggregate` carrier + (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than + on it because the spec this release is built against does not carry the key + yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` / + `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries + the locale strings in (core stays React-free and i18n-free — the same + division as `nullCategoryLabel`). A field carrying a recognised + discriminator resolves through that map; every other field keeps its wire + `label` verbatim — never by matching the label's text or the field's name + (the rejected option A). + - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings + through the existing `report.aggregate.*` keys (zh already carried 计数 / + 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary). + - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`), + `plugin-report` (`DatasetReportRenderer`): pass the resolved map to + `buildChartSeries`. + + Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same + chart titled `计数`; an `en` session still reads `Count`; an author-labelled + measure (`Tasks`) and a measure literally named `count` without the + discriminator are byte-for-byte unchanged. Until the upstream field is + populated the wire carries no discriminator and every chart renders exactly as + before. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [8631c32] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [98188c2] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [f46bd39] +- Updated dependencies [b98352a] +- Updated dependencies [b76ca67] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [e552c31] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [7b90231] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [8579e34] +- Updated dependencies [d57db5d] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [0caacca] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [b458300] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [39d69ad] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [e8e4c4d] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [7ed9808] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [e4e9557] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [b6e83be] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-report/package.json b/packages/plugin-report/package.json index e3857bed11..b733ae8200 100644 --- a/packages/plugin-report/package.json +++ b/packages/plugin-report/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-report", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/packages/plugin-timeline/CHANGELOG.md b/packages/plugin-timeline/CHANGELOG.md index 1752c39e3a..49ad5e891d 100644 --- a/packages/plugin-timeline/CHANGELOG.md +++ b/packages/plugin-timeline/CHANGELOG.md @@ -1,5 +1,1057 @@ # @object-ui/plugin-timeline +## 17.7.0 + +### Minor Changes + +- e4559d1: `TimelineSchema` now declares the presentational keys the timeline renderer actually reads + (objectui#6170, maintainer ruling 2026-08-25 — the same family rule adopted on + objectui#6172: the exported type aligns to the measured authored + read set). + + Before this, `TimelineSchema` declared `events` (required), `orientation` and `position`, + and nothing else. `TimelineRenderer` is annotated `schema: TimelineSchema` and reads nine + keys off that node — `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`, + `rowLabel`, `scale`, `timeScale` — and **none** of the three that were declared. The docs + property table and the registration's own designer `inputs` had agreed with the renderer + all along; only the exported type disagreed. It was invisible to `tsc` because `BaseSchema` + carries `[key: string]: any`, so every undeclared key resolved as `any` and the annotation + constrained nothing. + + The most visible casualty was the docs page's own TypeScript example, which did not + compile: `Property 'events' is missing in type '{ type: "timeline"; variant: string; items: + … }' but required in type 'TimelineSchema'`. The page taught an authoring form its own + published type refused. + + **Declared now** (TS interface and the `@object-ui/types/zod` mirror together): `variant`, + `items`, `dateFormat`, `scale`, `timeScale`, `rowLabel`, `minDate`, `maxDate`. `onItemClick` + is deliberately left undeclared — it is a runtime slot `ObjectTimeline` installs, and this + package keeps callback-shaped keys off the authored surface. + + **`scale` is the canonical axis key.** It is `@objectstack/spec`'s `ui/TimelineConfig.json` + spelling and the one `resolveTimelineScale` reads first (`scale ?? timeScale`). The designer + now offers it, with all six buckets: `hour` / `quarter` / `year` have rendered correctly + since objectui#2942 but were offered by neither the designer (which listed three) nor the + exported type (which listed none), so they were authorable and undiscoverable. `timeScale` + stays as a deprecated alias so stored JSON keeps working; retiring it is routed separately. + + **`events` is now optional.** It was required, which is why the documented authoring form + did not type-check. That widening is the only non-additive change here — strictly more + programs compile and strictly more input parses than before. `events`, `orientation` and + `position` remain declared and remain read by nothing; a timeline authored with `events` + still renders an empty rail. Their removal is a breaking narrowing of a published type and + is routed through ADR-0049 enforce-or-remove as its own change, not smuggled into this one. + + Accept-set note for consumers: keys that previously resolved as `any` are now typed, so a + value the renderer never implemented — `variant: 'diagonal'`, `dateFormat: 'medieval'`, + `scale: 'fortnight'` — is a type error and a Zod rejection where it used to pass silently. + Nothing that renders today stops rendering. `BaseSchema`'s index signature is untouched, so + an undeclared key is still accepted by both halves (objectui#5155 / objectui#6269 own that + ceiling). +- 9d86e1d: Retire the `timeScale` alias on the timeline node — `scale` is the only axis spelling + (objectui#6355, maintainer ruling 2026-08-27). + + **BREAKING for authored metadata.** `timeScale` was this renderer's pre-spec spelling of the + Gantt axis bucket. `scale` is canonical — it is `@objectstack/spec` `ui/TimelineConfig.json`'s + axis key and the key the renderer preferred (objectui#6170 ruling, 2026-08-25: `timeScale` + goes the alias-retirement route, not a silent second spelling). objectui#6355's ruling + retires it immediately, with no phased window, while the project is at startup stage. + + **What breaks, and how you will find out.** A timeline document that spells `timeScale` is + now **refused**, loudly, at the authoring boundary: + + - `TimelineSchema.timeScale` is declared `?: never` — writing it is a type error; + - the Zod twin declares `z.never().optional()` — parsing a document that carries the key + fails with `invalid_type` / `expected: never` on the `timeScale` path. + + The fix is a rename: `timeScale` → `scale`. The accepted values are unchanged (`hour`, + `day`, `week`, `month`, `quarter`, `year`), so no value needs rewriting. + + **Why a tombstone rather than deleting the key.** `BaseSchema` is `.passthrough()` on the + Zod side and carries `[key: string]: any` on the TS side, so an *undeclared* key is accepted + unvalidated by both halves. Deleting `timeScale` outright would have let the retired spelling + parse green and type-check green while the renderer no longer read it — the Gantt axis would + silently fall back to the `month` default, the chart would change bucket, and nothing would + error. That is the silent axis breakage objectui#2942 closed, running in the other direction, + and it is the specific outcome this retirement is shaped to prevent. Keeping the key declared + as `never` on both halves is what makes the removal audible. Absent stays valid on both, so a + document that never wrote the alias is untouched. + + Also in this change: + + - `resolveTimelineScale` drops the `?? schema.timeScale` fallback read; its parameter narrows + to `{ scale?: unknown }`. + - The designer drops its deprecated `timeScale` input. The `scale` input already offers all + six buckets. + - `ObjectTimeline` now emits the resolved axis under `scale` when it composes the schema it + hands to the renderer. It previously wrote the alias, which would have made **every** + object-bound Gantt fall through to the `month` default the moment the fallback read went — + silently, since that is a composed schema no author ever sees. Writing `scale` after the + spread also restores the precedence the surrounding code intends: a `timelineConfig.scale` + now actually beats a flat `schema.scale`, where under the alias the resolver's + `scale ?? timeScale` ordering let the flat key win. + - The two in-repo authors are migrated in the same change: the schema-catalog + `gantt-style-timeline.json` fixture and the registration's own `examples.gantt` block. + - Docs drop the `timeScale` row and gain a retirement callout; + `packages/components/.../TIMELINE.md`'s Gantt table now documents `scale` with the full + six-value vocabulary it has accepted since objectui#2942 (its row still claimed three). + + Version note: `minor`, not `major`, per AGENTS.md §版本号策略 — objectui's major tracks the + `@objectstack` major and all publishable packages share one `fixed` group, so a breaking + narrowing is declared `minor` with the break spelled out here. +- 20cb8db: `ObjectTimeline` refuses an undeclared date axis instead of inventing one + (objectui#7459). + + Steps ① and ② of the three-step sequence the maintainer ruled on objectui#7070 + (2026-09-01, 总监批 #28). House posture, on record with that ruling: + 日期轴永不虚构 — a date axis is never fabricated. + + `ObjectTimeline` resolved its date axis through five declared bindings and then + closed the chain with a sixth rung that was a bare literal field name nobody + has ever declared. A name therefore ALWAYS resolved: for a view that declared + no axis, every record read a key its object does not carry, every event landed + in the "No date" bucket, and the screen read as a timeline that had been built + and simply had nothing in it. + + Two changes, shipped together because neither is observable alone: + + - **The renderer now refuses.** An object-bound timeline with no declared date + axis renders a diagnostic naming every binding it accepts — + `timeline.startDateField`, `timeline.dateField`, `mapping.date`, and the two + deprecated flat spellings — instead of a chart. The twin of `ObjectGantt`'s + "Gantt configuration required" screen, in the shape objectui#7070 settled. + - **The invented sixth rung is gone**, which is the only thing that makes the + refusal reachable. Added while the floor stood, it would have been dead code; + retired without the refusal, it would have produced exactly the silent + "No date" outcome the ruling rejects. + + **What changes for an author.** A view that declares a date axis is completely + unaffected — all five declared spellings resolve exactly as before, and a + timeline authored from literal `items` is never refused, since its items carry + their own dates and no field name is read for them. A view that declared no + axis anywhere, and was rendering an empty-looking timeline, now says so. + + ⚠️ Both premises were RE-MEASURED on the current tree before anything was + edited, rather than taken from the card: the renderer had no absent-axis + refusal (against a live control term that fires in the same file), and the + floor was still present and still spelled as reported. The pairing itself is + pinned — the refusal cases go red the moment the floor returns, including one + whose records carry a column that happens to be named `date`, where a returned + floor renders a convincing timeline rather than an empty one. + + Refusal is distinguished from "renders an empty timeline" by asserting the + canvas is ABSENT, not merely event-free. The component's success surface is now + named (`data-testid="timeline-canvas"`) so that distinction can be measured; + every other terminal state of the component already named itself. + + Step ③ of the ruling — the `'created_at'` floors on the two plugin faces — + stays on objectui#7070 and is deliberately NOT in this change. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- d813e8b: The bare `timeline` component key is now owned by DECLARATION rather than by + module-evaluation order (objectui#6353). + + `packages/plugin-timeline` registers the same short name twice — + `plugin-timeline:timeline` (`src/renderer.tsx`, the presentational renderer) and + `view:timeline` (`src/index.tsx`, the object-bound `ObjectTimelineRenderer`). Neither + passed `skipFallback`, so under `Registry.register` both also claimed the **bare** + `timeline` key and the last module to evaluate won it. `src/index.tsx` re-exports + `./renderer` (line 300) before its own `import` (line 307), so the presentational one + registered first and the object-bound one overwrote it. + + The resolved outcome was the intended one and **does not change here**: `type: + 'timeline'` still renders `ObjectTimelineRenderer`, which delegates inward to the + presentational renderer. What changes is that it is now decided rather than inherited. + Reordering those two lines would previously have handed `type: 'timeline'` to the + presentational renderer, which reads none of the object-bound keys (`object`, `filter`, + `sort`, `limit`) — an authored timeline would have stopped fetching, with no error and + no failing test. The registry's own collision guard names this remedy in its warning + text; this applies it. + + `src/renderer.tsx` now registers with `skipFallback: true`, so only `view:timeline` + claims the bare key, in any evaluation order. The presentational renderer stays + reachable under its explicit `plugin-timeline:timeline` key, which is the lookup a + presentational host already uses — no consumer-visible resolution changes. + + `src/__tests__/timeline-bare-key-ownership.test.ts` is the half that outlives the fix: + it fails if the declaration is dropped, if a third registration starts claiming the bare + key, or if resolution becomes order-dependent again. It reads both registrations' real + declared metadata back out of the registry and replays them into a fresh `Registry` in + **both** orders, so order-independence is a property under test rather than a property + of the file the test happens to import. +- f7ea89b: An object-bound timeline with `variant: 'gantt'` refuses loudly instead of + throwing (objectui#6655). + + The two timeline item shapes are not interchangeable, and this path crossed + them. `ObjectTimeline` maps each record to a flat FEED item — one per record, + no nested `items` — while the renderer's gantt branch reads a gantt ROW + (`row.items[].startDate`). Every `row.items` was therefore `undefined`, + `calculateDateRange` reduced an empty list, `Math.min()` over it was `Infinity`, + and `new Date(Infinity).toISOString()` threw `RangeError: Invalid time value` + mid-render. There was no guard and no diagnostic — the component simply threw. + + Per the maintainer ruling of 2026-08-29, the object-bound path now rejects + `variant: 'gantt'` with an author-facing diagnostic naming the limitation + (object-bound timelines render the feed variants; gantt needs literal rows, each + carrying its own nested items). Composing real gantt rows from records was + considered and NOT adopted; that capability stays open and unruled. + + The refusal keys on whether the items were AUTHORED, not on the variant alone, + so a literal gantt is untouched — including the bare `timeline` key that this + component answers, which is what the in-repo catalog fixture + `plugin-timeline/gantt-style-timeline.json` uses. The feed variants + (`vertical` / `horizontal`) and the presentational `TimelineRenderer` are + unchanged. + + Side effect the ruling asked for: the gantt-only axis this path composes + (`timeline.scale ?? scale`) is no longer silently inert on the gantt variant — + an author who set it is now told why it has no effect, rather than getting a + crash. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 15b1776: A gantt timeline with an EMPTY literal `items` array renders a zero-row grid instead of + throwing (objectui#6750). + + `calculateDateRange` reduced the empty list with no guard: `allDates` is `[]`, `Math.min()` + over no arguments is `Infinity`, and `new Date(Infinity).toISOString()` throws `RangeError: + Invalid time value` during render. Both entry points crashed identically — `TimelineRenderer` + given `{ variant: 'gantt', items: [] }`, and `ObjectTimeline` given the same schema (an + authored empty array is truthy, so it passes straight through as authored items). + + An empty gantt is the **ordinary empty state of a valid schema**, not a malformed document. + Any author or generator that builds `items` from a collection emits `items: []` the moment + the collection is empty — a filtered project list with no matches, a fresh workspace, a plan + whose rows are yet to be added. + + The fix covers the whole gantt branch in one pass rather than the one `throw`, because + patching only the crash site moves it two stops down the same branch: + + - `calculateDateRange` returns a one-day sentinel range anchored on today when the rows carry + no dates at all. The span is one day — the smallest coherent range — because how much time + an empty gantt should show is a question about what an empty gantt should look like, which + this change deliberately does not answer. + - `generateTimeScaleHeaders` needed no change, and that is a measured verdict rather than an + assumption: a degenerate `min === max` range is not inverted, so the loop runs once and + every scale emits exactly one bucket. The empty gantt therefore gets a real one-column axis, + not a header row with zero cells. + - `calculateBarDimensions` gains a `totalDuration === 0` guard. A zero-width axis — every task + starting and ending on the same day, or an author pinning `minDate === maxDate` — divided + `0 / 0` into `NaN`, and the bar was handed `left: NaN%; width: NaN%`. That is not a crash + and not a visible error: the CSSOM rejects both declarations, so React left the element with + no `style` attribute at all and the bar rendered unpositioned and zero-width. On a zero-width + axis every task covers the whole of it by definition, so the guard returns `{ start: 0, + width: 100 }`. + + An author-pinned `minDate` / `maxDate` is untouched by the sentinel: the gantt branch resolves + `schema.minDate || dateRange.minDate`, so a pinned range with `items: []` renders exactly that + range with no rows in it — most likely what the author wanted, and free. + + **No product judgment about what an empty gantt should look like.** "Do not crash" is a + correctness floor; whether the empty case should become the repo's standard empty-state panel + instead of a zero-row grid is a separate, still-open question, and substituting one here would + have been taking a decision that was left open on purpose. objectui#6655's object-bound gantt + refusal is also untouched and stays keyed on whether items were authored, which is precisely + why it does not fire on this case. +- 4f3a1e2: fix(plugin-timeline): refuse an unusable gantt date range with a diagnostic that names the offending value + + A gantt whose date range cannot be used now renders a `role="alert"` diagnostic + naming the value that made it unusable, instead of crashing or drawing a chart + that is confidently wrong. Two input classes, which failed in opposite + directions: + + - A date that does not parse threw `RangeError: Invalid time value` mid-render — + the same crash site objectui#6750 guarded for the empty list, on a different + input. This covers a malformed value (`startDate: 'not-a-date'`), an absent + one, and an unparseable `minDate` / `maxDate` pinned on the schema. + - An inverted pinned range (`minDate` after `maxDate`) drew a bar at + `left: 157.9%; width: -4.3%` under a header row with zero cells, with no + error and no diagnostic. + + Valid gantts, the empty-list sentinel from objectui#6750 and the degenerate + `minDate === maxDate` axis are unchanged. +- e661d63: fix(plugin-timeline): refuse a `null` gantt date instead of drawing it at the epoch + + `new Date(null).getTime()` is `0`, not `NaN` — the Unix epoch, not an invalid + date — so a `null` `startDate` / `endDate` passed the objectui#6759 parse guard + and reached the arithmetic as `1970-01-01`. One row item with `endDate: null` + drew a 649-column axis spanning 1970 to 2024 and a bar at + `left: 100%; width: -100%`, with no error and no diagnostic; a `null` + `startDate`, or both null, drew a bar with entirely plausible geometry on an + axis anchored in 1970 — a chart a reader would believe. + + A `null` row date now renders the same `role="alert"` diagnostic objectui#6759 + established, naming the authored path and spelling the value as `null`. This is + consistency with that card rather than a new policy: it already refuses the same + absence spelled `undefined` (an omitted `endDate`), and which of the two a + document carries is decided by the record mapping upstream, not by the author. + + Unchanged: a `minDate` / `maxDate` pinned as `null` is falsy, so the caller + discards it and the rows' own range still renders; numeric timestamps and `Date` + instances still render; objectui#6750's empty-list sentinel and every valid + gantt are untouched. +- 85f6a60: Gantt dates are now judged by TYPE: a `string`, a finite `number`, or a `Date` + — anything else is refused (objectui#6781, maintainer ruling 2026-08-30). + + **This is a reject-direction change. Metadata that renders a chart today can + stop rendering one.** `new Date(x)` runs ToPrimitive on anything, so values that + are not dates at all used to become instants silently. These now produce the + same loud diagnostic #6759 and #6770 already use — an alert naming the authored + path and the offending value — instead of a chart: + + | authored gantt date — a row item's `startDate` / `endDate`, or a **truthy** `minDate` / `maxDate` pin | before | after | + | --- | --- | --- | + | `false` | a 649-column axis starting Jan 1970, bar `width: -100%`, no warning | refused, named | + | `true` | the same 1970 axis | refused, named | + | `['2024-01-01']` | drew a normal-looking chart | refused, named | + | `[0]` | drew a chart dated to the **year 2000** | refused, named | + | `{ toString() { return '2024-01-01' } }` | drew a normal-looking chart | refused, named | + | a `bigint` or a `symbol` | threw an uncaught `TypeError` mid-render | refused, named | + + **Falsy pins are not affected, and never were.** A `minDate` / `maxDate` of + `false`, `0` or `''` is discarded by the renderer's existing truthy-only `||` + before anything judges it: such a pin never produced a 1970 axis and it is not + refused now — the chart simply uses the range computed from the rows, exactly as + it did before. The before/after readings above are row-date readings. + + **If your gantt stops drawing after this upgrade, the diagnostic names the exact + authored path** (e.g. `items[0].items[0].endDate`). Fix it at the producer: emit + a date string (`'2024-01-01'`), a millisecond timestamp (`1704067200000`), or a + `Date`. A boolean or an object arriving in a date field means an upstream + mapping picked the wrong column — the chart was drawing 1970 from it before, and + that render was never right. + + **`0` keeps working, deliberately.** It is a legitimate epoch timestamp — under + a millisecond encoding an author who writes `0` means 1970-01-01 — so it is + accepted and renders exactly as it did before. `NaN` and `Infinity` are refused, + as they already were. + + Unchanged for everyone else: valid string / numeric / `Date` dates draw the same + axis and the same bar geometry, an empty gantt keeps its one-bucket sentinel, + and `null` / absent dates keep the identical diagnostic they got before. +- 7fc5c3c: Spell a refused gantt date by a RULE, not by `String` (objectui#6907) + + `spellGanttDateValue` fills the `{{value}}` hole of the unusable-gantt-date + alert, whose job is to name the value the author wrote. Its `String(value)` + fallback was written when nothing but `[object Object]`-shaped values could + reach it; objectui#6905's type rule routes the whole non-date type space + through it, and it failed three ways — measured on `b458300ca`: + + - it **VANISHES**: `endDate: []` rendered "endDate is , which is not a valid + date", the value gone from its own sentence; + - it **LIES**: `['2024-01-01']` read as `2024-01-01` and `[0]` / `0n` as `0` — + a text that IS a valid date, and a number that IS an accepted one (`0` is a + kept gantt date), so the author was told a correct-looking value was invalid + with no hint the wrapper was at fault; + - it **THROWS**: `{ toString() { throw } }`, a throwing `Symbol.toStringTag` + getter and `Object.create(null)` each crashed the render outright. #6759 built + this helper "total by construction" and #6905 made that load-bearing, but the + type gate only stopped `new Date` from throwing — `String(value)` handed + control to author code one line later, so the crash class moved into the + speller instead of going away. + + The rule now recorded on the helper: + + Spell the value when the LANGUAGE owns its spelling. + Name its TYPE when producing text would run AUTHOR code. + + Every primitive keeps a spelling fixed by the grammar, so it is spelled as the + author typed it — including `bigint`, which gains its `n` (`0n`, no longer the + accepted `0`). A `Date` uses `Date.prototype.toString.call`, byte-identical to + `String` but not hijackable by a subclass. Everything else is named: + `an array`, `a function`, `an object`, chosen with `Array.isArray` and `typeof`, + which read no author-controlled property. + + `JSON.stringify` is refuted, not overlooked: it throws on a `bigint` and on a + cycle, which would put the crash class straight back. A bounded rendering is + refused on the same ground — even an element count is not total, because + `Array.isArray` is true of a Proxy whose `length` trap throws. + + Which values are refused is unchanged (that is objectui#6781's ruling), the + `undefined` / `null` / quoted-string spellings pinned by #6759 and #6770 do not + move, the shared inverted-range diagnostic reads identically, and no new i18n + key is added — the article rides in the existing `{{value}}` hole. +- c17446a: Gantt date type gate: judge the `[[DateValue]]` slot, not the prototype chain + (objectui#7027). + + `isGanttDateType` asked `value instanceof Date`, which answers "does this + inherit from `Date.prototype`?" and not "is this a `Date`?". An object that + inherits the prototype without owning the internal slot passed the gate, + reached `new Date(value)`, ran ToPrimitive, and threw + `TypeError: Method Date.prototype.toString called on incompatible receiver` — + uncaught, mid-render, so the author got a blank screen where #6781's named + diagnostic belongs. Three spellings crashed on `main`, measured: + `Object.create(Date.prototype)`, that impostor behind a `Proxy` that throws on + every get, and a `Proxy` with a throwing `getPrototypeOf` trap (`instanceof` is + not total on its own terms either). + + Both sites now ask a total brand test that invokes the builtin + `Date.prototype.getTime` with `.call`: it reads the receiver's `[[DateValue]]` + slot and nothing else, so no author getter runs, no `Symbol.toStringTag` is + consulted, and no proxy trap fires. The two brand tests the finding suggested + were measured and rejected — `Object.prototype.toString.call` performs + `Get(O, @@toStringTag)` unconditionally, and `Number.isFinite(value.getTime())` + calls the author's `getTime`, which would refuse a real `Date` subclass by + dying on it. Both are pinned as red rows. + + No change to which values are accepted: #6781's accept set + (`string | finite number | Date`) is untouched, `new Date(NaN)` still passes + the type gate and is still refused by the parse check with its `Invalid Date` + spelling, and every newly-refused value is one no authored document can carry + (ObjectUI metadata is JSON, which cannot spell a prototype). +- 20c04b2: A gantt timeline whose rows are malformed now refuses to draw, naming the row, + instead of crashing the render (objectui#7164, maintainer ruling A+). + + `TimelineRenderer`'s gantt branch used to read the authored rows twice — once + defensively in `findUnusableGanttDate`, once bare in `calculateDateRange` — and + every input in the gap threw a `TypeError` mid-render from ordinary JSON: + `items: [null]`, a row whose `items` is `5` / `true` / `{}` / an array-like + object, or `items` itself not an array. The three readers (the date scan, the + range computation and the render loop) now consume ONE verdict from + `classifyGanttRows`, and a malformed shape renders the existing `role="alert"` + refusal through a new diagnostic key, + `timeline.gantt.unusableRange.malformedRow` — "items[0] is null, which is not + a row shape" — never the `malformedDate` copy, which named the wrong fault. + The key lands in `en` and the nine sibling locale packs. + + `@object-ui/types` (minor — the accept set narrows): `TimelineSchema.items` no + longer declares `z.array(z.any())`. Every element must be an object, and a + gantt row's own `items`, when present, must be an array, so `validate` refuses + `items: [null]` and `items: [{ items: 5 }]` at authoring time — before they + reach a renderer. Feed items (`vertical` / `horizontal`) carry no `items` key + and parse exactly as before; every in-repo `type: 'timeline'` fixture parses + green on both sides of the change. Rows with no bars (`items: []`, a row + without `items`) stay the ordinary empty state and still draw. +- e75f4c9: `colorField` now means the same thing in the gantt, the calendar and the timeline + (objectui#7243). + + **The inversion this fixes.** `gantt.colorField` is documented as "field that drives the + bar color", and the renderer passed the stored value straight into the bar's + `backgroundColor`. Pointing the key at a select field therefore emitted + `backgroundColor: "open"` — not a colour, so the browser dropped the declaration and + every bar rendered identically. OMITTING the key was strictly better: the absent-key + branch derived a real colour per status. Declaring the documented key was worse than not + declaring it, with no error, warning or console message either way. + + The same key also meant three different things across the three lenses: the timeline + resolved the field's authored option `color`, the calendar hashed the raw value onto a + fixed palette, and the gantt emitted the raw value. An author colouring three views by + one field got three unrelated results, one of which was no colour at all. + + **The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's + resolver, lifted so all three call it: + + 1. the field's own option `color` for the record's value; + 2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`, + `#rrggbbaa`, `rgb(...)`, `hsl(...)`); + 3. else each renderer's own last rung, which is deliberately NOT shared — the gantt + derives a semantic-token hex (a bar must be painted), the calendar keeps its + theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its + default marker. + + **What changes for authors.** A gantt or calendar whose `colorField` points at a select + field with authored option colours now paints those colours. A gantt value that is + neither an option colour nor a colour literal now derives a colour instead of emitting + an invalid CSS value — including a palette NAME (`red`), which now resolves to that + palette's hex, the behaviour the key's own contract has always promised ("hex or + semantic name") and the one `borderColorField` already had. `gantt.borderColorField` + takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps + today's behaviour otherwise and deliberately gains no derivation rung, since the stroke + is opt-in and deriving one for every record would draw an alert on records that have + none. + + Calendars whose `colorField` points at a plain categorical field are unchanged: that + value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline + is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar + already accepted. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 37043cf: `ObjectTimeline` waits for the object definition instead of querying twice + (objectui#7895). + + It was the last member of the set objectui#6482 converged on the shared + settled-schema gate — `ObjectKanban`, `ObjectView`, `ObjectCalendar` and + `ObjectTree` were named there, `ObjectGantt` was ask 2 of objectui#7225 — and + nothing marked it a deliberate exclusion. It still held the object definition in + a local `useState` fed by its own metadata effect, and listed that definition in + the record-fetch effect's dependency array. + + **User-visible.** Every object-bound timeline load issued **two** `find` calls + instead of one: the first before the definition landed, with `buildExpandFields` + seeing no fields and therefore carrying no `$expand` at all, and a second one + after. Whenever the metadata read is the slower of the two — the common case on + a cold metadata cache — the second call is not merely a wasted round trip but a + **three-step paint**: raw foreign-key ids, back to the loading skeleton (the + effect's re-run calls `setLoading(true)` and `loading` is an early return), then + the expanded rows. After this change the timeline paints once, from a query that + already carries its expansion. + + Measured on the component with an instrumented renderer, one mount per hold, + `getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with + `ObjectCalendar` and `ObjectGantt` as positive controls in the same run: before, + 2 `find` calls with expand sets `[null, ['owner']]`, 1 paint at the readiness + predicate and 3 late writes after it at every hold from +3 ms up; after, 1 `find` + carrying `['owner']`, 1 paint, 0 late writes, and a first-paint time that tracks + the hold (8 ms at +3, 30 ms at +25, 105 ms at +100) where before it was a flat + 3-7 ms at every hold. Both controls read 1 paint / 0 late writes before and + after. + + The resolution half is now `useSettledSchema` from `@object-ui/react`, which + settles on **every** exit — no source, no `getObjectSchema`, no object name, and + a read that threw alike. That is what makes the gate safe: the replaced effect + returned without settling on all four, which cost nothing while nothing waited on + it and would have held a gated query open forever. Pinned by + `ObjectTimeline.fetchGate-7895.test.tsx`, including a timeline whose adapter + exposes no `getObjectSchema` and one whose definition read rejects — both still + query, unexpanded. + + Unlike the two sibling conversions, the metadata read is **not** disabled for a + timeline whose items were authored inline: this component also reads the + definition's fields for option colours and field labels on that path, where no + record query is issued at all. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index fc412fe1b9..a33e6a5f42 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-timeline", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Timeline component plugin for Object UI", diff --git a/packages/plugin-tree/CHANGELOG.md b/packages/plugin-tree/CHANGELOG.md index 70c8973ff7..42c1e2380c 100644 --- a/packages/plugin-tree/CHANGELOG.md +++ b/packages/plugin-tree/CHANGELOG.md @@ -1,5 +1,928 @@ # @object-ui/plugin-tree +## 17.7.0 + +### Minor Changes + +- e3d117a: `ObjectGantt` and `ObjectTree` resolve a relationship target only from the two + spellings a contract carries, dropping the third one no contract declares + (objectui#6837, second slice). + + - `ObjectGantt`'s quick-filter option fetch was + `fd?.reference_to ?? fd?.reference ?? fd?.referenceTo`; it is now + `fd?.reference_to ?? fd?.reference`. + - `ObjectTree`'s `detectParentField` was + `def?.reference || def?.reference_to || def?.referenceTo`; it is now + `def?.reference || def?.reference_to`. + + **Accept-set move — a def carrying ONLY `referenceTo` stops resolving a target + at these two seams.** Concretely: the gantt quick filter for that field falls + back to the distinct values present in the loaded rows instead of fetching the + referenced object's full domain, and the tree stops auto-detecting that field as + its parent pointer, so records render as a flat forest unless `parentField` is + configured explicitly. Nothing else changes; the two surviving arms are + untouched. + + Two things bound that move: + + - Any def that entered through the ingestion choke point is unaffected. + `normalizeSchemaReferenceKeys` reads `reference_to ?? reference ?? + referenceTo` and stamps both snake_case keys, so a `referenceTo`-only def + arriving via `MetadataProvider` or `ObjectStackAdapter.getObjectSchema` + already carries `reference_to` before either component sees it. Only a def + that bypassed that door entirely is affected — and that door is not total: + `getObjectSchema` is a required member of the published `DataSource` + interface, and both components call it on the generic `dataSource`. + - No contract declares the deleted spelling. `@objectstack/spec` 17.2.0's + `FieldSchema` refuses `referenceTo` by name with `unrecognized_keys`, carrying + its own "Did you mean `referenceTo` -> `reference`?" rename, and `referenceTo` + is additionally a tombstone in `RETIRED_FIELD_KEY_TOMBSTONES` (objectui#6041) + at all three strip sites, so the designer read door removes it before a draft + round-trips. + + A repo-wide structure-walk producer census found **0** emitters of `referenceTo` + reaching either seam, measured in the cell these components read (a value inside + an object schema's `fields` container) against controls `reference` (92 hits / 36 + files) and `reference_to` (52 / 36) hot in the same pass over the same cells; + the only two in-cell hits are negative fixtures of the retirement machinery, + asserting the read door strips the key. Neither `plugin-gantt` nor `plugin-tree` + emits `referenceTo` anywhere, while both packages' own fixtures are hot on the + surviving spellings. + + Pinned by `ObjectGantt.referenceArms-6837.test.tsx` and + `ObjectTree.referenceArms-6837.test.tsx`, which keep the live arms green beside a + named refusal for the deleted key. +- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone, + dropping the `reference_to` fallback arm (objectui#6837, half 2). + + Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要 + 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front + end just executes the protocol. objectstack#13847 landed the server half — a + `field-reference-to-alias` conversion rewrites stored `reference_to` to + `reference` on the serve path and in `os migrate meta`. + + `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema` + declares. Measured on the installed 17.2.0: it refuses `reference_to`, + `referenceTo` and `target` with `unrecognized_keys`, each carrying its own + "Did you mean -> `reference`?" rename, while a nonsense key gets the same + refusal with NO rename hint and `reference` parses clean. + + ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this + + **This is a behaviour change for BYO consumers, and it is being stated rather + than shipped silently.** ObjectUI is usable without an ObjectStack backend + (`examples/byo-backend-console`), and a hand-written TypeScript schema passes + through no zod door, so nothing rejects the legacy spelling at authoring time. + + **The break surface is narrower than "all BYO consumers", and this is the + measurement rather than a blanket claim.** Two ingestion choke points stamp both + snake_case keys from whichever spelling arrived — `MetadataProvider`'s type + cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any + def that passed either one already carries `reference` and is **completely + unaffected**. What is affected is exactly: + + - **A `DataSource` implementation other than `ObjectStackAdapter`.** + `getObjectSchema` is a required member of the published `DataSource` + interface, and the readers call it on the generic `dataSource` (through + `useSettledSchema` and directly), so a host adapter's object schema reaches + them raw. Every in-repo example of one is on this path: + `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`, + `examples/byo-backend-console/src/mockDataSource.ts`, + `packages/runner/src/lib/mockDataSource.ts`, + `apps/site/app/components/galleryDataSource.ts`, + `apps/console/src/sdui-workbench-preview.tsx`, + `packages/plugin-grid/demo/bulk-actions.tsx`. + + **Measured on this tree, none of those eight emits a relationship target at all** — + `reference_to` and `reference` are both zero in each, and + `examples/byo-backend-console` carries no lookup or master_detail field + anywhere (its only `reference` hits are a vite triple-slash directive and a + tsconfig `references` array). The single in-repo producer that WAS on this + surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer. + + ⇒ **If you author object metadata by hand and spell a lookup's target + `reference_to`, rename that key to `reference`.** Symptom if you do not: the + target silently fails to resolve, and the affected surface degrades rather than + erroring — a related list is not derived, a gantt quick filter falls back to the + distinct values in the loaded rows instead of the referenced object's full + domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a + raw id, a chart's group-by labels stay unresolved. + + The ingestion choke point now emits a **dev-mode warning** when a def arrives + carrying only `reference_to` or `referenceTo` and no `reference`. It names the + object, the field and the offending key, and points at this ruling. Stamping is + deliberately unchanged, so nothing that worked stops working. It is memoised + once per **(object name, field name, spelling, target value)** — every segment + of that key is pinned, in both directions, in + `reference-keys.legacyWarning-6837.test.ts`. + + ⛔ **This warning does NOT cover the break described above, and it is worth being + exact about that rather than letting it read as mitigation.** It lives in + `normalizeFieldReferenceKeys`, reachable only through + `normalizeSchemaReferenceKeys`, which has exactly two production call sites — + `MetadataProvider` (metadata type `object`) and + `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the + warning fires precisely where the def still resolves and nothing is broken. A + hand-written schema served through any OTHER `DataSource` — the break surface — + reaches a reader raw: it never passes through this code and produces **no + warning at all**. On that path the failure is exactly as silent as before. + A reader-side or shared-resolver diagnostic, which would cover it, remains open + on objectui#6837. + + ## What did NOT change + + **Every key these readers EMIT is byte-identical**, and that was verified + mechanically over the whole diff rather than asserted. Eleven of the sixteen + sites write a target onto a bag whose own contract spells it `reference_to` (or + camelCase `referenceTo`): the six whose read and write share a line — + `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`, + `FilterConditionField`, `resolveActionParams` — plus five more that read on one + line and emit on another, and so are just as much emitters: `RecordDetailView`, + `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and + `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the + emitted key is what its target contract declares, and renaming it would be a + separate change. + + **Three readers were deliberately left alone.** `LookupCellRenderer` + (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` — + ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and + never declares `reference`. They are fed by the emitters above and by published + example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so + narrowing them would break in-repo producers, and `plugin-grid`'s + `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly + those three sources — where `reference_to` is recorded with verdict + `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched. +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- b00fcba: `ObjectTree` formats its cells the way the flat table does: a lookup column renders the + referenced record's display name and a select column renders its translated option label, + instead of a raw record id and the raw stored value (objectui#6014). + + Reported against the built-in 业务单元 (`sys_business_unit`) page, whose 「组织架构」 tree tab + showed the manager column as a bare user id and the type column as `department`, while the + flat-table tab on the same page — over the same expanded records — showed the user's name + and 「部门」. + + The card carried its own control, and it pointed at the fetch rather than the formatter. The + tree treated "the host passed inline `data`" as "I do not need the object schema" and skipped + `getObjectSchema`, but its record-fetch branch prefers a live object dataSource over any + inline data. On the one mount shape `ListView` actually uses — `objectName` + a dataSource + + its own pre-fetched `data` — the tree therefore issued its OWN query with + `buildExpandFields(undefined)` → `[]` → no `$expand` at all, and had no field definitions to + format cells from. Both reported symptoms fall out of that single gap, which is why the flat + tab was unaffected and why the tree's existing tests (inline data, no dataSource — a path + that never runs the tree's own fetch) could not see it. + + Three changes, all inside `packages/plugin-tree`: + + - The object schema is fetched whenever the dataSource can serve one, not only when no host + passed inline data. The guard inside the fetch already no-ops without a dataSource, so the + pure inline/static path is unaffected. + - Records are no longer fetched until that schema has settled — settled, not necessarily + successful, so a rejected or inapplicable schema fetch can never block the tree. This also + removes a wasted first query whose lookup columns came back as bare ids and were painted + for a moment before the real query landed. + - Cell values route through a field-aware formatter that delegates both decisions rather than + re-deciding them: option labels through the `translateOptions` seam `ObjectGrid` already + uses for the flat tab (so both tabs read one `fieldOptions.*` i18n key, with the same + exact-then-case-insensitive match and `humanizeLabel` fallback as `SelectCellRenderer`), and + expanded references through `getRecordDisplayName`, the unified display-name resolver + (ADR-0079), with the family judged by `isExpandableFieldType` — the same predicate that + decided what to put in `$expand`. + + No new exports and no new package dependencies: both resolvers were already published from + `@object-ui/core`, and `translateOptions` was already reachable through the `useSafeFieldLabel` + hook this component calls for its column headers. + + One visible consequence beyond the report: an expanded record that comes back with no name-ish + field now reads as ADR-0079's `Record #<id>` floor — the string every other surface shows for + it — rather than as the bare id. +- 9abc02a: `ObjectTree` no longer queries a switched-to object with the previous object's + `$expand` set (objectui#6481). + + The schema-settled gate was two separate pieces of state — the definition + (`objectSchema`) and a bare `schemaSettled` boolean that was set `true` in a + `finally` and never reset. Two independent values cannot express "settled, but + for a DIFFERENT object", so when the host swapped the bound object both effects + re-ran while the latch still read `true` from the previous object's settle and + the definition still held the previous object's fields. The tree issued + + find(newObject, { $filter: …, $expand: [ …previous object's relation fields… ] }) + + — rejected or silently ignored depending on the adapter, plus the transient it + painted — before a correct second query followed. + + `ObjectTree` now adopts `useSettledSchema` from `@object-ui/react` (the shared + resolution hook ruled in objectui#6482), replacing BOTH pieces of state with the + hook's single `{ key, def } | null`. Readiness is derived during render by + comparing the settled key against the currently bound object, so the gate closes + in the same commit that changes the object rather than one commit later — the + stale-key window is not merely fixed but unrepresentable. + + Behaviour that deliberately does NOT change: the gate stays inside the + object-provider branch of the record effect (the inline/static branches issue no + metadata read and must not wait on one), and every exit still settles — no + `getObjectSchema`, no object name, or a rejected read each settle with no + definition, so a tree whose adapter serves no schema still queries instead of + waiting forever. +- 37200ee: Re-key `ObjectTree`'s record-fetch effect onto the primitive fields it + actually reads off `dataConfig` (`provider` / `object` / `items`) instead of + the whole memoised `dataConfig` object (objectui#6700, closing out the + census #6592 opened — the schema-resolution half of this component was + already re-keyed onto `useSettledSchema`'s primitive `schemaKey` by #6696). + + `useMemo` carries no semantic guarantee — React is permitted to discard a + memo cache and recompute even when its dependency array compares equal to + the previous render, and the local `getDataConfig(schema)` helper builds a + fresh `{ provider, object }` / `{ provider, items }` wrapper object on every + call. So the record-fetch effect, keyed on `dataConfig` itself, was correct + only for as long as that identity happened to survive a discard: a + recompute alone (no author or caller action) was enough to re-run the + effect and issue an extra `dataSource.find` call. Keying the effect on the + primitives instead makes a cache discard a no-op, restoring `useMemo` to a + pure optimisation — mirroring the fix already shipped for `ObjectMap` / + `ObjectCalendar` / `ObjectGantt`. + + No behaviour change for a schema whose `useMemo` caches survive normally; + the effect is unaffected by React discarding one. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- ce2aaef: One shared record-source ladder, five plugins delegate (objectui#7632). + + `@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation + of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as + `{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and + `null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader + `resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module. + + That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its + zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939, + maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` — + and it was hand-copied into five plugin components with no gate holding them together. A + change to the ruled order had five edit sites and nothing that noticed a missed one; that + is the AGENTS.md #0.1 drift class. + + **No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the + shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data` + shorthand as a documented head above the shared call and are otherwise unchanged. + `record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse + bodies verbatim and asserts the post-collapse spelling agrees with each across the whole + input matrix, so a later edit to the shared reader that moves any site turns red. + + **Two divergences were measured rather than assumed, and both are preserved.** + + `ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is + the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. + That is a TypeScript narrowing device with no runtime effect — an absent property reads + `undefined`, falsy either way — so the guard could never change which rung is taken. The + shared reader's optional-property parameter accepts the union directly, and the + equivalence is pinned on a fixture that really lacks both keys rather than argued. + + `ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`; + `ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That + is a real divergence on off-contract input — `ViewData` is a discriminated union over object + variants, so an array under `data` cannot be published. It is NOT unified here: the shared + rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627 + left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are + pinned, so neither folding the head in nor deleting it as redundant can happen silently. + + `ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed + parameter. Types are erased at runtime, so nothing it resolves moves. +- 2ce2612: A record-page URL now names the object the clicked rows actually came from, in + `ObjectTree` and `ObjectCalendar` (objectui#7638). + + `useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed, + and both components handed it the bare top-level `schema.objectName` while resolving + their own rows through the objectui#6939 record-source ladder (`data`, then + `staticData`, then `objectName`). objectui#6939 published `objectName` as that ladder's + THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one + record source — and a row fetched through `data.object` whose click built + `/{schema.objectName}/record/{id}` named a record that the URL's own object does not + contain. + + Two shapes change, both toward the object the rows came from: + + - a block carrying **both** bindings navigated to the top-level key and now navigates to + `data.object`; + - a **data-only** block had no name to build a URL from at all, so the hook took its + `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed + record URL. + + `ObjectCalendar` is where the divergence was plainest: on one click it resolved the + detail drawer through the ladder and the navigation URL through the top-level key. The + URL now reuses the very `schemaObjectName` that already keys the calendar's record query + and its `$expand` derivation, so query, drawer and URL agree by construction. + + **Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName` + tail for the off-contract `data: { provider: 'object' }` that carries no `object` + (`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName` + already carries, and the same conservatism objectui#7627 applied when it published the + shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an + `objectName`, and only what callers hand it has changed. + + The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is + why there were copies to convert at all — component authors copied the divergence out of + the documentation, correctly, as written — so it now points at + `resolveRecordSourceObjectName` and says explicitly that a caller with no data config + has nothing above rung three and should keep passing `schema.objectName`. + + `ObjectKanban` is deliberately **not** converted: it has no data config, no + `getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so + `schema.objectName` already IS its record source and its board, drawer and URL already + agree. +- f556a89: `ObjectTree`'s parent-pointer auto-detection accepts a `tree` field only when it is + this object's own (objectui#7839, objectstack#14892 follow-up). + + A hierarchy is parent/child **within one object**, so `@objectstack/spec` refuses a + `tree` field whose `reference` names any other object (`refuseForeignTreeReference`); + `reference` stays optional on a `tree`, where it is a redundant self-annotation. + `detectParentField` returned the first `type: 'tree'` field whatever its `reference` + said, so a foreign-shaped one was silently picked as the parent pointer and the forest + was grouped on a pointer into a table it does not point at. It now mirrors the spec's + own kernel predicate `hasDetectableParentField` term for term — accept when `reference` + is absent, or when it equals the bound `objectName` — and the `objectName` guard comes + with it: an object whose name we do not know cannot be self-referenced. + + Skipping rather than returning also removes a masking bug the old early-return hid: a + foreign `tree` declared before a self-referencing `lookup` used to win by position and + the lookup never got its turn. The `lookup` / `master_detail` arm is otherwise + unchanged. + + Reachability is narrow and stated rather than oversold: the refused shape does not + survive `ObjectSchema.parse` on a spec carrying that rule, so it is unreachable from + parsed metadata and reachable from a hand-built one — `getObjectSchema` is a required + member of the published `DataSource` interface, so a third-party implementation reaches + this reader raw. Note also that the renderer tightens **ahead** of the spec copy this + repo installs: `@objectstack/spec@17.2.0` still accepts the foreign shape at parse + (measured), so until that pin moves this function is the only door. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index 2419150c27..62350af3ec 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-tree", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Tree / tree-grid visualization plugin for Object UI", diff --git a/packages/plugin-view/CHANGELOG.md b/packages/plugin-view/CHANGELOG.md index e0af743c3d..127a6f663c 100644 --- a/packages/plugin-view/CHANGELOG.md +++ b/packages/plugin-view/CHANGELOG.md @@ -1,5 +1,1288 @@ # @object-ui/plugin-view +## 17.7.0 + +### Minor Changes + +- 53dc89d: **Breaking (shipped as `minor` per AGENTS.md §版本号策略).** `ObjectViewProps.views[].sort` + now spells its direction key **`order`**. The retired spelling is **`direction`** — named + here so that a host still writing it can find this entry by searching the old key + (objectui#5293). + + ```diff + <ObjectView + views={[{ + id: 'recent', label: 'Recent', type: 'grid', + - sort: [{ field: 'created_at', direction: 'desc' }], + + sort: [{ field: 'created_at', order: 'desc' }], + }]} + /> + ``` + + **Nothing that worked stops working on this surface, because on the `views` prop + `direction` never worked.** All three consumers of the resolved `activeView.sort` read + `order`: the non-grid fetch lowers it through the shared sink `convertSortToQueryParams`, + whose `entry.order === 'desc'` is false for a missing key; the grid path forwards it to + `ObjectGridSchema.sort`, where `ObjectGrid` builds the wire string `` `${s.field} ${s.order}` `` + — literally `"created_at undefined"` — and `parseSchemaSort` reads a missing `order` as + ascending, so the column header even drew an ascending arrow; `mergedSort` hands the same + value to the delegated list view. + + So a host writing the exact shape the prop declared got an **ascending** list with no + failure signal anywhere: the declaration said the value was well-formed, and the direction + was dropped at three independent readers rather than rejected at one. This rename does not + take away a feature — it converts a silent wrong answer into a loud type error at the one + place that can still be fixed cheaply. + + **Scope — one published export still accepts `direction`, and this release does not retire + it.** `toSortItems` (`packages/plugin-view/src/config/view-config-utils.ts`, re-exported + from the package root and listed in the README) folds `s.order || s.direction || 'asc'`. + It serves a different surface — the studio inspector-draft that feeds `SortBuilder` — and + it is not reachable from the `views` prop, so it neither affects nor is affected by this + rename. If you migrate by searching for the old key, that is the other hit you will find: + it is dormant (nothing in this repo calls it outside a test), and removing it would be a + separate break on a separate public export, tracked as objectui#6011. It is not a partial + retirement of this one. + + `order` is the spelling every other sort surface already uses (`SortConfig`, + `NamedListView.sort`, `ObjectGridSchema.sort` / `.defaultSort`, and the shared + `QuerySortEntry` sink), so the prop now has one spelling repo-wide and declared equals + enforced. + + ⛔ Deliberately **not** a tolerant dual-read (`direction ?? order`): that is the tolerance + layer objectui#4869 ruled against, and admitting the old key as an alias would rebuild the + drift this change removes. `SortUI` is untouched — it legitimately owns `direction` on its + own `SortUISchema` and converts at its boundaries. +- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935). + + Seven modules resolved authored icon names into lucide's runtime `icons` record, each + with its own copy of the logic: **three different tokenisers** (`split('-')` on five of + them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House` + rename on only **four** of the seven. The same authored name therefore rendered on one + surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened + with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`, + and the other six call it. + + **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it + was measured rather than chosen.** Its regression set is empty three independent ways: + against the authored population, against a maximally-pessimistic every-authored-name x + every-surface cross-product, and against a bound-free differential over 8,298 spellings + derived from all 1,767 live record keys — each with a discrimination control that fired + in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface + pairs in that last reading, stripping two surfaces of every snake_case and + space-separated spelling they resolve today. + + **What changes for you — all of it widening, none of it removal.** No name that resolved + before stops resolving: no key of lucide's record contains `_`, whitespace or `-` + (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the + wider one produces the same key. Sixteen name-surface pairs start resolving where they + rendered a fallback or nothing before: + + - `layout_dashboard` and `building_2` (and every other snake_case or space-separated + spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state, + `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and + the related list. + - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried + no rename map. `Home` is not a live record key, so this could only ever be a widening. + + **What does NOT change: what each surface draws when a name does not resolve.** The seam + answers `name -> component`, returning `null`, and decides nothing else (maintainer + ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at + the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning + (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph, + `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar` + and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and + then dropped once the tree was measured to have four such behaviours rather than two: a + lookup function is the wrong place to publish a presentation decision. + + `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface + this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change: + its census goes from seven sites to one, and its normalisation stops being a + widest-common approximation of three disagreeing resolvers — so the under-reporting that + gate disclosed at objectui#5932 is closed rather than merely bounded. +- c5fbe0b: **Breaking (shipped as `minor` per AGENTS.md §版本号策略).** The published `toSortItems` + export now reads its sort direction from **`order`** only. The retired spelling is + **`direction`** — named here so that a host still writing it can find this entry by + searching the old key (objectui#6011). + + ```diff + import { toSortItems } from '@object-ui/plugin-view'; + + - toSortItems([{ field: 'created_at', direction: 'desc' }]); + + toSortItems([{ field: 'created_at', order: 'desc' }]); + ``` + + **What changed, exactly.** `toSortItems` folded `s.order || s.direction || 'asc'`: two + spellings for one key, silently preferring the canonical one. It now folds + `s.order || 'asc'`. Everything else about the helper is unchanged — `id` is still + preserved when present and minted with `crypto.randomUUID()` otherwise, `field` still + defaults to `''`, and a non-array draft still yields `[]`. + + **The failure mode if you do not migrate is silent.** A draft entry spelled + `{ field: 'created_at', direction: 'desc' }` used to produce + `{ field: 'created_at', order: 'desc' }`; it now produces + `{ field: 'created_at', order: 'asc' }` — the documented default for an entry that names + no direction. Nothing throws and nothing warns: the `SortBuilder` row renders, and it + renders **ascending**. If you have a studio inspector draft, a persisted view body, or any + other producer that still writes `direction`, grep for the key and re-spell it to `order`. + + **Why the tolerant read went rather than staying.** objectui#4869 ruled that a spelling the + sink does not recognise gets ruled into the contract or rejected at the producer, never + absorbed by a tolerance layer. objectui#5293 retired the same word on + `ObjectViewProps.views[].sort` and shipped it as a `minor`; this entry finishes the job on + the sort family's public surface, so `order` is now the one spelling repo-wide and declared + equals enforced. The scope note in the objectui#5293 entry — that this export was *not* + retired by that change — described that release's scope correctly and is superseded here. + + `SortUI` is untouched. Its own file-local `toSortItems` is a different symbol, and + `direction` is the key `SortUISchema` legitimately declares. +- b97790a: Seven more `find()` readers now read exactly what `QueryResult` declares — the + `records` arm is removed from each (objectui#6726, following objectui#5945). + + `QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` — + alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`. + `records` is not a member of it. It is the spelling the server envelope and the + client SDK use, which `ObjectStackAdapter.normalizeQueryResult` maps to `data` + before returning — a *below*-the-adapter spelling that had leaked into + above-the-adapter consumers. objectui#5945 removed it from two app-shell + readers; these are the seven the same producer sweep turned up and that card did + not name: + + | module | what it does | + | --- | --- | + | `components/src/hooks/related-count-store.ts` | related-list tab badge count | + | `components/src/renderers/basic/data-list.tsx` | `element:repeater` rows | + | `components/src/renderers/basic/elements.tsx` | `element:number` client-side aggregate | + | `components/src/renderers/basic/record-picker.tsx` | `element:record_picker` options | + | `plugin-detail/src/renderers/record-activity.tsx` | `record:activity` self-fetch | + | `plugin-detail/src/renderers/record-history.tsx` | `record:history` self-fetch | + | `plugin-view/src/ObjectView.tsx` | non-grid (kanban / calendar / gallery / timeline) fetch | + + **One of them was actively wrong, six were dead.** `related-count-store.ts` + read `records` *ahead of* `data` — the precedence inversion objectui#5945 was + filed about — so a `find()` answer carrying both would have been counted from + the key the contract does not declare. The other six read `data` first, so their + `records` arm could never be reached by a conforming producer. A dead tolerant + arm is not harmless: it is where a non-conforming producer keeps working + unrejected, and hardens into a second de-facto contract nobody is checking + (AGENTS.md #0.1). + + **What stops being accepted.** A `find()` answer shaped `{ records: [...] }` + now reads as **no rows** at these seams instead of silently resolving. Every + call site degrades rather than throws: the tab badge counts 0, the repeater and + the picker render their empty state, `element:number` reports 0, the activity + and history feeds render empty, and the non-grid views paint no rows. + + **Nothing produces that shape at this seam today**, which is why this is a + removal rather than a migration. Measured repo-wide over every tracked file: + `ObjectStackAdapter.normalizeQueryResult` CONSUMES the server/SDK `records` + envelope and returns `{ data, total, page, pageSize, hasMore }`; every other + `find()` implementation in the repo (`ApiDataSource`, `ValueDataSource`, the + runner and example mocks, the `@object-ui/types` REST example) returns `data` + or a bare array. The `records` producers that DO exist are on other seams and + are untouched: `ViewDataProvider`'s own `ResolvedData` interface, which declares + `records` legitimately; the raw Cloud HTTP payloads `marketplaceApi.ts` and + `packagedActions.ts` read; and the client-SDK doubles that sit *below* + `normalizeQueryResult`. + + **The bare-array arm is kept** wherever it existed, because it is live: fakes at + these seams answer with a plain array. Each module carries its own pin — + `*.contractEnvelope-6726.*` — asserting the contract read, the live arms, and + the refusal of `records`, so the live and the dead shapes cannot drift into each + other. + + `QueryResult` is **not** widened to bless `records`; that would be a + published-type change and a maintainer decision. +- c18d099: Read `find()` answers as `QueryResult` declares them on two more seams: the + related-count badge store no longer reads `count`, and `ObjectView`'s non-grid + unwrap no longer reads `value` (objectui#6840, following objectui#6726). + + `QueryResult` (`@object-ui/types`) declares exactly one rows member, `data`, and + exactly one count member, `total`. objectui#6726 removed the `records` arm from + seven consumers after measuring that nothing produces it at the + `DataSource.find()` seam, and deliberately left two arms reading *other* + undeclared keys standing in the same expressions — because it had measured + `records` and not them. Its own pin says so in as many words. This is the + measurement it deferred. + + - `related-count-store.ts` dropped `typeof res?.count === 'number' ? res.count`, + which was tried second and *ahead of the contract's `data`* — the same + precedence inversion objectui#5945/#6726 were filed about, on the key those + cards did not measure. The store already asks the server for the count with + `$count: true` and reads it back as `total`, which is a declared member. + - `ObjectView.tsx` dropped the ladder's last branch, + `Array.isArray((results as any).value)`. Unlike the store's arm this was a + pure fallback, not an inversion — `data` was already read first. + + Both keys are the raw-payload spellings that `ObjectStackAdapter.normalizeQueryResult` + and `ApiDataSource.normalizeQueryResult` already fold into `total` / `data` + *below* this seam, so nothing above it emits them. A producer sweep over every + `find()` definition body in the repo (452 bodies / 331 files, bracket-scanned so + a body cannot leak into sibling properties) found `count` emitted **0** times, + against controls `total` (85 hits / 75 files) and `data` (135 hits / 103 files) + drawn from the same cells. Narrowed to the 25 bodies reachable by `ObjectView`, + `value` is emitted **0** times against the same controls (6 and 6). + + No producer changes behaviour, because there is no producer; what changes is + that a non-conforming one is now refused instead of silently absorbed — which + is the point (AGENTS.md #0.1). Each module gets its own refusal pin + (`*.contractEnvelope-6840.*`), and the pins keep the live arms green alongside + the deleted ones, because live and dead is the whole distinction. + + Deliberately not done: `QueryResult` is **not** widened to bless `count` or + `value`. That is a published-type change and the maintainer's call, the same + floor objectui#6726 respected. + + The `value` reading here is **seam-local** and does not transfer: at + `extractRecords` (`@object-ui/core`, objectui#6839) the same key is still LIVE — + five test doubles in plugin-calendar / plugin-kanban emit it today. +- 2a7ac32: Calendar views no longer render on invented field names (objectui#7029; ruled on + objectstack#13748, director batch #19, option A). + + A view that carried no `calendar:` block used to have a complete-looking calendar + configuration synthesized for it. `ObjectCalendar` has always decided whether it + has a usable configuration by asking whether a start-date binding is PRESENT, so + the fabrication short-circuited its own refusal screen — "Calendar configuration + required. Please specify startDateField and titleField." — which existed all + along and was simply unreachable. Measured on a leave-request object whose real + fields are `start_date` / `end_date`: every record piled onto today's cell under + titles resolved through the display-name chain. A plausible, fully wrong screen, + with zero signal to the author. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/ObjectView` emitted `startDateField: 'due_date'` and + `titleField: 'name'` into `options.calendar` for every object view; + - `plugin-list/ListView`'s calendar branch floored the same two bindings at + `'start_date'` / `'end_date'` one layer down; + - `plugin-view/ObjectView.generateViewSchema` — the authored `object-view` + element route, which bypasses `ListView` entirely — carried its own copy. + + All three now forward only what the author declared. This converges the calendar + on the shape its siblings already had: `timelineViewOptions` (objectui#3129 + retired this very literal from the timeline axis), the kanban lane detector + (ADR-0085, "never invents a field the object doesn't have"), and + `defaultCalendarFromObject` (a binding, or nothing). + + **Behaviour change, loud over silent.** With no binding to forward, ADR-0047's + capability gate stops offering the Calendar toggle to views that configured + none, and a view forced onto the calendar renderer reaches the refusal screen + instead of a wrong one. A view that happened to sit on an object carrying a real + `due_date` field was rendering by luck; it now refuses until its `calendar:` + block is written. Correctly configured calendars are unaffected — same fields, + same render. The same deletion also stops the fabricated name from answering for + the Timeline switcher, which accepts a calendar binding as a legitimate axis. + + The spec half — cross-field validation rejecting a half-written declaration at + authoring time — is objectstack#13817. This half makes the runtime honest + independent of which spec version the host pins. +- 5f4514f: Gantt views no longer render on invented date field names (objectui#7070). + + The half PR #7062 fenced out and reported separately. A view that carried no + `gantt:` block used to have a complete-looking date axis synthesized for it: + all three faces floored `startDateField` at `'start_date'` and `endDateField` + at `'end_date'` — field names no view had written and most objects do not + carry. + + `ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props + are present, so the fabricated pair short-circuited the renderer's own refusal + screen — "Gantt configuration required. Please specify startDateField, + endDateField, and titleField." — which existed all along and was simply + unreachable from every route. The same fabrication answered ADR-0047's + capability gate in `ListView.availableViews`, so the Gantt toggle was live on + every object view in the product. + + ⚠️ The premise was MEASURED before anything was deleted, because #7029's + mechanic is only correct where a refusal path exists and that had never been + established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an + absent binding — it does not render empty, and it does not throw. + + Three faces were fabricating, on two independent routes to the same renderer: + + - `app-shell/src/views/ObjectView.tsx` — the console object page. The inline + branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and + `timelineViewOptions`: the declared block spread whole, title floored at + `'name'`, no date field invented. + - `plugin-list/src/ListView.tsx` — the render branch AND the capability gate. + - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored + `object-view` element route, which bypasses `ListView` entirely. + + **What changes for an author.** A view that declared no gantt configuration is + no longer offered the Gantt toggle, and one forced onto the renderer reaches + the refusal screen instead of a plausible, fully wrong chart. A view that + declared a binding is unaffected — the declared block is forwarded exactly as + before, every spec key included. + + Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx` + certified the gantt branch below it as already using the safe two-rung shape. + It did not. The note now states each sibling branch as measured, and says + explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the + two plugin faces, which objectui#7070 routes to a ruling rather than settling + per-face. + + Deliberately out of scope, and left in place: `progressField` / `dependenciesField` + (not date axes, different absent-value semantics) and the timeline `'created_at'` + posture conflict. +- 04a67b9: Retire the `'created_at'` timeline date-axis floors at both plugin faces + (objectui#7070 step ③, maintainer ruling 2026-09-01, 总监批 #28). + + **Breaking, deliberately.** A timeline view that declares **no** date axis anywhere no + longer renders. `ListView`'s and `ObjectView`'s timeline branches used to hand + `ObjectTimeline` a `startDateField` of `'created_at'` for such a view; both now forward + a declared axis or no key at all, and the renderer shows its "declare a date axis" + refusal instead. + + House posture, entered with the ruling: **日期轴永不虚构** — a date axis is never + fabricated. This is the third and last step of a sequence the ruling ordered and forbade + reordering: `ObjectTimeline` gained the refusal screen and lost its own internal + `|| 'date'` floor first (objectui#7459), which by its own measurement changed nothing a + user could see — precisely because these two faces still supplied a name. They are the + supply. + + The floor was not a harmless default. `'created_at'` is a column nearly every object + carries, so downstream it was indistinguishable from a real binding and could never + resolve to nothing — while the `$select` projection is collected from the **declared** + `timeline` / `options.timeline` blocks and never from this prop. An undeclared view was + therefore given a timeline bound to a column the query had not requested, and every + record bucketed into "No date": a screen that looks built, is wrong, and gives the + author no signal. The ruling also explicitly replaced the written decision that stood on + the deleted `ListView` line ("`created_at` stays the last resort for a view that + declares no date axis anywhere") — it was a second, de-facto contract held at one face, + on the very literal objectui#3129 had retired at the app-shell face. + + **Migration.** Declare the axis on the view: `timeline.startDateField` (spec-canonical), + `timeline.dateField` (legacy alias), or a `calendar.startDateField` — objectui#3129 + established that a calendar binding is a legitimate timeline axis, and it still is. All + three keep rendering exactly as before; only the *undeclared* case changes. A view that + really did want records laid out by creation time says so in one key: + `timeline: { startDateField: 'created_at' }`. The refusal names the accepted keys on + screen, so an affected view reports its own fix. + + `titleField` is unaffected and keeps its `'name'` floor at both faces — it is not a date + axis. So do gantt's `progressField` / `dependenciesField`, which the ruling scoped out + for separate evaluation. +- 6712930: The two kanban adapters stop writing the retired `groupField` onto the + `object-kanban` node they generate (objectui#7773). `groupBy` — the key the + renderer actually reads — is unchanged and is now the only lane key emitted. + + **What was measured, on this branch's base (`a915064e`).** Both adapters emitted + the key twice: + + ``` + packages/plugin-view/src/ObjectView.tsx:1362 groupField: groupBy, + packages/plugin-list/src/ListView.tsx:2500 groupField: laneField, + ``` + + The `object-kanban` renderer never read it: `groupField` has ZERO hits anywhere + under `packages/plugin-kanban/`, against a control of thirteen `schema.groupBy` + read sites in `ObjectKanban.tsx` from the same query — so the zero is a reading, + not a blind grep. The write was inert; the board grouped by `groupBy` and + `groupField` rode along unread. + + **Why it is removed rather than tolerated.** objectui#7322 RETIRED + `groupField` on this node on both published faces — `groupField?: never` on the + TypeScript interface and a `retirementTombstone()` in the Zod mirror, which + refuses an authored value BY NAME. So the adapters were producers emitting a + node their own published contract rejects. That was harmless only because a + generated node never reaches the mirror at runtime (`SchemaRenderer` runs the + structural `validateSchema`, not `safeValidateSchema`) — but the CLI's + `os check` / `os validate` DO run the mirror, so the identical node was already + refused when authored by hand and admitted when generated. This closes that + split. + + **What changes for a consumer.** Nothing on any documented path: the renderer's + behaviour is byte-identical, because it never read the key. A host that + registers its own `object-kanban` component and reads `props.schema.groupField` + off the generated node now reads `undefined` — read `groupBy` instead, which + carries the same value and always did. Graded `minor` rather than `patch` for + exactly that narrowing, following the repo convention that objectui's own + breaking changes ship as `minor` (AGENTS.md 版本号策略, mechanically enforced by + `scripts/check-changeset-no-major.mjs`). + + **Who is NOT affected — the boundary is node-local.** Every VIEW-LEVEL + `groupField` read is untouched and still live: it is a legacy alias of the + spec's `groupByField` on the kanban *view config*, mapped by + `normalize-list-view.ts`, and both adapters still resolve lanes through it + (`ObjectView.tsx`'s `kanbanCfg.groupField ||`, `ListView.tsx`'s + `groupByField || groupField`). Authoring `options.kanban.groupField` on a + `list-view` or `object-view` keeps working exactly as documented in + `packages/plugin-list/README.md`. `groupField` is dead only on the generated + `object-kanban` NODE. + + The two tests that pinned the duplicate write are TURNED, not deleted — they now + assert the key is absent, so restoring the write reddens them instead of being + silently re-blessed by a missing assertion. +- 4ef29f0: Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). + + The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. + + Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. + + `@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on + every relative specifier, and a type error in the declaration build now fails the build + instead of being printed and ignored (objectui#5439, objectui#5483). + + **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that + is the fix working.** These packages re-export mostly through NAMED re-exports — + `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the + extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent + `any`. Nothing errored; consumers simply got no types. With the extension emitted, the + symbol carries its real type, and any call site that was relying on the `any` now type + checks for the first time. This is the mode that produced the 21 residual `TS7006` on + `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, + unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced + immediately as `TS2305: has no exported member`. + + 410 extensionless relative specifiers across 19 packages were emitted before this change; + the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. + `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and + is wired so it stays that way. + + The second half changes no emitted output today: 22/22 packages built green unmodified, so + making the declaration step's exit code honest turns nothing red. It changes what a FUTURE + regression does — print and exit 0, versus fail the build. +- 9caa7d4: `ObjectView` wraps `table.defaultSort` before handing it to a delegated list view, so a + view whose only ordering is the deprecated key actually sorts (objectui#6235). + + `ObjectGridSchema.defaultSort` is declared a SINGLE `{ field, order }` object — the zod + mirror agrees (`z.object({ field, order })`, not a union) — while the `list-view` node's + `sort` slot is declared `string | SortConfig[]`, imported by reference from the spec's own + `ListViewSchema`. `mergedSort`'s last branch forwarded the bare object into that slot + unwrapped. The three branches ahead of it all produce an array or a string, so this was the + one shape the slot never declared. + + Nothing crashed and nothing warned: every reader of that slot drops an unparseable sort + silently. `ListView.parseSortConfig` and `ObjectGrid.parseSchemaSort` both open + `typeof sort === 'string' ? [sort] : Array.isArray(sort) ? sort : []`, so a bare object + yields `[]`; the shared sink `convertSortToQueryParams` returns `undefined` for it. Both + in-tree hosts feed the slot straight into `ListView` (`app-shell`'s `fullSchema` and + Studio's `renderStudioGridList`), so the symptom was an unsorted list with no error — + while the SAME metadata sorted correctly as a grid, because `ObjectGrid` performs this + lowering for the same pair. + + The wrap is verbatim the one the non-grid fetch path in this same file already applies + (`|| (schema.table?.defaultSort ? [schema.table.defaultSort] : undefined)`), so all three + consumers now agree and no fourth dialect is introduced. The shared sink is deliberately + NOT widened to accept a bare `{ field, order }`: that is the widening the maintainer ruling + of 2026-08-22 rejected on the merits, because the same slot legitimately carries + `$orderby`'s own `Record<field, direction>` map, in which `{ field: 'desc' }` is a legal + ordering by a column literally named `field`. + + Precedence is unchanged — a named view's sort still outranks `table.sort`, which still + outranks `table.defaultSort`. Only the final branch changes shape. + + One behaviour note for hosts writing off-schema metadata: an ARRAY in `table.defaultSort` + was previously forwarded verbatim by this path alone and is now lowered like every other + resolver in the repo, which leaves it unreadable rather than rescuing it. That input is + already refused by the zod mirror and already behaves this way on the fetch path and in + `ObjectGrid`; the canonical slot for an array is `table.sort`. +- e929c56: `ObjectView`'s non-grid fetch now carries `$expand` (objectui#6419). The effect built its + expand set from `objectSchemaRef.current` — a ref assigned in the render body, deliberately + kept out of the effect's dependency list so the effect would run exactly once per mount. On + that one run the ref was still `null`, so `buildExpandFields` saw no fields and the query + went out as `{ $top: 100 }` with no `$expand` at all; because the effect never re-ran on the + schema's arrival, it never went out with one either. + + `ObjectView` hands the rows it fetches to the child view as `data={data}`, which suppresses + that child's own fetch. So every lookup / master_detail / user / tree field in the six + non-grid views it hosts — kanban, calendar, gallery, timeline, gantt, map — rendered from + raw foreign-key ids: blank on the kanban (its `resolveDisplay` suppresses opaque ids) and + potentially the raw id on the other five. + + The object schema and the fact that its read has SETTLED are now one piece of state, keyed + by object name, and the record query waits on it — the shape `ObjectKanban` adopted in + objectui#6271. The gate is on the read having settled, **not** on a truthy schema: a view + whose adapter exposes no `getObjectSchema`, or whose read threw, still queries (unexpanded) + rather than waiting forever, and switching objects closes the gate in the same commit rather + than sending the previous object's expand set. + + The trade was measured on this effect rather than inherited, because it has five more + dependencies than the board's. With an instrumented adapter (schema and `find` both 30ms) + across four host regimes: before, one query with no `$expand` and one raw delivery to the + child; with `objectSchema` merely added to the dependency list, two queries and two + deliveries — `raw` then `expanded`, a visible two-step paint, because here the raw rows + settle into state *before* the re-run's cleanup rather than being discarded as they were on + the board; gated, one query carrying `$expand` the first time and a single expanded + delivery, with correct rows landing at the same wall clock as the dependency version. +- 6a7893d: `ObjectView`'s non-grid fetch no longer re-queries once per parent render when the host + passes an inline `views` array (objectui#6460). + + The effect that fetches rows for the six non-grid view types (kanban, calendar, gallery, + timeline, gantt, map) listed `activeView` — an **element of the `views` prop array** — among + its dependencies. A host writing `views={[{ id: 'cal', type: 'calendar', label: … }]}`, which + is how this component's own docs write it, produces a fresh element object on every one of + its own renders, so the dependency changed identity every render and a new `find()` went out + each time. Measured with an instrumented adapter and three parent re-renders: **4 queries + where a hoisted array gives 1**. Because `ObjectView` hands its rows to the child view as + `data={data}`, each extra query also re-delivered a fresh row array downstream — the + "duplicate events in child views like the calendar" hazard, from the re-run direction. + + The effect now depends on the **values it reads** — the active view's `filter` and `sort`, + plus its `id` — held at a steady reference while they are structurally unchanged, instead of + on the view object's identity. Asking hosts to hoist the array was considered and rejected: + that is a contract change on every caller of a published component, and it leaves the defect + live for every host that does not comply. + + Nothing about precedence moves: a named `listViews` config's `filter`/`sort` still outrank + the view's, which still outrank `table.filter`/`table.sort` and their deprecated aliases. + Changing a view's filter, changing its sort, and switching the active view all still + re-fetch. The comparison never serializes, so it stays correct for filter and sort values + that have no faithful stringification — a `Date`, a function, a `Map`, `NaN`, or plain + key-order instability — and every case it cannot model resolves to "changed", which costs a + redundant query rather than withholding a needed one. +- 34ea56d: Relay a per-view `rowColor` through the two object-view hosts (objectui#7218). + + `rowColor` is a declared member of `ListViewSchema` — imported by reference from + `@objectstack/spec`, shape `{ field, colors? }` — and `ListView` reads it to + seed its `rowColorConfig` state, which colours whole rows from the named field's + value. Neither object-view host relayed it: `app-shell`'s + `ObjectView.renderListView` builds its list schema by spreading the host's and + then relaying 47 named keys off the active view, and `plugin-view`'s + `ObjectView` assembles 46 inside its `object-view HOST-COMPOSITION SURFACE` + fence. `rowColor` had a rung in neither. + + So an authored per-view row colour was unreachable on the object route: + authored, validated, built and served correctly, then dropped at the relay. + Nothing errored and every authoring gate passed — the only symptom was that the + rows were not coloured, which an author cannot notice short of diffing the DOM. + Same "declared and inert" shape objectui#7199 fixed for `description`. + + **This is a relay, not a new surface.** The interface route + (`InterfaceListPage.tsx`) has shipped `rowColor: view.rowColor` next to + `grouping` and `pagination` since ADR-0047, into a schema typed + `ListViewSchema`, with no fence of any kind — so the key was already + author-reachable and already had a delivery path; two of three hosts simply did + not use it. The legacy shorthand for the same feature (bare `color`) already had + a rung in both literals; only the spec-canonical spelling was missing. + + **No published surface moves.** Both rungs are view-sourced only, and neither + adds a cast read off the object-view node — that would have added a 28th name to + the objectui#5097 HOST-COMPOSITION exemption whose count the 2026-08-18 ruling + fixed at 27, which is a ruling and not a refactor. `grouping` is the in-fence + precedent for a view-only rung. + + ⚠️ Not `userActions.rowColor`, a boolean permission toggle sharing this name at + a different nesting level ("may the user open the colour panel" versus "what the + colours are"). That key is untouched, and the new pins hold the two apart. +- 7c3df8f: The settled-schema convergence, and the gantt's duplicate query gated + (objectui#7225, maintainer ruling B, 2026-09-02). + + `useSettledSchema` was extracted and published in PR #6690 with exactly **one** + non-test adopter (`ObjectTree`, the component that had an actual defect — + objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and + `ObjectCalendar` kept their own hand copies of the same shape, so a published + export was owed compatibility forever **and** the duplication it was named for + stayed. All three now call the hook. + + The migration is a pure deduplication with no behaviour delta — the hook was + extracted *from* these three shapes, so each becomes a one-line call. + `ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the + recipe the hook's own doc comment prescribes for it by name: pass the data + source as `undefined` for a render that must not read metadata + (`hasInlineData ? undefined : dataSource`), so "inline value data set" is + expressed as "there is no source to read from" rather than as a second enable + flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and + what made the calendar's obstacle a non-obstacle: it was about the gate half. + + **One observable change:** `ObjectKanban`'s rejected definition read now logs + on `console.error` with a `[useSettledSchema]` prefix instead of + `console.warn`. Its test spy moves with it, and now asserts on the channel + rather than merely silencing it. + + **The gantt's duplicate query is gated** (ask 2 of the card; #6482's + undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s + dependency list, so every load issued two unbounded queries — the first with no + `$expand` at all. Measured on this component across three latency profiles, the + cost is not the mild "round trip bought and thrown away": when the metadata + read is the slower of the two, which is the common case on a cold + `MetadataCache`, the user sees the full three-step paint — raw foreign-key ids, + back to the loading placeholder, then the expanded rows. It now issues one + query, already expanded. + + Gating the gantt required its schema resolution to settle on EVERY exit + (objectui#7232): the hand-rolled effect returned without settling on + `!effectiveDataSource`, on `!resource` and in its `catch` — harmless while + nothing waited on it, and a chart that never loads once something does. + `useSettledSchema` settles on all three by construction, which is what makes + the gate safe; both exits are pinned. + + ⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's + separate ruling, in its own commit on the same branch. +- d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields` + call sites (objectui#7429). + + objectui#7215 / PR #7229 gated the two projection sites in its scope + (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more + (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This + closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView` + (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`, + `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch. + + **All seven pass no column list at all**, which makes every one of them the + sharp shape: `buildExpandFields` reads an absent column list as "no column + restriction" and falls back to **every declared relation on the object**, + denied ones included. So each of these components asked the server to resolve + the object's full relation set by default, not by configuration — the + ordinary shape of each surface, not a corner of it. + + **`PagePreview` is the one site where the judged principal is not the page's + eventual audience.** It calls the browser's own `fetch` with + `credentials: 'include'` rather than `DataSource.find`, so it runs under + whichever session is loading the Studio preview. Gating on that same session's + `usePermissions()` is still the correct principal: it is exactly the request + the browser is about to make, on its own credentials, regardless of who later + opens the published page. + + **Reproduced before it was fixed**, as a failing test per site (and, for the + two sites — `ObjectView`, `PagePreview` — where the gate was implemented + before its test was run red, a reverse-verification: the gate was reverted, + all four denial-and-set pins on each went red, and the two deferral/positive + control pins stayed green, before the gate was restored). + + **Grading, measured rather than assumed** — the same reading objectui#6898, + #7215 and #7230 recorded: against ObjectStack's own server this is + defence-in-depth, not a live disclosure. `plugin-security`'s + `FieldMasker.maskRecord` deletes every unreadable key from each returned row + and objectql's expand path writes the resolved record back under that same + key, so one statement removes the expanded object and the bare id alike; the + expansion sub-read is itself gated (the referenced object's full CRUD + RLS + + FLS treatment, objectstack#7626). It is load-bearing for any backend that does + not strip, and the client-request side is real regardless. + + **Nothing a permitted view did stops working.** The gate judges each site's + `buildExpandFields` OUTPUT, which contains only the object's declared + reference-bearing fields, so the "`checkField` answers false for an + undeclared key" trap cannot be reached. An unanswered permission policy + filters nothing. `buildExpandFields` itself is unchanged. + + `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`, + `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth + one objectui#7429's own dependency count missed (it named four); `plugin-list` + and `app-shell` already had it. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- cad512f: A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s + view switcher instead of the grid one, and the `tree` / `chart` view types are + recorded as host-composition-only surfaces (objectui#5321). + + `viewSwitcherSchema`'s `iconMap` carried an entry for every view type except + `tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn + with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by + adding a single key — nothing recorded that the map had to be COMPLETE, so the + next missing member went unnoticed. The map is now typed + `Record<ViewType, string>`, which is how `ViewSwitcher`'s own + `DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: + a future `ViewType` member fails `type-check` rather than silently rendering as + a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph + `DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback + stays for host props that carry an unrecognised type. Reached in practice by + the console, whose `CreateViewDialog` offers `tree` among the view types a user + can create. + + No authoring surface changes. `generateViewSchema` renders eight view types + while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of + them, so `tree` and `chart` are selectable only through the component's `views` + prop. The maintainer ruled on 2026-08-20 that both stay recorded as + host-composition-only rather than being added to those unions, following the + objectui#5097 precedent; the record now lives beside that one, with the branch + set derived from a source fence, the authored unions pinned at the type level, + and host reachability measured. +- cfc2c7a: **Bug fix:** a non-grid `object-view` (calendar / kanban / gallery / timeline) + whose sort came from the deprecated `table.defaultSort` no longer comes back + empty. `ObjectGridSchema.defaultSort` is declared a single `{ field, order }` + object, and `ObjectView`'s own fetch handed it to `$orderby` verbatim — where + the ObjectStack adapter reads it as an `$orderby` map and folds it with + `Object.entries`, so the request went out sorting by two columns literally + named `field` and `order`. The server answers `400 INVALID_SORT`, `ObjectView` + swallows the error, and the view rendered with no records — while the *same* + metadata sorted correctly as a grid, because `ObjectGrid` lowers that pair + before using it (objectui#4869, maintainer ruling 2026-08-22). + + `ObjectView` now performs the same legacy-to-canonical lowering `ObjectGrid` + already does (`sort ?? (defaultSort ? [defaultSort] : undefined)`) and routes + the whole chain — named view sort, `views` prop sort, `table.sort`, + `table.defaultSort` — through the shared `convertSortToQueryParams` sink. This + was the last object-bound read site sending an authored sort to `$orderby` + unlowered; every other block (gantt, map, calendar, timeline, + `record:line_items`) already did, so an adapter that implements `find` itself + now receives one normalized `Record<field, direction>` from all of them instead + of two different dialects. + + Precedence is unchanged, and both spellings of the pair keep working. The + shared sink was deliberately **not** widened to accept a bare `{ field, order }`: + its input slot also legitimately carries `$orderby`'s own map, in which + `{ field: 'desc' }` is a valid ordering by a column named `field`, so widening + it would make one shared function guess. + + Two visible shape changes on the wire, both semantically identical to before: a + string `table.sort` such as `'name desc'` now serializes as `-name` rather than + riding through as `name desc`, and a `SortConfig[]` arrives as a map rather than + as an array. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 6b348d2: `ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both + icon maps in the package now name only spellings lucide still resolves + (objectui#5586). + + `ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up + in lucide's runtime `icons` record. lucide retires a spelling by dropping it + from that record while KEEPING it as a deprecated named export, so a retired + name still imports, still type-checks and still renders as a component — and + silently resolves to nothing as a string. `ObjectView` composes the switcher + from names, and two of them had been retired on lucide-react 1.31.0: + `chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a + label with no icon at all while every sibling type had one, and nothing went red + because no lucide symbol appears in that map for the compiler to check. Measured + against the installed package: `BarChart3` and `GanttChart` are absent from + `icons`, while `ChartColumn` and `ChartGantt` are present. + + - `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`, + `gantt-chart` → `chart-gantt`. + - `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named + deprecated aliases move to the names the record carries — + `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`, + `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the + aliases already pointed at, so those two glyphs are unchanged; the `gantt` + default picks up the plain gantt glyph, which is what `iconMap` now supplies + for that view type. + + The regression pin widens from `tree` alone to EVERY name both maps supply: a + pin scoped to the two names that broke would not have caught this and would not + catch the next lucide bump. +- Updated dependencies [9c74902] +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [062943f] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [67a87d9] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [3e853c9] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [17ccec9] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [beccf1c] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [7a72422] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [5173a5e] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [129bcc5] +- Updated dependencies [c9a7252] +- Updated dependencies [5f19b92] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [e0b289d] +- Updated dependencies [46f0bb4] +- Updated dependencies [2da6441] +- Updated dependencies [3b9c774] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [f8c70f4] +- Updated dependencies [5d3a2d1] +- Updated dependencies [c38162d] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [1c19722] +- Updated dependencies [5961030] +- Updated dependencies [faa863d] +- Updated dependencies [fd814d6] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [c4987fb] +- Updated dependencies [f55d666] +- Updated dependencies [f241a4d] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [969ba84] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [4ac3769] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [19f3637] +- Updated dependencies [9bd08fe] +- Updated dependencies [45ac2cb] +- Updated dependencies [b97790a] +- Updated dependencies [dbd5194] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [fab4802] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [f9984c0] +- Updated dependencies [9700dd9] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [1349400] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [78ca238] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [84ffdbc] +- Updated dependencies [a276480] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [67dadd6] +- Updated dependencies [e21308e] +- Updated dependencies [a4514e8] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [669d71b] +- Updated dependencies [2d3fe73] +- Updated dependencies [ed27d7c] +- Updated dependencies [7dedec6] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [7bf244b] +- Updated dependencies [f0bb9fa] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [3c9fca3] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [20e317c] +- Updated dependencies [425762e] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [591bf27] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [43ca9d5] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [ba306e3] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [5d79faf] +- Updated dependencies [30266cf] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/plugin-form@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/permissions@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index e34a92e5e8..3a348226a2 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-view", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Object View plugin for Object UI", diff --git a/packages/providers/CHANGELOG.md b/packages/providers/CHANGELOG.md index 06d4ccf643..f6b68ddf38 100644 --- a/packages/providers/CHANGELOG.md +++ b/packages/providers/CHANGELOG.md @@ -1,5 +1,210 @@ # @object-ui/providers — Changelog +## 17.7.0 + +### Minor Changes + +- c9327c9: Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. + + Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): + + - `Typography` — the shape lives on as the inline `Theme['typography']` member. + - `BorderRadius` — lives on as inline `Theme['borderRadius']`. + - `Shadow` — lives on as inline `Theme['shadows']`. + - `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. + + Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). + + The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. + + `@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). + +### Patch Changes + +- a1c41c5: `@object-ui/providers` no longer declares `@objectstack/spec` as a dependency. Nothing + in the package imports it, so consumers stop installing it on this package's account + (objectui#5753). + + The edge was live for exactly one release cycle. It was promoted from `devDependencies` + to `dependencies` when `ThemePreference` was derived from the spec's `ThemeMode` union, + because the package's public `.d.ts` then referenced the spec. objectui#5716 re-pointed + that derivation at `@object-ui/types` (`ThemeMode` / `THEME_MODES`), which removed the + last three import sites — `src/types.ts` and the two retirement-era test files — and + left the declaration behind with no reader. + + Re-measured on `origin/main` at `ad0f5f11f` before removal, with a positive control so + the empty result is a real absence rather than a broken command: the import-shaped grep + (`from` / `require(` / `import(` / `vi.mock(` against `@objectstack/spec`, bare name and + every subpath) returns **0** hits under `packages/providers/` and **434** across + `packages/` + `apps/` — same command, same invocation. The only surviving mentions in + the package are the declaration itself, immutable `CHANGELOG.md` history, and a prose + comment in `tsconfig.test.json` that this change corrects. + + No API change and no behaviour change: `dist/types.d.ts` imports only `react` and + `@object-ui/types`, and no emitted file references a spec symbol. Consumers on an + isolated `node_modules` (pnpm) never had supported access to the spec through this + package, so nothing they could legitimately import goes away — the change is to the + install graph only, which is why it is scored `patch` rather than `minor`. +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [d2fb6ef] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [d6ceb8d] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [6bca0e4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/providers/package.json b/packages/providers/package.json index 19c97f85de..685d681d26 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/providers", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Reusable context providers for ObjectUI applications", diff --git a/packages/react-runtime/CHANGELOG.md b/packages/react-runtime/CHANGELOG.md index 761adb0e2e..c092a21a84 100644 --- a/packages/react-runtime/CHANGELOG.md +++ b/packages/react-runtime/CHANGELOG.md @@ -1,5 +1,55 @@ # @object-ui/react-runtime +## 17.7.0 + +### Patch Changes + +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. + ## 17.6.0 ## 17.5.0 diff --git a/packages/react-runtime/package.json b/packages/react-runtime/package.json index 2c6f1aa917..ab3d5410e2 100644 --- a/packages/react-runtime/package.json +++ b/packages/react-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react-runtime", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index c6d1af825c..fae80966d7 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,1667 @@ # @object-ui/react +## 17.7.0 + +### Minor Changes + +- 1b1d772: Expression-bindable text keys: `statistic.value`, `card.title`, `button.label` + and their siblings now evaluate `${...}` on the node (objectui#4795 Direction 1, + maintainer ruling 2026-08-25). + + **What changes for you.** Four text keys — `title`, `label`, `value`, + `description` — can now carry an expression written directly on the component + node, on the component types that declare them: + + | Component | Bindable node keys | + |---|---| + | `statistic` | `label`, `value`, `description` | + | `card` | `title`, `description` | + | `button` | `label` | + + ```json + { "type": "statistic", "label": "Active users", "value": "${data.metrics.active}" } + ``` + + That node used to render the literal text `${data.metrics.active}`. A dashboard + `statistic` previously had no way at all to bind a dynamic number — the + documented workaround (moving the key under `props`) evaluated the value and + then discarded it, painting a blank card instead. Both shapes are fixed by the + same change: the value is evaluated once, at the single place that produces + evaluated schema, and lands where the renderers already read. + + **No component behaviour changed.** `statistic.tsx`, `card.tsx` and `button.tsx` + are untouched — they always read these keys off the node; nothing was writing an + evaluated value there. + + **Scope, and how it grows.** The list is closed and lives in + `@objectstack/spec` (`EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT`); the renderer + reads that declaration rather than keeping a copy. On any other component type + these four keys are still read raw, so an expression reaches the screen as + literal text — notably `text`, whose `value` is read but has no declaration. + Adding a type or a key is a change to the spec, never something the renderer + infers. + + **Nothing is newly rejected.** This release only widens what evaluates; no + metadata that used to render now fails to. The build-time rejection of `${...}` + in undeclared keys — the second half of the same ruling — is not in this release + and is still open. + + Published authoring guidance updated to match: `skills/objectui/rules/protocol.md` + (new "Bindable Text Keys" rule), plus the `page-builder`, `schema-expressions` + and `data-integration` guides, which taught the now-retired "never evaluated" + statement and its host-pre-resolution workaround. +- 30c73cd: A `disabled` / `disabledOn` predicate that cannot be evaluated is now reported on the console, in development **and** in production — and the message says what this gate's fail-soft default actually did. + + `SchemaRenderer` routes six visibility legs (`visibleWhen`, `visible`, `visibleOn`, `visibility`, `hidden`, `hiddenOn`) through one reporter, and called `evaluateCondition` **bare** on the two enablement legs — the only uninstrumented predicate pair in the file. A faulting `disabled` predicate had therefore never been reported in any build, in any dialect that does not report on its own. + + It is also the pair whose fail-soft answer **bites**. `evaluateCondition` answers an unevaluable predicate with `true`; on the negated visibility legs that means SHOWN, here it means GREYED OUT. So the user got a control they could see and could not use, and the author got nothing to grep for. + + - **Wiring only, one engine call.** Both legs pass `EvaluationOptions.onFault` (objectui#6038's seam), which hands back the fault the evaluator has already caught. No `throwOnError`, no second evaluation, no `__DEV__` split — dev and production print identical bytes. + - **No verdict moves.** The fail-soft `true` is preserved deliberately: a faulting `disabled` predicate still disables, exactly as before. Flipping that is a shipped-behaviour change and is not part of this. + - **Its own copy, not the visibility reporter's.** The shipped line says the safe default meant the gate "did NOT bite", which is written about a gate that shows the node. This gate's line says the opposite, because the opposite is true: `[ObjectUI] An enablement predicate could not be evaluated`, then the node, the key, the source, the engine's reason, and that the node renders disabled — on screen, greyed out — with nothing else on screen to say a predicate failed. One reporter, one dedupe, one severity; a second message. + - **Rate-limited exactly as the visibility gate is**, per `(node type, key, predicate source)`: two hundred rows of one broken predicate print one line, a second distinct source still prints, and the same source authored on `disabled` and on `visibleWhen` prints two — the gates cannot silence each other. +- 830ed58: The record page's approval band offers its **Recall** button to the approval's submitter + only (objectui#6464). + + Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval; + user B — not the submitter, read access, not an admin — opens the record and the band still + lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes + on submitter identity and refuses everyone else, so the only outcome available to that + button was a failure toast. Record state was never at risk; this was purely a + writability-feedback mismatch, the same family as objectui#3794. + + The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at + all" — which is a question about the DataSource, not about the viewer. Identity now joins + it, threaded the way every other signal on that band already travels: the HOST resolves it + and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and + never re-derives who submitted what. + + - `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on + `InlineEditContextValue`. Additive and optional; no existing prop changes. + - `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a + resolved `false`. + - `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing + approvals read and threads it. + + **The signal is tri-state, and the third state is the load-bearing one.** `true` offers + recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity + — renders exactly as it did before this release.** Omission preserving prior behaviour + mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide" + would have traded a cosmetic defect for a functional loss: every host whose band runs off + the record's `approval_status` mirror alone would silently lose its submitter's only way to + unlock their own record. + + **Withdrawn rather than disabled-with-reason.** The card offered either. For a + non-submitter this control is never actionable on any pending record, so a permanently + disabled button is standing clutter rather than a lesson; and the two sibling submitter + levers already hide — the approvals panel's Remind button, and the declared + `approval_recall` action's `visible` predicate. The band, its quorum tally and the + approvals timeline still tell a non-submitter exactly what state the record is in. Only the + lever they can never pull is gone. + + **This changes no permission.** Nothing about what the server allows moves, `canEdit` and + the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an + authorization verdict — the recall endpoint remains the sole authority, and it refused + these callers before this change and refuses them after. There is deliberately **no admin + carve-out** (the reporter ruled that case out, cf. objectstack#9464). + + The derivation itself is now one function, `isSubmitterOf` — server-resolved + `viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends + that predate it, joined with `??` so a server that resolved `false` is believed rather than + re-litigated client-side. The approvals panel's Remind gate, which already carried that + expression inline and whose behaviour is unchanged, now reads the same answer: two copies + would have been two definitions of who submitted. + + The **untranslated refusal text** the reporter also saw ("No pending approval request found + for this record", concatenated after a localized prefix) is a separate defect and is not + addressed here; it is tracked on objectstack#11993. +- c71e14d: **The unresolvable-visibility-predicate report now names the roots of the tier the + predicate was actually evaluated against** (objectui#6487). An app-shell author + whose nav, area or field `visible` faulted was told to check `record` and + `page.<var>` — two roots that tier does not bind at all. + + `formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate` + (both exported from `@object-ui/react`) take a new **optional sixth argument**, a + `PredicateScopeTier` — also exported — selecting the closing advice paragraph. + Everything above that paragraph is unchanged on every surface, and so is every + verdict: this is diagnostics copy only. + + **The published signature grew; nothing existing breaks.** The argument defaults + to `'page-component'`, so a five-argument call keeps printing the bytes it + printed before. All three in-repo call sites pass their tier explicitly rather + than lean on that default. + + Each tier's root set was derived from the code that builds the bag, not from the + prose that described it: + + - **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item + predicates. Both bind `record`, `current_user` and `page.<var>` (the roots + `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is + byte-for-byte what it was. + - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` + runs, wired onto this reporter by objectui#6443. Its evaluator is built from + `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so + the line now names `current_user` with its three ADR-0068 alias spellings, + `app`, and `features` — the deployment-flag root that provider documents for + exactly this kind of predicate — and states outright that `record` and + `page.<var>` do not exist there. + + **Why not generalise the copy instead.** Dropping the concrete root names would + have made one paragraph true everywhere at the cost of making it useful nowhere: + an author who mistyped a root needs to know which roots exist *at their tier*, + which is the whole reason the paragraph is read. + + `data` is bound at the app-shell tier but is deliberately not advertised there — + every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it, + so naming it would point an author at a root that answers nothing. +- 748494b: The unresolvable-predicate diagnostic tells `hidden` / `hiddenOn` authors what actually + happened to their node (objectui#6503). Those two legs are the only ones in + `SchemaRenderer`'s visibility chain whose verdict is NOT negated: `evaluateCondition` + answers an unevaluable predicate with `true` on every path, the four negated legs turn that + into SHOWN, and these two return it as-is, so the same `true` sets `_hidden` and the + component returns `null`. Both were handed the consequence paragraph written for the negated + legs — "the gate did NOT bite - a predicate that cannot be evaluated reads on screen exactly + like one that said yes" — which is the opposite of what an author whose block VANISHED is + looking at. That line exists to name their predicate, and instead it sent them to hunt a + rendering bug that does not exist. + + The two legs now print their own paragraph: the safe default is the one that BITES here, the + node was REMOVED and is not on the page at all, an absent node is indistinguishable from + metadata that meant to hide it, and nothing is wrong with the renderer. + + Copy only — no verdict moved. The node still vanishes, which is the shipped fail-soft the + neighbouring family (objectui#3862 / #3955 / #6443 / #6487 / #6445) preserved deliberately, + and every case in the new suite pins the verdict beside the sentence. + + `PredicateGateKind` — re-exported from `@object-ui/react`'s entry — gains a third member, + `'concealment'`, joining `'visibility'` and `'enablement'`. The opening line is deliberately + UNCHANGED (`UNRESOLVABLE_VISIBILITY_PREFIX`): these are visibility predicates, and an app or + a test filtering the console by that constant must go on catching them. Consumers that + switch EXHAUSTIVELY over the union, or key a `Record` by it, gain a third case to answer; + no runtime signature moved and every value accepted before still is. +- dc4365c: Retire the discovery-wire preview mode — the console no longer turns + authentication off because a server said `mode: 'preview'` (objectui#6654). + + `@objectstack/spec` retired the `RuntimeMode` value `'preview'` and the whole + `PreviewModeConfig` block (objectstack#11846). This console still read that + surface back off the runtime discovery payload, which is a different layer from + the retired compile-time type — so the consumption could not simply be assumed + dead, and its removal was ruled deliberately (2026-08-29). + + - `ConditionalAuthWrapper` (`@object-ui/app-shell`) drops the branch gated on + `discovery.mode === 'preview'`. That branch called `setAuthEnabled(false)` and + simulated an identity out of `discovery.previewMode`, every field behind a + default. Auth availability is now decided **only** by the ADR-0076 D12 service + reading (`isServiceUsable(discovery.services.auth)`), exactly as for any other + mode. + - `DiscoveryInfo` (`@object-ui/react`) drops the `previewMode` block and stops + documenting `'preview'` as a runtime mode; the package README's discovery + section is updated to match. + + **Accepted failure direction:** a deployment that still emits `mode: 'preview'` + or a `previewMode` block now falls back to the ordinary auth reading — it + requires login. That is loud, diagnosable and more secure than keeping a dormant + auth-off path keyed on a spelling the platform no longer produces. + + **Not affected:** `AuthProvider`'s `previewMode` prop, `useAuth().previewMode` + and `PreviewBanner` in `@object-ui/auth` are a separate published capability + with a different producer (a host passing the prop). Only the discovery-wire + producer of that prop is retired; hosts that pass it explicitly are unchanged. +- e321d52: `useSchemaPersistence` refuses to save a schema carrying function-valued keys + (objectui#6658). + + The designer save door handed the schema straight to `JSON.stringify`, which + does not preserve a function value and says nothing about it: an object key is + **dropped**, an array element is **coerced to `null`**. No throw, no warning. + `save()` + resolved with the id, `error` stayed null and `lastSavedAt` advanced, so every + observable signal reported success while the stored schema had quietly lost the + handler. The failure ordering was the bad one: loss at save, symptom at render + or click, arbitrarily later, in a different component, with no link back. + + Per the 2026-08-29 maintainer ruling on objectui#6658, the door now refuses such + a save instead of performing a lossy one: + + - `save()` walks the schema for function-valued keys **anywhere** in it — + nested objects and array elements included — before serialization. + - On a hit it sets `error`, returns `null`, leaves `lastSavedAt` and `isDirty` + untouched, and never reaches the adapter. The message names the exact + offending key paths (`columns[2].cell`, `toolbar.actions.onExport`) and both + escapes: strip the callables before saving, or use the declarative form. + - The guard sits at the **hook layer**, not inside the default localStorage + adapter, so a host-injected or REST adapter is covered too — the documented + REST adapter has the identical `JSON.stringify` shape, so a host following + the docs inherited the behaviour rather than escaping it. + + True lowering (objectstack's server-side `lowerCallables`) is recorded as + unavailable at a browser door: functions cannot round-trip storage without a + code registry, so refusal is the honest shape. + + Behaviour change for hosts that previously saved callable-bearing schemas — + those saves were already losing the callables and now fail loudly instead. + Fully declarative schemas are unaffected and store byte-identically to before. + No published type or signature changed. +- 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read + it, so the html tier stops reporting the one working saved-view spelling as + `unknown-prop` (objectui#6678). + + `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is + the one spelling that resolves a saved view for an object-bound block. It works, + and it drew the identical `unknown-prop` warning as the two spellings that do + nothing (`viewName`, `view`), because `validateTree` looks a prop up in the + block's declared `inputs` and no registration declared this key. On the tier + built to accept AI-authored pages, where the diagnostic IS the contract, the + only signal pointed away from the key that works. + + Adopting the maintainer ruling of 2026-08-29 — option B **in the injection + form**: + + - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core` + beside the binding's own semantics; `Registry.register` emits it for any + registration whose renderer passed through the new `elementDataSourceBlock()` + seam. One mechanism, one copy — not a hand-kept declaration per block, which is + the shape that drifts and that a new block forgets. The seam lives in + `@object-ui/core` and is re-exported by `@object-ui/react` beside + `ElementDataSourceGate` for discoverability; call sites take the core import, + because a registration runs at module scope and this repo's suites partially + mock `@object-ui/react`. + - Seventeen renderers, in thirteen files across twelve packages, reach the seam + and now publish the key to the save gate, the parser whitelist, the generated + JSX authoring types and the block list. The card named nine blocks; the tree + also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and + `element:record_picker` — nothing was hand-listed, so the mechanism covered + them. `element:record_picker` consumes the gate's HOOK and status panels rather + than the wrapper tag (its object lives under `properties`), and was found by a + render probe rather than by reading sources. + - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports + `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for + exactly this reason — that set mirrors `BaseSchema`, and silencing the key + everywhere would make the diagnostic lie in the other direction. + - New `check:element-data-source-declaration` fails any source that consumes the + gate without reaching the seam, so a block added tomorrow cannot forget. + + Behaviour of the binding itself is unchanged — this is a declaration, not a + resolution change. The saved view still resolves its columns, and an + unresolvable `view` still fails loudly rather than widening to the object's full + scope. + + The spec/registry parity gates (repo-wide and the `record:related_list` per-block + pin) now derive their accepted set from the WHOLE node contract rather than from + `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps + `dataSource` on a page-component node — it is a node-level key, a sibling of + `type` and `className`, not a per-block prop — so the gates' previous complaint + was measurably wrong. Derived from the spec, not exempted, and both still + discriminate against an invented key. +- 26896c6: `element:*` renderers stop re-reading a degenerate config bag as its own character indices — the third and last channel of the objectui#6752 / objectui#6760 hazard (objectui#6783). + + Five modules under `packages/components/src/renderers/basic/` — `elements.tsx`, `data-list.tsx`, `text-input.tsx`, `record-picker.tsx`, `metadata-viewer.tsx` — each carried a copy of the same reader, `{ ...(schema?.props ?? {}), ...(schema?.properties ?? {}) }`. `??` only replaces `null`/`undefined`, so a non-object bag went into the object spread and came back out as indexed keys: for `properties: 'not-a-bag'`, the config bag a renderer received was `{ '0': 'n', '1': 'o', … '8': 'g' }` — nine keys nobody authored. The five copies are now one `readProps` (`renderers/basic/readProps.ts`) that asks `isConfigBag`, and a degenerate bag on either side contributes no keys. + + `@object-ui/react` exports `isConfigBag` from its package entry. That is the API addition here, and it is the reason the fix is not a sixth spelling of the predicate: objectui#6761 converged six occurrences of "is this a real config bag?" behind one definition in `packages/react/src/utils/configBag.ts` and pinned it, but the pin scans `packages/react/src` only — a copy written one package over would be invisible to it. `@object-ui/components` already depends on `@object-ui/react` (all five modules import from it today), so the reachable answer was to publish the definition rather than retell it. Same reason the node-gate predicate reporter is exported at that entry (objectui#6038): one definition, read by every package that asks. + + **What this does not change, measured rather than predicted.** No rendered output moves on today's tree. All five renderers read named keys off this bag, and the single onward spread — `metadata-viewer`'s `<StateMachineView {...props} />` — hands it to components that destructure named fields, so the indexed keys were computed and then dropped. The census behind objectui#6708 found zero authored nodes carrying a degenerate config bag, so this was a latent shape, not a live failure. What the guard buys is what objectui#6752 measured its own guard buys, one channel further down: the authored value's shape is not reinterpreted. objectui#5123's precedence is untouched — `properties` still wins a contested key, and a degenerate bag declares no key for either side to win. +- b87f15b: `useOffline` auto-syncs mutations queued while already online (objectui#6818). + + The auto-sync effect was keyed `[isOnline, enabled]` with + `react-hooks/exhaustive-deps` suppressed, so its `queue.length === 0` guard was + evaluated against the queue as it stood when `isOnline` or `enabled` last + changed. `queueMutation` has never been conditional on being offline — it + accepts entries whenever the hook is enabled — so anything queued while ALREADY + online found the effect asleep, and nothing re-ran it. Only an explicit `sync()` + drained those mutations; the hook whose job is auto-sync did nothing for them. + + The suppression's stated reason ("only trigger on `isOnline` changes, not on + every queue change") was about TIMER RESTARTS, and it is kept: the effect is + keyed on the **boolean** `queue.length > 0`, never on `queue` or `queue.length`, + so queueing a second mutation while the 100ms stabilization timer is already + armed still does not re-run the effect or restart the timer. What the + suppression never justified — the early return against a stale snapshot — is + what changed. + + `sync` also read `batchSize` through a ref (newest) while reading `queue` from + its own closure (a snapshot), so the two halves of one call disagreed about how + current they were, and the auto-sync effect retains such a closure by design. + The queue now reaches `sync` through the same commit-phase mirror the sync + config uses, so both halves are the newest committed values. That also takes + `queue` out of `sync`'s dependency list: `sync` is keyed `[enabled]` and is + stable across queued mutations, which is what lets the effect name every value + it reads and drop the `eslint-disable` entirely rather than reword it. + + **Behaviour change, graded `minor` deliberately.** `useOffline` is published and + its out-of-repo population is unmeasured; the single in-repo caller + (`AppHeader`) destructures `isOnline` only and is unaffected. A consumer that + called `queueMutation` while online and relied on nothing being sent until it + called `sync()` itself will now see that mutation flushed ~100ms later. + `sync`'s identity is also more stable than before — it no longer changes on + every queued mutation — which is safe for effects keyed on it but is a visible + difference. + + Not changed here: a `batchSize` smaller than the queue still drains one batch + and leaves the remainder for the next transition, because whether one auto-sync + should chain batches until the queue is empty is a separate question about what + `batchSize` means, not about this guard. Filed as objectui#6857. +- 03380aa: Inline edit: a rejected save now says WHICH field the server refused, and why. + + Editing a record in place on a detail page and hitting Save used to surface the + backend's own string when the write was refused — `VALIDATION_FAILED: + Validation failed for crm_opportunity` — leaving the user to guess which of the + fields they had just edited was the problem. The refusal has always been + field-scoped (`@objectstack/objectql`'s validators throw `VALIDATION_FAILED` + with `fields[]`, and both the REST layer and the runtime dispatcher pass those + entries through intact); the inline surface was the last one still dropping + them. `<InlineEditSaveBar>` now renders one reason per rejected field, named by + that field's own label — the same treatment record forms have had since #3222. + + Attribution never guesses. It reads the envelope through + `@object-ui/react`'s `extractFieldErrors`, the single in-repo normaliser the + form surface already uses, and an entry with no usable `field` is dropped + rather than pinned on whichever input is nearby. In the drawer's callback mode, + where persistence loops `onFieldSave(field, value)` one key at a time, a + rejection is attributed to the key that was in flight — a fact about the write, + not an inference. Anything that is not field-scoped (a network failure, a + permission denial) keeps the cleaned single-line message it had before. + + `@object-ui/react` gains one additive public API member to carry this, and it + is the reason that package's entry is `minor` rather than `patch`: + `InlineEditContextValue` now has **`fieldErrors`** — a nullable map of field + machine name to the server's reason — alongside a **`setFieldErrors`** setter, + the exact companions of the `error` / `setError` pair that interface already + carried. Nothing is removed and nothing changes shape, so every existing host + and consumer compiles and behaves as before; a host that never reads the new + member sees no difference. It exists because the save bar and the field rows + are SIBLINGS under `InlineEditProvider` in both persistence modes, so before + this there was no channel between the component that receives a refusal and the + components that render the fields it is about. + + Also recorded in code, per the maintainer's ruling on objectui#6868: **the + server is the validation authority on the inline-edit surface.** That was + previously an absence — `InlineFieldInput` runs no rules and takes no `error` + prop — and it is now a decision, written into both modules' headers with a + pointer to the ruling. No client-side rule evaluator was added, and none should + be: the server is the only rule source, and this surface only presents it. +- 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is + never silent (objectui#7210, maintainer ruling a′, 2026-09-02). + + Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each + issued a `find` with **no `$top` at all**, so the request returned the entire + filtered result set. At the 186 rows the card was filed from that is invisible; + on an object with 100k scheduled rows it is the whole table into the browser, + and nothing an author could write — `pagination.pageSize` included — could + bound a request that never carried a cap to begin with. + + **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP` + rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was + larger they render a footnote naming both numbers, verbatim as it renders: + *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the + ceiling nothing changes: the full set draws and no footnote appears. + + The four view packages take a **minor**, not a patch: a result set above the + ceiling is no longer drawn in full, which is a behaviour break whatever the + fixed group does to the released version number. + + **The ceiling is a platform constant, not an authorable key** — `2000`, exported + from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or + `dataSource: { limit }` still does not reach these queries, by the same ruling; + three alternatives were rejected with it (a documentation note only — still the + whole table; truncating at `pageSize` — silent, and a complete schedule capped + at one page; an authorable `maxRows` — a new permanent key every author sets). + + **Why 2,000.** One constant for all four, so the binding view sets it. Measured + in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows + grow (virtualised task list; four events per day cell; auto-clustering above + 100 markers), while `ObjectTree` flattens every expanded node into the document + at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows + is where the worst of the four lands at ~10,400 elements — an order of + magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the + real application result set this card came from. + + New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`, + `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote` + and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the + footnote copy in all ten packs. +- b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which + object is this block bound to" (objectui#7627). + + Six view plugins each spelled that resolution locally — `ObjectCalendar` twice, + `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three + wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in + dataConfig` test. They now delegate to one function that states the published + objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once. + + **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim + into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its + post-collapse spelling across the whole contract-valid input matrix — both bindings + present, data only, `objectName` only, empty `objectName`, empty `data.object`, the + `api` / `value` / `staticData` / array-shorthand providers, and nothing bound. + + **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477, + ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers + how `objectName` gets POPULATED when absent, where an already-present `objectName` wins. + The new reader answers which object a block RESOLVES, where the `data` block wins — the + order declared on both published faces in `@object-ui/types` and pinned by + `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override + one standing ruling or the other. + + **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted + order, with an in-place comment saying why: its receiver is a localStorage key + (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every + saved layout and filter-chip set of a view carrying both bindings. Two more sites the + finding listed are not object-name readers at all and were struck: `ObjectGantt`'s + refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider` + type-guard over a widget's `data`. + + `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four + lines that taught it, so the copies cannot re-seed from the hook that replaced them. +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. +- ae61ad4: The console form now renders a refusal message the producer explicitly marked + as user-facing, instead of always substituting a generic string + (objectui#5210). + + An application's hook guards could not talk to their users. When a hook refused + a write with 403, the form replaced the server's text with + `form.noPermissionToSave` unconditionally — the recorded objectstack#3821 fix, + which exists because a raw refusal body puts untranslated platform diagnostics + (`FORBIDDEN: insufficient privileges to update showcase_private_note + pi-TgoJ4_DM55Fqz`) in front of end users. The external report behind this change + had 11 guards whose deliberate, localized guidance — which role owns the action, + whom to ask — never reached anyone, and named the incentive that creates: + returning 400 instead of 403 for permission failures, degrading the status + semantics logs, monitoring and API consumers depend on. + + The maintainer ruling (2026-08-19) was a producer-side opt-in rather than a + chattier 403 branch, and the platform half shipped as objectstack#9934: a hook + marks its refusal text with `userMessage` at throw time. This is the consumer + half. + + - `@object-ui/react` gains `declaredUserMessage(err)` — the one "is this + marked?" read. It answers the marking verbatim, from the two places the + adapter boundary parks the envelope (the error itself, where + `@objectstack/client` lifts it, and `details`), and `null` for everything + else. + - The form prefers a marked message over both its generic strings, on ANY + status — the marking is status-agnostic; 403 is where this was reported, not + a fence the contract draws. + + **Unmarked refusals are unchanged**: a 403 with no marking still shows the + generic `form.noPermissionToSave`, and the raw text still goes to the browser + console only. objectstack#3821's protection is preserved by construction, not + by re-guessing which 403 bodies are presentable — the mark and the marked text + are one field, so no boundary that rewraps or substitutes `message` can promote + platform prose into the user-facing channel, and platform code never sets it. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- cac64b3: The spec-bridge's form-view input now accepts everything `@objectstack/spec`'s + FormViewSchema accepts — three key types had drifted narrower, and one of them + had inverted (objectui#5652). + + `bridges/form-view.ts` held a third hand-written description of the FormViewSchema + contract, after the leaf (objectui#5542) and the two containers (objectui#5596) + were converged elsewhere. A description nothing compares is one spec release from + being a fork, and this one had already drifted on the keys that decide whether a + legal document renders: + + - `FormSection.columns` refused the string spelling of a column count, which the + contract admits and folds to a number in its own pipe. It is now the contract's + type, and the bridge performs the fold — the `object-form` node's section + declares `columns` as a number and its container indexes a grid-class map by + it, so forwarding `'2'` handed every downstream renderer a value outside the + type it declares. + - `FormField.dependsOn` was declared `string[]`, which is the exact inverse of the + contract: it admitted only the array arm the contract rejects, and refused the + bare parent-field name that is the one configuration making `field-selector` + and `dynamic-config` work (objectui#5040). + - `visibleWhen`, on both fields and sections, was declared `string`, so the + ADR-0089 expression object — the arm `evalFieldPredicate` reads — could not be + described at all. Both arms now travel whole onto the node. + + A section's `fields` may also be a bare object-field name, the spec shorthand the + list bridge already honours. The form bridge ran it through the object mapper + instead, producing a field with no identity (`{ name: undefined }`) for the most + ordinary section a form can declare; it is now forwarded verbatim, which is what + the node's own `fields` slot admits. + + Each drift-prone key's type is now bound to the `@objectstack/spec` symbol that + owns it rather than restated, so it cannot drift again, and the compile-time pins + in `FormViewWidenedArms.test.ts` fail if a future edit restates any of them by + hand. The documented subset itself is unchanged: the keys this bridge does not + declare, including the retirement ledger it keeps, stay exactly as they were. +- bf28341: refactor(spec-bridge): retire the spec-bridge — `SpecBridge`, `bridgeListView`, `bridgeFormView` (#6366) + + Removed under the 2026-08-27 maintainer ruling on #6366 (Option A — remove, and + for `SpecBridge` as a WHOLE, not just the form half), on the same criteria the + 17.0.0 entry used to remove `bridgePage` / `bridgeDashboard`: no runtime + consumer, and a route that could not deliver a working result even in + principle. The stage-1 measurement (2026-08-26, PM-verified) the ruling rests + on: + + - **Zero consumers at every reachable endpoint** — no non-test caller in this + repo, none in the sibling `objectstack` repo (which does not depend on + `@object-ui/react` at all), and a GitHub-wide public code search returning + hits only inside this repository. External npm-private hosts are + unmeasurable from here; the ruling records fallback C (keep + document the + boundary) should evidence of one ever surface. + - **The bridged form route was structurally unable to work**: a bridged node + carries neither `objectName` nor `customFields`, so `ObjectForm` takes the + branch its own code labels "cannot proceed" and renders, in the registry + wrapper's words, "a field-less card in silence". The only working host + recipe (`{...node, objectName: '...'} as any`) existed solely inside the + list bridge's integration tests and was documented nowhere. + + This knowingly reverses the 17.0.0 changelog line "The `list` and `form` + bridges are unaffected and remain the live authoring path" — by maintainer + ruling, on the same measurement criteria that entry itself applied. + + #5898's restored-key work on the form-view bridge (the + `FormViewSpecConformance` suite and the spec keys it carried onto the bridged + node) is **superseded by this removal, not fixed** — the route it repaired is + gone. #6366's measured type-vocabulary asymmetry (a bridged field carrying + `text` where the normalizer produces `field:text`) is likewise mooted rather + than repaired. + + The suites pinning the removed route go with it — retirement of a route + nothing travels, not quarantine: the seven suites under + `react/src/spec-bridge/__tests__/`, and plugin-grid's + `specBridgeColumnSpelling` / `specBridgeExportFormats` render-integration + suites. `ObjectGrid`'s own column-spelling, export-gate and density behavior + keep their non-bridge pins (`columnDeclaredSpellingOnly`, `exportGate`, + `exportServer`, `rowHeightOffSpecBoundary`, and core's + `normalize-list-view` suite). + + BREAKING CHANGE: the public exports `SpecBridge`, `bridgeListView`, + `bridgeFormView` and the types `BridgeContext` / `BridgeFn` / `ObjectDefLite` + are removed from `@object-ui/react`. There is no replacement translation + layer — author `object-grid` / `object-form` nodes directly (the live path is + `app-shell`'s `ObjectView`, which builds them from the object's own metadata), + exactly as the 17.0.0 entry already directed for pages and dashboards. +- 83fe6e7: row predicates on runtime record surfaces resolve `record.*` only; the bare-field and `data.*` spellings are no longer bound + + Phase 2 of the row-predicate canon (objectui#5330, ruled 2026-08-20, option B; + Phase 2 ruled 2026-09-02 and amended 2026-09-05 on objectui#5741). Until now a + row predicate — `visible` / `disabled` / `enabled` on an action renderer, a row + action, a `record:alert`, a `page:header` action, a conditional-formatting + `condition` — bound the row three ways: canonical `record.status`, bare + `status`, and `data.status`. The two non-canonical spellings are retired on + every runtime record surface, in both evaluation tiers (`evalRowPredicate` / + `partitionRowsByPredicate` in `@object-ui/core`; `usePredicateRecordContext` + + `useCondition` in `@object-ui/react`) and for both dialects: a legacy + `${data.x}` / `${x}` string on a row surface retires with the CEL spellings. + + **What a retired spelling does now: it faults, exactly as it already did on the + server** (`buildScope({ record })` mounts exactly `['record']`, so `status` and + `data` are unknown variables there), and each surface applies its EXISTING + fault policy — no runtime detector, no "treat as absent" special case, no + uniform override: + + - `evalRowPredicate` / `partitionRowsByPredicate` (row kebab, selection bar, + `page:header` actions, conditional formatting): the caller's `fallback` — + hidden / every row excluded / no style — reported once by the existing fault + warning, which names the unknown variable (`Unknown variable: status`) and, + on the fast route, carries the `record.` hint. + - `useCondition` legs that opt into `throwOnError` (`action:button` and + `action:menu` `visible`, `DeclaredActionsBar` `visible`): fail-closed — + hidden on every row, reported once as `was hidden/disabled: its predicate + threw — status is not defined`. + - the non-throwing `useCondition` legs (`action:icon` / `action:group` + `visible`, every `disabled` / `enabled`, `record:alert`): fail-soft — shown / + greyed / enabled on every row, with the evaluator's own console line. + - a host scope that carries its OWN `data` (app-shell's ambient `data: {}`) is + left standing: `data.*` on a record surface then reads the host's object — a + constant, silent `false` — which is what "no longer bound to the row" means. + + The Phase-1 deprecation warning is removed with the bindings: + `warnNonCanonicalRowSpelling` and `resetRowPredicateCanonWarnings` are no + longer exported from `@object-ui/core`. `detectNonCanonicalRowSpelling`, + `ROW_PREDICATE_CANONICAL_ROOT` and the `NonCanonicalRowSpelling` type stay + exported — the offline instrument for sweeping authored metadata. + + The layer rule is unchanged: `data` remains the canonical root on + metadata-editing surfaces (ADR-0089 D3, `CANONICAL_ROOT_BY_LAYER`), and + app-shell's metadata-admin `SchemaForm` / `predicate.ts` keep binding + `{ data: row }` through their own evaluator. + + No stored-metadata survey, export or migration rewrite was run (the maintainer + ruled the stored population out of scope, 「不考虑存量」); the Phase-1 warning + period was the notice. + + Release note: Phase 1 (PR #5737 — the canon statement plus the warning) shipped + in `@object-ui/core@17.6.0` (npm, 2026-08-24) although its changeset + `.changeset/row-predicate-record-canon-5330.md` is still pending on `main`, so + the next CHANGELOG section lists Phase 1 and this Phase 2 together: the warning + it describes was live from 17.6.0 and is gone from this release on. +- d1ab06f: Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now + warn once, and are unchanged otherwise. + + A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row + scope, a `record:alert`) has bound the row three ways since objectui#4075 — + `record.status`, bare `status`, and `data.status` — without any of them being + declared the contract. The maintainer ruled that question on 2026-08-20 + (objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for + the identical renderer-tolerance shape: **the canon is `record.*`**, and the + other two enter a deprecation window. + + The canon states the **server's** accept set, which was this card's first + measurement and turns out to be strictly narrower than the renderer's. Measured + against `@objectstack/formula@17.1.0`, the engine the server evaluates with: + + | spelling | server runtime | server authoring oracle | + |---|---|---| + | `record.status` | `{ ok: true, value: true }` | accepted | + | bare `status` | `Unknown variable: status` | refused | + | `data.status` | `Unknown variable: data` | **silently accepted** | + + `buildScope({ record })` mounts exactly `['record']` — `data` is never bound and + the row's fields are never flattened to top level. The three-way binding is a + client tolerance with no server counterpart, which is why the warning belongs on + this side. + + `data.*` is the dangerous one, and the reason the warning exists. `data` is in + `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle + waves it through — that list is a deliberately generous "never faults" lint + baseline, not the runtime accept set. A `data.*` row predicate therefore passes + every authoring gate the platform has and then binds nothing at runtime: not an + error, a constant `false`. A `visible` that is constantly false is a button that + silently never appears — the objectui#4075 fail-closed signature. + + What ships: + + - `@object-ui/core` exports `detectNonCanonicalRowSpelling`, + `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and + `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, + which carries the canon statement and the measurement. + - Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and + `useCondition` (react, for bags bound by `usePredicateRecordContext`). + - Detection reuses the server's own oracles (`collectCelRootIdentifiers`, + `firstUndeclaredReference`) rather than a regex, so no second dialect + judgement is invented client-side. + + **No spelling is removed and no behaviour changes.** Every predicate that + resolved before resolves now — the ruling defers removal behind a stored-metadata + survey, and the warning is what makes that survey possible (ADR-0078: a + tolerance nothing ever reports can never be retired). + + The deprecation is scoped to the **runtime record layer**. `data` remains the + canonical root one layer over, in a metadata-editing form (ADR-0089 D3 + `CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. +- dba7d84: New export: `useSettledSchema` — the settled-schema RESOLUTION half shared by + `ObjectKanban` / `ObjectView` / `ObjectCalendar`'s fetch-gate hand copies + (objectui#6482, maintainer ruling Option A). It tracks whether an object's + definition has finished resolving FOR THE KEY THE CURRENT RENDER IS ASKING + ABOUT, returning `{ ready, def }` from one piece of internal state so `ready` + and `def` can never be observed inconsistently and a stale key can never read + as ready — the structural fix for the `ObjectTree` defect (objectui#6481) + where a definition and a separate, one-way-latched "settled" boolean could + disagree for a render after the object changed. + + Gate PLACEMENT — which effect branch actually waits on `ready` — stays a + per-component decision and is not part of this hook; see the hook's own doc + comment. Existing hand copies are migrated on their own subsequent cards, not + by this change. +- c86185e: Bind `record` into the node-level visibility evaluator, and stop a hoisted + `properties.visible` swallowing a declared `visibleWhen`. + + `@objectstack/spec` has declared since ADR-0089 that a page component's + `visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`, + `current_user`, `page.<var>`"*. `SchemaRenderer` bound no `record` at all. Its + evaluator was built from the ambient predicate scope, `data: dataSource` (the + connector **adapter**, not the row) and `page: pageVariables`; the row lives in + `RecordContext`, which that evaluator never read. + + Because the surface is fail-soft, a `record.*` predicate did not misfire — it + resolved to **shown**. Both polarities of the same predicate returned the same + verdict, so a visibility gate silently did not gate, on every block on every + record page. Measured on `record:alert`, `record:path`, `page:card` and + `element:text`. + + Three changes, all in `SchemaRenderer`'s evaluation memo: + + - **`record` is bound**, as the `record` root only — the three roots the + describe promises and nothing more. Not as bare fields, and never over + `data`, which is what `${data.*}` in a props bag resolves against. Bound + conditionally, so "no row" binds nothing rather than shadowing a `record` a + host supplied through the ambient scope. + - **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` + onto the node, so a node carrying `properties.visible` short-circuited the + declared node predicate — the one key the spec tells authors to write was the + one key that could be silently ignored. The two deprecated aliases + (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into + `visibleWhen` at parse, so a spec-parsed page never reaches them. + - **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this + predicate is broken" and "this predicate said yes" with the same word. The + verdict is unchanged on every path — `evaluateCondition` already returned + `true` for every unevaluable predicate, including the non-negated `hidden` / + `hiddenOn` legs where that `true` means HIDE — so only the silence moved. + + **Behaviour change, stated plainly:** a shipped page whose node-level + `record.*` predicate was previously inert now evaluates. A block that was + permanently visible may begin to hide — which is the point, but it is a verdict + change, not a no-op. `properties.visible` is unaffected in verdict: an + in-tree census found **zero** node-level `record.*` predicates on page + components, so nothing in this repository changes verdict. + +### Patch Changes + +- 546ddf7: A node-gate visibility predicate that FAULTS now says so in a production build, once per + distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the + silence is no longer an accepted property"). Observability only — no verdict moves. + + `SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated + resolves to the same answer as one that said yes, so a gate that stops biting looks + exactly like a gate the author got right. The diagnostic that names it (objectui#5454 / + objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection + channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition` + implements that by evaluating **twice** — too expensive to ship for every predicate of + every node. + + **What production actually printed before, measured per dialect on the built evaluator** + — the card's premise held for one dialect of three, and the other two failed in opposite + directions: + + | dialect | production console, before | + |---|---| + | bare string | **nothing** | + | `{ dialect: 'cel' }` envelope | one generic line, deduped per source | + | `${…}` template | one generic line **per evaluation**, never deduped | + + So the dialect objectstack#11254 measured a live gate breaking on was the silent one, + while the template dialect was the console flood the ruling's rate-limit clause exists to + prevent. + + **The fix reports the fault the evaluator already detected, at the same number of engine + calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s + `ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the + canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer + up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason + passback), and supplying it transfers reporting to the caller so one fault stays one + line. Pinned: the CEL branch performs the same number of record reads with the passback + as without it, and strictly fewer than the `throwOnError` probe. + + `SchemaRenderer` passes it in production and reports through the **same** reporter the dev + branch uses — same message, same severity, same dedupe `Set`, same key. Development and + production now print the identical line for the identical fault; the `__DEV__` gate no + longer decides *whether* a fault is reported, only *how* it is detected. + + `page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same + reporter and the same rate limit. It swallowed the identical fault under a different + helper, and it was the worse of the two: the node gate at least reported in development, + while a faulting item predicate was silent in *both* builds on a gate whose false verdict + removes an entire tab, header and panel. + + **Rate limit:** deduped per (node type, gate key, predicate source) — never per render and + never per node instance. A two-hundred-row list of one broken predicate is one line; a + second distinct predicate source still gets its own line. Both halves are pinned, because + a test that asserts only "a warning was emitted" is equally green on an implementation + that emitted fifty, and one that asserts only "exactly one" is equally green on an + implementation that suppresses everything. + + **Not changed by this card, deliberately:** the fail-open semantics themselves; the + objectui#5687 adapter-only `data.*` report, which stays development-only under its own + 2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against + the wrong object); and the `/forms/:name` scope wiring of objectui#6262, which lands in + its own PR. + + `reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`, + `UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported + from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one + reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one + authored predicate would be entitled to one line per package instead of one line. +- 22ba927: `SchemaRenderer` now hands a stable `schema` object identity to a node carrying + `responsiveStyles` (objectui#6270). + + ADR-0065 scoped styles make a styled node take a branch that rebuilds the schema + object to merge the generated scope class into `className`. That rebuild was not + memoised, so it allocated a new object on **every** `SchemaRenderer` render — even + when the `evaluatedSchema` memo directly above it held. Every downstream renderer + that memoises on `[schema]` therefore saw a fresh identity and re-ran: concretely + `ObjectMap`'s `dataConfig` and `mapConfig`, and the whole marker cascade below them + (`markers` → `filteredMarkers` → `clusteredData` / `markerBounds` → `initialViewState`). + Only nodes on that branch were affected — a plain node was already handed the + memoised `evaluatedSchema` itself. + + The trigger is narrower than "has `responsiveStyles`" reads: it needs one of the four + sized breakpoint keys (`large` / `medium` / `small` / `xsmall`). A `{ base: … }` shape + never took the branch and was never affected. + + The scoped-style computation now lives in a `useMemo` keyed on + `[evaluatedSchema, autoStyleId]`, hoisted above the renderer's early returns — its old + use site sits after them, so a memo written there would have been a conditional hook. + A genuinely changed `className`, interpolated value or breakpoint still produces a new + identity, so nothing goes stale. +- 5967be0: Extends objectui#5687's adapter-only `data.*` constant-predicate diagnostic — a node-gate + predicate that evaluates perfectly, against the wrong object, because at the node tier + `data` is the data-source adapter, not the row — to the `disabled` / `disabledOn` gate + (objectui#6504, maintainer ruling 2026-08-27 option A). + + A node written `{ "type": "button", "disabled": "data.status == 'locked'" }` evaluates + cleanly (no fault, so objectui#6445's fault reporter correctly stays silent), and on the + constant's other polarity (`data.locked == null`, `!data.assignee`, or an adapter that + answers nothing) hands the gate a constant `true` that greys the control out on every row, + in every build, with nothing on the console. This leg now names it, in development only — + option C (always-on) was excluded, outside the #5687 precedent. + + The copy is new, not reused: the visibility leg's sentence ("a constant `false` hides the + node on every row") is written about the opposite polarity and would be false on this gate. + The enablement leg's own sentence names the constant-`true` direction — the control renders + DISABLED, greyed out, indistinguishable from a gate the author meant to close. + + Both legs carry the same dissolution pointer: this diagnostic — visibility AND enablement + together — dissolves when objectui#5330's `data.*` deprecation window closes. + + Dev-only, no verdict change, no interpolation change, no published type widened (the new + `AdapterOnlyPredicateGateKind` type and the new prefix constant are module-internal, not + re-exported from the package entry — matching objectui#5687's own symbols, which never were + either). +- 12402a9: Drop the undeclared object-level `titleField` from the object page's five + remaining view-config seams, and from the record-search memo key + + `ObjectView` resolves a title field for seven view kinds. Two of them — + calendar and gantt — already read `viewDef.<kind>?.titleField || 'name'`. The + other five (timeline, kanban, map, gallery, tree) carried a three-rung chain + with `objectDef.titleField` in the middle, so one file answered the same + question two different ways. This converges the five on the shape the two + siblings already had; it is a convergence, not a removal. + + The middle rung could never fire for legal metadata. `@objectstack/spec`'s + object schema is a `strictObject`, so + `ObjectSchema.safeParse({ …, titleField: 'x' })` is rejected with + `unrecognized_keys` — the same issue code a nonsense key gets — while + `nameField`, `displayNameField` and `titleFormat` all parse (measured against + `@objectstack/spec@17.2.0`, the dist this repo installs). objectui#6531 + established that measurement and dropped the twin read inside + `getRecordDisplayName`. Reading a key no producer can ship is the + consumer-side alias AGENTS.md Commandment #0.1 bans. + + Behaviour for every legal config is unchanged, and both directions are pinned: + a view that declares its own `titleField` still wins on every kind, a view that + declares none still floors at `'name'`, and an object carrying the + contract-rejected key is now honoured by no kind. Re-pointing the middle rung + at the declared `nameField` was considered and rejected: it would have added a + rung calendar and gantt do not have — increasing the divergence — and, unlike + this change, it would have altered behaviour for legal configs. + + `useRecordSearch`'s candidate signature — the memo key that decides when the + cross-object fanout re-runs — appended `o?.titleField ?? ''` to every entry. + Because no legal object definition can carry the key, that half was permanently + `''`: a constant suffix in a cache signature, and the last thing in the repo + that read as evidence some producer supplies it. The signature is now the object + name alone, which is the only field of an object definition the effect actually + consumes. Change detection is unaffected — a changed candidate name still + re-runs the fanout, and a new array with identical content still does not. +- 46b9bc9: A `props` config bag on a component-renderer node is now named at render + instead of dropped in silence (objectui#6708). + + `SchemaRenderer` HOISTS every `properties.*` value onto the node, so a key + written under `properties` is a real value on `schema.<key>` by the time a + renderer destructures it. `props` — the annotated legacy alias of the same bag + — is NOT hoisted: it is evaluated and then spread as React props on the created + element. A renderer declared as `({ schema })`, which is the normal shape for + the component renderers, therefore never sees it. The `element:*` family is the + exception: its `readProps()` merges `{ ...schema.props, ...schema.properties }`, + so the same spelling is honoured there. + + Every gate accepts the `props` spelling — `BaseSchema` is `.passthrough()` with + `[key: string]: any` — and the docs call it a supported alias, so nothing + between the author and the screen said a word. Re-measured on `faac0d935` + through the real `SchemaRenderer` with a probe that records both channels: + + | node | React prop `data` | `schema.data` | + |---|---|---| + | `props: { data: "${data.customers}" }` | the evaluated array | absent | + | `properties: { data: "${data.customers}" }` | the evaluated array | the array | + + Same key, same value, one envelope apart. The expression is evaluated on both + legs, so this is a dropped value rather than an unevaluated one. Read through a + real `data-table` (objectui#6665's four-leg pin) the same pair renders + `No results found` against the two rows. + + The diagnostic's level and dedupe were chosen from a census, which the ruling + made a precondition. Every JSON document, every `json` fence in every + `.md`/`.mdx`, and every TypeScript object literal in the repo was walked for + nodes carrying both `type` and `props`: 39 such nodes, 22 of them on + component-renderer types, and 19 of those 22 are test fixtures exercising this + shape on purpose. The authored, non-test corpus holds 5 — three of which are + deliberate counter-examples in the skills guides. Nothing floods, so the level + is not softened for volume; the dedupe is keyed on the MESSAGE rather than on + the schema object, so a metadata generator emitting one wrong envelope across + many nodes still gets one line while two genuinely different nodes get two. + + `console.warn`, matching objectui#6575 and objectui#6665 — the two prior + instances of this exact "you declared something and the renderer dropped it" + shape — rather than the `console.error` its neighbour at this tier uses for a + raw `${...}` placed verbatim in front of a user. Nothing is placed here; a + value is dropped. + + No behaviour change, which is the entire reason this arm was chosen. Hoisting + `props` to parity with `properties` was refused at ruling: it would weld the + legacy alias in as a permanent second spelling, against this repo's + alias-retirement direction. Refusing the key at parse stays blocked on the + `.passthrough()` ceiling (objectui#5155 / objectui#6269). What every renderer + receives is pinned byte-for-byte against a reading captured on the tree before + the diagnostic existed. Nothing is added to the published surface either — the + predicate, message builder, prefix constant and test-only reset are + module-internal and are not re-exported from the package entry, matching + objectui#6575's own symbols. The trap stops being silent; it does not stop + being a trap. +- d47de51: `useSchemaPersistence` no longer writes its adapter ref during render + (objectui#6745). + + The hook keeps the live adapter in a ref so `save`/`load`/`list`/`remove` can be + created once and still reach the newest adapter at call time. That ref was + written in the render body: + + ``` + const defaultAdapter = useRef(createLocalStorageAdapter()); + const adapterRef = useRef(adapter ?? defaultAdapter.current); + adapterRef.current = adapter ?? defaultAdapter.current; // during render + ``` + + which `react-hooks/refs` flags on three counts. A render React discards or + replays — StrictMode, a Suspense retry, a concurrent interruption — still + performed that write, so a save could be routed through an adapter belonging to + a render that never committed. + + The write now happens in `useInsertionEffect`, and the default adapter comes + from `useMemo` instead of a ref read during render. + + **Timing is preserved for every legal call site.** Insertion effects run in the + mutation phase — before every layout effect in the tree, before paint, and + before any event handler can fire — so a changed `adapter` prop is in place + before anything that may legally invoke these methods can observe it. This is + deliberately *not* `useEffect` (which lands after paint) or `useLayoutEffect` + (a child's layout effects run before its parent's); either would route a call + made earlier in the same commit to the previous adapter. The only window that + changed is a read during the render phase itself, which no legal consumer has: + `save`/`load`/`list`/`remove` are side effects and are never callable during + render. + + Also fixed in passing, on the same lines: `useRef(createLocalStorageAdapter())` + invoked the factory on **every** render and discarded all but the first result. + The `useMemo` runs it once. The adapter is a stateless facade over + `localStorage` and its identity is never exposed, so this is unobservable + beyond the saved work. + + No API, signature or observable behaviour change for any supported call site. +- 3fe6463: `SchemaRenderer`: a non-object `props` bag is no longer object-spread into + indexed React props (objectui#6752). + + A node written `{ type: 'card', props: 'not-a-bag' }` reached `createElement` + carrying nine React props named `0` through `8`, one per character, because + `{ ...'not-a-bag' }` enumerates a string's character indices. Nothing threw and + nothing was logged, so the symptom — a component handed nine props it never + declared — sat a long way from the `props` value that caused it. Measured + through the real `SchemaRenderer`; the in-repo corpus has no such node today, so + this is a latent shape rather than a live failure. + + The canonical `properties` branch already carried the wider guard. Its comment + claimed the guard was hoist-specific, and that turned out not to survive + measurement: ablating it leaves the indexed keys the hoist puts on the node + completely unchanged, and moves only whether `schema.properties` still holds the + value the author wrote. The reason is channel-independent — a degenerate value + must not have its shape reinterpreted by an object spread — so `props` now + carries the same guard, both bags share one `isConfigBag` predicate, and the + `properties` comment states the measured reason instead of the old one. + + Both sites that spread the bag are covered: the evaluation memo (so + `schema.props` keeps the authored value) and `propsWithoutCanonicalKeys` (so the + `createElement` spread does not re-enumerate it). A degenerate bag now + contributes no keys, and the authored value is passed through unmangled on the + React prop named `props`. + + Unchanged: a normal object `props` (still evaluated, still spread per key), + objectui#5123's two-bag precedence, the `properties` hoist, and objectui#6708's + dropped-`props` diagnostic. +- 31ab372: A degenerate `properties` bag no longer reaches the element as indexed React + props (objectui#6760). + + `properties` is the spec spelling of a node's config bag. The hoist that copies + `properties.*` onto the node's top level walked it with `Object.entries` + unconditionally, so a non-object value was enumerated rather than skipped: + measured on `c6732825d`, `{ type, properties: 'not-a-bag' }` reached the element + as nine React props named `0` … `8`, and `properties: ['x', 'y']` as `0`, `1`. + Nobody authored those keys — they are the walk's reading of a string's character + indices. + + The hoist now asks the same `isConfigBag` question the evaluation memo and the + `props` bag already ask (objectui#6752, objectui#6761). Of the two arms the card + left open, this is "guard the hoist" rather than "declare that the hoist may + enumerate anything", because objectui#5123 ruled that a key gets one answer + whichever channel reads it — and the alternative would have answered one + authored mistake two ways, with the reinterpreting half falling on the canonical + spelling while the quiet half fell on its legacy alias. + + Nothing else moves: a real object bag hoists exactly as before (including the + `type`/`id` keys the hoist has always refused to copy), the authored + `properties` value still reaches renderers on both channels unchanged, and + `properties: 42` / `properties: true` were already contributing no keys. +- 67fc3b0: `useETagCache`, `useGlobalUndo` and `useOffline` stop writing their config refs + during render (objectui#6797). + + Each of the three kept a "latest value" ref that was assigned in the render + body, one `react-hooks/refs` warning apiece on this base + (`useETagCache.ts:204`, `useGlobalUndo.ts:57`, `useOffline.ts:262` — all three + `Cannot update ref during render`, all three the WRITE only; unlike + `useSchemaPersistence` none of them also READ a ref during render). A ref + written in the render body is also written by renders React discards or + replays — StrictMode's double render, a Suspense retry, a concurrent + interruption — so a tree that never committed could publish its config to + callbacks that outlive it. + + The write moved to `useInsertionEffect` in all three, but that shape was chosen + per hook rather than carried over, because what each ref protects differs: + + - **`useETagCache`** — five resolved config scalars read by five `useCallback`s + with `[]` deps whose identity is part of the published result. Re-keying them + on the config values would have changed `fetchWithETag`'s identity whenever a + caller's `ttl` moved, re-firing consumer effects keyed on it, so the ref + stays. + - **`useGlobalUndo`** — the whole options bag. Every caller passes a fresh + inline literal with inline `onUndo` / `onRedo` closures, and the keydown + effect is keyed on `undo` / `redo`, so the ref is the only thing keeping + those two stable while still reaching the newest callbacks. + - **`useOffline`** — `config.sync`, read by one caller (`sync`) that is + *already* unstable (deps `[enabled, queue]`). Here the ref protects RETAINED + closures rather than an identity: the auto-sync effect deliberately captures + a `sync` and fires it 100ms later, and that closure must still see the newest + `batchSize`. Dropping the ref for a `syncConfig?.batchSize` dep would have + changed what that retained closure reads, so it was rejected. + + `useInsertionEffect` runs in the mutation phase — ahead of every layout effect, + ref attachment and paint — so the only window any of the three defers is the + render phase itself, where none of the affected callbacks is legally callable. + `useEffectEvent` would be the idiomatic answer but is React 19.2+, and this + package's peer range starts at React 18. + + **No behavioural change is claimed for callers that exist today**: reverting any + of the three implementations leaves the whole suite green, and the new pins pass + against the old code and the new code alike. They guard the next edit — each + file's discriminating pin fails under both `useEffect` and `useLayoutEffect`. +- 33a3b3c: `useETagCache` builds its config object once per hook instance instead of once + per render (objectui#6817). + + `useRef({ enabled, storage, storagePrefix, maxEntries, ttl })` evaluated that + literal on **every** render and kept only the first result, so every later + render allocated a five-key object that was discarded. It now comes from a + `useMemo` keyed on the five values, which is also what the ref's + `useInsertionEffect` write publishes. + + `patch`, not `minor`: nothing a published consumer can observe changes. The + public shape, the returned callbacks' identities and the values the stable + `[]`-deps callbacks read off the ref are all unchanged — the object's identity + is private to the hook, so the only difference is the allocation that no longer + happens. Same pattern PR objectui#6796 repaired in `useSchemaPersistence`; this + is the half of that class the `react-hooks/refs` rule structurally cannot see, + which is why it needed a test rather than a lint fix. +- 2ce2612: A record-page URL now names the object the clicked rows actually came from, in + `ObjectTree` and `ObjectCalendar` (objectui#7638). + + `useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed, + and both components handed it the bare top-level `schema.objectName` while resolving + their own rows through the objectui#6939 record-source ladder (`data`, then + `staticData`, then `objectName`). objectui#6939 published `objectName` as that ladder's + THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one + record source — and a row fetched through `data.object` whose click built + `/{schema.objectName}/record/{id}` named a record that the URL's own object does not + contain. + + Two shapes change, both toward the object the rows came from: + + - a block carrying **both** bindings navigated to the top-level key and now navigates to + `data.object`; + - a **data-only** block had no name to build a URL from at all, so the hook took its + `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed + record URL. + + `ObjectCalendar` is where the divergence was plainest: on one click it resolved the + detail drawer through the ladder and the navigation URL through the top-level key. The + URL now reuses the very `schemaObjectName` that already keys the calendar's record query + and its `$expand` derivation, so query, drawer and URL agree by construction. + + **Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName` + tail for the off-contract `data: { provider: 'object' }` that carries no `object` + (`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName` + already carries, and the same conservatism objectui#7627 applied when it published the + shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an + `objectName`, and only what callers hand it has changed. + + The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is + why there were copies to convert at all — component authors copied the divergence out of + the documentation, correctly, as written — so it now points at + `resolveRecordSourceObjectName` and says explicitly that a caller with no data config + has nothing above rung three and should keep passing `schema.objectName`. + + `ObjectKanban` is deliberately **not** converted: it has no data config, no + `getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so + `schema.objectName` already IS its record source and its board, drawer and URL already + agree. +- d1bebb0: The unevaluated-expression diagnostic now lists `properties` among the channels + that evaluate and read back, matching the sibling `props`-bag diagnostic that + tells authors to write their keys there. + + Both messages are dev-build diagnostics in `SchemaRenderer`, and one node can + trip both. They disagreed: `propsBagDiagnostic` said *"`props` is NOT hoisted + onto the node — only `properties.*` is … Write them under `properties` + instead"*, while `unevaluatedExpression` enumerated *"channels that do evaluate + and read back today"* as `content` or host-side resolution — omitting + `properties`. An author who hit both was told to use a channel the other message + said did not work. + + The enumeration was the wrong half, established by measurement rather than by + reading the `COMPAT` label on the hoist. `properties` has no retirement on + record: `@objectstack/spec@17.2.0` calls the vocabulary carried there *"ALIVE — + this is not dead surface to retire under ADR-0049"*, keeps + `PageComponentSchema.properties` as the open carrier on purpose, gates it at the + authoring door, and tombstones no part of it via `retiredKey()`. In this repo + `props`, not `properties`, is the spelling annotated as the legacy alias. + + Diagnostic text only — no evaluation, hoist or schema behaviour changed. +- 98c3a74: Fix `createLazyPlugin`'s JSDoc example, which taught a call that does not compile. + + The `@example` block passed `() => import('@object-ui/plugin-grid')` as the + `importFn`. ObjectUI plugin packages export their components by name and have no + `default`, so that call supplies the module namespace object — rejected by the + compiler (`Property 'default' is missing`) and, at runtime, handed to + `React.lazy` as the component. The examples now unwrap the named export via the + `async` spelling, which is the form that actually type-checks. This ships in the + published `.d.ts`, so it is what editors show on hover. +- 9850c6e: `SchemaRenderer`'s node visibility gate now emits a dev-only warning when a + predicate reads `data.*` and the data-source adapter cannot answer that read, so + an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the + constant they authored instead of shipping it (objectui#5687). + + At the node tier `data` is the data-source **adapter** — the object `${data.total}` + in a props bag resolves against — and it has never been the row. A predicate + written with the deprecated `data.*` spelling therefore resolves `undefined == + 'draft'`, which is a perfectly good `false`: the block is hidden on every row, and + because it does not throw, the unresolvable-predicate reporter added for + objectui#5454 never fired. Measured on this base, the same predicate written as a + `{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether + an author heard about the identical mistake depended on which dialect they happened + to write it in, which is the arbitrariness objectui#5454 existed to remove. + + **No verdict changes and no interpolation changes** (maintainer ruling, + 2026-08-22, option A). The node tier keeps its documented `data` = adapter + semantics; objectui#5330's row binding does not extend here. The evaluator's answer + is returned exactly as computed and `${data.*}` interpolation is untouched — only + the silence moved. + + The report fires on a `data.*` read the bound adapter answers with `undefined`, not + on the spelling. A genuine adapter read stays silent (`data.total > 0` against an + adapter carrying `total`), a canonical `record.*` predicate stays silent, and a + correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per + node type + key + predicate source — the same module, Set and lifecycle as the + objectui#5454 reporter. + + This loudness is temporary by design: it dissolves when objectui#5330's deprecation + window for the `data.*` row spelling closes. +- b2ea297: `ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject` + instead of being a hand-written copy (objectui#5362; maintainer ruling + 2026-08-20: the object document type belongs to the spec). + + What changes on the published type surface: + + - **Gained:** the full spec object-document surface, including the three keys + the runtime already reads but the old interface rejected as excess + properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`, + `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of + the spec document). + - **Removed:** nine members the old interface declared that no objectui + runtime code reads and the spec document does not know: `extends`, + `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling + is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`. + `ObjectTrigger` and `ObjectRelationship` remain exported unchanged. + - **Kept:** `editMode` — the one measured client-side member the runtime reads + (`recordFormNavigation` / `AppContent`) — now declared on the new + `ObjectSchemaClientExtensions` interface, which the derivation intersects. + Note the spec's strict parse rejects `editMode` on published documents + (`unrecognized_keys`); it is a client-type member only. + + Spelling settlement: `listViews` (camelCase) is canonical — `list_views` + appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in + `@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling + READ fallback for stored pre-settlement documents (that stock has never been + censused — objectstack#7917); the CRUD guide and its pinned transcription now + author the canonical spelling. +- 4f14ad7: `SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated + spelling of the objectui#5454/#5687 diagnostics when it is written inside + `properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }` + (objectui#5756). + + The `properties.*` evaluation loop runs, and interpolates every `${…}` template it + finds, **before** the visibility gate ever sees the value — so by the time the + existing diagnostics ran, a template-spelled predicate had already collapsed into a + plain boolean and there was no predicate text left to inspect. The bare-string + spelling of the exact same gate (`properties: { visible: "data.status == 'draft'" + }`) was unaffected — nothing interpolates a string with no `${` in it — and was + already reported; only the template spelling was structurally invisible. + + Reached by moving the diagnostic check to inside the `properties` evaluation loop, + on the predicate's raw (pre-interpolation) text, gated on the key being one of the + six visibility keys the render chain consults (`visibleWhen` / `visible` / + `visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content` + interpolation, or any other non-visibility key, is untouched and stays silent. + + Reports only the key that actually **decides** the node's visibility, mirroring + objectui#5454's own leg semantics (its reporter is likewise only ever invoked on + the leg the chain's early-return sequence actually reaches): a `properties.visible` + template that a co-declared `visibleWhen` outranks is not reported for deciding + nothing. + + **No verdict changes and no interpolation changes.** The diagnostic call's return + value is discarded; the real verdict is still computed afterward, off the + post-evaluation, post-hoist schema, by the same code path as before this change. + Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data + predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate — + only the silence moved, one render-step earlier. +- f90b8fb: `toRenderableSchema`'s header now says the bridge is permanent, instead of instructing + callers to remove it (objectui#4622). + + No executable line changes — but the artifact is **not** unchanged, and that is worth + stating plainly rather than rounding to "comment-only". This package builds with plain + `tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc + is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both + what an editor shows on hover at every call site and bytes that ship. + + Measured by building the package the way the repo builds it, at both revisions: + `dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850 + to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19 + differing lines in the emitted file are JSDoc continuations and the three executable lines + are byte-identical, so the growth is the paragraph and nothing else. The trade is + deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old + paragraph's instruction produced once already. + + The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have + not been reconciled" and that "when it lands, the call sites using this can go back to + forwarding directly". Both halves went false when PR #4608 merged, and the second half is + the harmful one: it is an instruction whose trigger condition has now fired, sitting + directly above the function a future author is about to call. + + The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of + `@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring + an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548 + ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So + a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a + permanent crossing between two intentionally different types rather than scaffolding + awaiting a merge. + + Following the old instruction has a measured cost: five `apps/site` call sites were + forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly + five hours until PR #4621 routed all five through this function (objectui#4617). +- f53a8d0: fix(react): stop the form-view bridge silently dropping 18 spec keys + + `spec-bridge/bridges/form-view.ts` promised (#2545) that "every serializable + spec key is either mapped onto the `object-form` node or listed here with an + explicit reason for being ignored". Measured against `@objectstack/spec` 17.2.0 + the promise was false for 18 keys, because the conformance test enforcing it ran + its completeness loop over its own hand-written fixture rather than the + contract's key set. + + Seventeen of them now reach the node, at the destinations the receiving layer + already reads: + + - `FormViewSchema.buttons` / `.defaults` — `ObjectFormSchema` declares both and + `ObjectForm` folds them at render (action-button visibility/labels, and + create-mode initial values). + - `FormSection.pane` — explicit split-pane placement; without it a spec-authored + split form fell back to the positional rule, so reordering sections moved them + across the divider. + - `FormSection.visibleOn` — the deprecated spelling now folds onto `visibleWhen`, + matching the contract's own parse-time normalisation and the field path. + - Thirteen `FormFieldSchema` keys — `maxLength`, `minLength`, `min`, `max`, + `precision`, `scale`, `multiple`, `immutable`, `span`, `language`, `keyField`, + `disclosure`, `fields` — so authored constraints, composite config and field + width survive the bridge instead of ending there. + + `publicPicker` is deliberately not carried and now says so: it is a server-side + public-lookup authorization opt-in with no client destination. + + The conformance test's key set is now derived from the contract's own shape at + all three levels, so a spec key that is neither mapped nor explained fails the + suite by construction. +- d91aed9: Name the case-only spelling when a component type misses the registry. + + Registry lookup is exactly case-sensitive, so a node typed `Page` misses a registered `page` and falls through to the OBJUI-001 "Unknown component type" panel. Because the mistake is usually uniform across a document, the symptom is not one broken widget — it is the whole page rendering as error panels, with nothing in the message pointing at the cause. + + Both surfaces that report the miss now name the spelling that would have resolved. `SchemaRenderer`'s panel reads `Unknown component type: Page — did you mean 'page'?`, and `objectui check` reports `Unknown schema type "Page" in <file> — did you mean "page"?`. When no known type differs by case alone, neither says anything extra — `zzz` gains no bogus suggestion, and this is case matching, not an edit distance, so `pge` suggests nothing either. + + **Lookup itself does not change.** `Page` still misses, still fails, and still renders the panel; only the message teaches. Normalising the lookup was considered and rejected (objectui#5247, maintainer ruling 2026-08-19): it would make two spellings valid everywhere, permanently, and legalise the typo class (`PAGE`, `pAge`) along with the PascalCase convention. + + Each surface reads its candidates from the set it can actually trust — the renderer from the live `ComponentRegistry` (including pending lazy stubs), the CLI from the registration-derived `KNOWN_SCHEMA_TYPES` snapshot — so neither can suggest a type nothing registers. +- Updated dependencies [64dae8e] +- Updated dependencies [b06e374] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [594704f] +- Updated dependencies [d3995fe] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [993f312] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [41b7ce3] +- Updated dependencies [d2fb6ef] +- Updated dependencies [94e2fa7] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [95f8704] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [8d3a529] +- Updated dependencies [5ac2e2c] +- Updated dependencies [7e19d03] +- Updated dependencies [b08b7eb] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [5fa06c4] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [5d3a2d1] +- Updated dependencies [8f1d995] +- Updated dependencies [b362c1b] +- Updated dependencies [5127378] +- Updated dependencies [dddb942] +- Updated dependencies [00c665e] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [c8ea8af] +- Updated dependencies [9602dc8] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [fff9645] +- Updated dependencies [9c3b7ce] +- Updated dependencies [2c1c967] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [969ba84] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [dbd5194] +- Updated dependencies [b392674] +- Updated dependencies [4f3a1e2] +- Updated dependencies [f07b976] +- Updated dependencies [045d20b] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [320374d] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [220c18d] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [639114c] +- Updated dependencies [1490691] +- Updated dependencies [1f31d3a] +- Updated dependencies [d8ec8d6] +- Updated dependencies [351eb31] +- Updated dependencies [866cd1d] +- Updated dependencies [20c04b2] +- Updated dependencies [01c9023] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [8952395] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [a4514e8] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [6a99bb2] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [3399704] +- Updated dependencies [81a2eb1] +- Updated dependencies [20cb8db] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [2a5bf45] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [91f9276] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [a915064] +- Updated dependencies [52cac38] +- Updated dependencies [53ded82] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [f57ca75] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [b2e85a9] +- Updated dependencies [d5c1f52] +- Updated dependencies [c7cd2b6] +- Updated dependencies [b2065e7] +- Updated dependencies [2fcefb9] +- Updated dependencies [77f846a] +- Updated dependencies [bc5870c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [b470e91] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [20e317c] +- Updated dependencies [0fce2ef] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [9118a31] +- Updated dependencies [de570cc] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [fd8dace] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [f157423] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [fb96ecb] +- Updated dependencies [1170ed1] +- Updated dependencies [f75810e] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/data-objectstack@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/react/package.json b/packages/react/package.json index 4bdea6727e..f8a750ce3d 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "React bindings and SchemaRenderer component for Object UI", diff --git a/packages/runner/CHANGELOG.md b/packages/runner/CHANGELOG.md index 63cb4bab68..a2bf0485f9 100644 --- a/packages/runner/CHANGELOG.md +++ b/packages/runner/CHANGELOG.md @@ -1,5 +1,418 @@ # @object-ui/runner +## 17.7.0 + +### Minor Changes + +- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate + the direction so the next one cannot land (objectui#8198). + + `check:phantom-deps` judges imports that are not declared; nothing judged the reverse, + so a declaration could outlive its last consumer indefinitely. That is what happened to + `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it + was removed by hand on objectui#7625, and nothing would have reported the next one. The + new `pnpm check:unused-deps` asks the reverse question over `dependencies` and + `optionalDependencies` of every released package. + + **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship + changes: their Vite `external` predicates are path-based and never read `dependencies`, + so no built artifact moves. What changes is the install graph — a project that imports + one of the removed packages while depending only on the ObjectUI package that used to + drag it in will no longer resolve it. Declare it directly; that is the correct + dependency edge in either case. The removals, by package: + + - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields` + - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`) + - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge` + - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod` + - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge` + - `@object-ui/core`: `lodash`, `zod` + - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply + - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect + - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge` + - `@object-ui/fields`: `clsx`, `tailwind-merge` + - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase` + - `@object-ui/auth`: `@object-ui/types` + - `@object-ui/plugin-calendar`: `@object-ui/fields` + - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react` + - `@object-ui/react`: `react-hook-form` + + Every one was verified by a whole-package grep before removal — the name appeared nowhere + under the package but its own manifest and CHANGELOG — and the whole workspace builds, + type-checks and tests green afterwards. + +### Patch Changes + +- adb2a86: The standalone runner renders `AppAction.items` from its declared type only, which + makes `AppActionSchema.onClick`'s retirement message true again (objectui#6854, + maintainer ruling of 2026-09-05, option B2). + + `AppAction.items` is `AppMenuItem[]`, and the zod mirror parses it with the legacy + eight-member `MenuItemSchema` — neither declares `onClick` or `shortcut`. + `LayoutRenderer` reached both through `as any`, past the type it was handed, and + that left three mutually exclusive signals about the same key: the TypeScript face + said `?: never`, the validator's refusal said "no renderer reads this key, so + nothing could ever run it", and a renderer read it. An agent or a reader could + believe any one of the three and be contradicted by the other two. + + **No published accept set moves and no exported symbol changes.** `AppAction.items` + is NOT re-typed (the alternative was measured and refused: it would have carried a + breaking migration for `path` / `href` / `badge` / `type` and the divider spelling, + for a capability with no measured consumer). The refusal message itself is unchanged + — it is shared by 22 other retired handler keys, and deleting the cast is what makes + its sentence true rather than restating it. + + - `@object-ui/runner`: `LayoutRenderer` no longer reads `onClick` or `shortcut` on a + `type: 'user'` action's `items`. The `onClick` branch was an empty body and could + never run a JSON value; the `shortcut` read rendered a `DropdownMenuShortcut` from + a key the mirror strips in silence, so no validated document could reach it. A + census of every JSON and TypeScript app document in this repository found zero + authors of either key (positive controls recorded on the issue). + - `@object-ui/types`: the rationale comments on `AppAction.onClick` and + `AppActionSchema.onClick` said "nothing reads `AppComponentSchema.actions[]`". + That was false — the runner renders both the `'button'` and the `'user'` arm. + Corrected to what was measured: `actions[]` is read, `onClick` is not. + + Whether `shortcut` should become authorable on `AppAction.items` is a separate + contract question and is filed on its own. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d796c8d] +- Updated dependencies [1b1d772] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [062943f] +- Updated dependencies [636b236] +- Updated dependencies [4172589] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [39f4309] +- Updated dependencies [d2fb6ef] +- Updated dependencies [7cd3987] +- Updated dependencies [e304a4e] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [7c96c94] +- Updated dependencies [3e853c9] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [f8cdbf2] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [e176053] +- Updated dependencies [e30ed15] +- Updated dependencies [90665e0] +- Updated dependencies [194fae1] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [a76b18c] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [6fd2cf7] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [b3d562c] +- Updated dependencies [129bcc5] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [8ec11e1] +- Updated dependencies [6f81384] +- Updated dependencies [22ba927] +- Updated dependencies [7d2a689] +- Updated dependencies [f8c70f4] +- Updated dependencies [8f1d995] +- Updated dependencies [f9c34df] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [6e88630] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [f24de8b] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [30c73cd] +- Updated dependencies [830ed58] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [c71e14d] +- Updated dependencies [bf3a03c] +- Updated dependencies [748494b] +- Updated dependencies [5967be0] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [12402a9] +- Updated dependencies [aff3d7a] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [9486ac6] +- Updated dependencies [9486ac6] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [dc4365c] +- Updated dependencies [e321d52] +- Updated dependencies [4c68077] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [06b8c42] +- Updated dependencies [46b9bc9] +- Updated dependencies [b97790a] +- Updated dependencies [7c9b044] +- Updated dependencies [d47de51] +- Updated dependencies [3fe6463] +- Updated dependencies [31ab372] +- Updated dependencies [846889b] +- Updated dependencies [26896c6] +- Updated dependencies [67fc3b0] +- Updated dependencies [33a3b3c] +- Updated dependencies [b87f15b] +- Updated dependencies [045d20b] +- Updated dependencies [c18d099] +- Updated dependencies [adb2a86] +- Updated dependencies [03380aa] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [6a449fc] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [f08bcd9] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [00d3f09] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [d1842ab] +- Updated dependencies [0349555] +- Updated dependencies [78ca238] +- Updated dependencies [40c4711] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [adbda1b] +- Updated dependencies [e8c553b] +- Updated dependencies [2e32ed4] +- Updated dependencies [7c3df8f] +- Updated dependencies [7c3df8f] +- Updated dependencies [b9f5ff1] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [bb459ea] +- Updated dependencies [4704aa4] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [26e06d7] +- Updated dependencies [6ca6e12] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [7bf244b] +- Updated dependencies [ed4a2f1] +- Updated dependencies [0dc2c93] +- Updated dependencies [f0bb9fa] +- Updated dependencies [d327b9c] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [2f61238] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [daf9d57] +- Updated dependencies [c15d7ec] +- Updated dependencies [5d0876c] +- Updated dependencies [f7ace0a] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [2ce2612] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [2af1fa7] +- Updated dependencies [01c27c4] +- Updated dependencies [c14d3a0] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [0ead1f6] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [d1bebb0] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0758bd8] +- Updated dependencies [0ea7054] +- Updated dependencies [2d80456] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [3c76801] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [894d103] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [2c8474c] +- Updated dependencies [0e05aac] +- Updated dependencies [ae61ad4] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [5eddeeb] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [aefe428] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [d6fe1e1] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [d7bd274] +- Updated dependencies [98c3a74] +- Updated dependencies [ebce5a3] +- Updated dependencies [9d9040d] +- Updated dependencies [0fce2ef] +- Updated dependencies [9850c6e] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [6c5ee71] +- Updated dependencies [6f017e9] +- Updated dependencies [ab92940] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [4f14ad7] +- Updated dependencies [258d264] +- Updated dependencies [cac64b3] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [bf28341] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [93bbc20] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [dba7d84] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [45d8288] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [9101be5] +- Updated dependencies [f53a8d0] +- Updated dependencies [57f9b07] +- Updated dependencies [3c73d99] +- Updated dependencies [d91aed9] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [c86185e] +- Updated dependencies [1170ed1] +- Updated dependencies [dd35800] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/plugin-kanban@17.7.0 + - @object-ui/plugin-charts@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/runner/package.json b/packages/runner/package.json index aceca9209c..94b4beff13 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@object-ui/runner", "private": false, - "version": "17.6.0", + "version": "17.7.0", "description": "Universal Object UI Application Runner", "type": "module", "homepage": "https://www.objectui.org/docs/utilities/runner", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 195115f84a..5bfda1aeaf 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,219 @@ # @object-ui/sdui-parser +## 17.7.0 + +### Minor Changes + +- 0db4fb3: html tier: a braced attribute value that is not strict JSON now draws a `inert-expression` warning instead of vanishing silently (objectui#6598) + + `interpretBrace` materializes strict-JSON values only; anything else — the + single-quoted array every JSX author writes (`columns={['name','amount']}`), + unquoted object keys, any JS expression — compiles to the deferred `{ $expr }` + marker, and nothing downstream evaluates that marker: the html tier parses, + never executes (ADR-0080), and no renderer consumes `$expr`. The value reached + the renderer as an opaque object, defensive non-array/non-object reads degraded + it to "not declared", and the author's binding vanished with zero diagnostics + anywhere — a production page's `list-view` rendered its row count and toolbar + with no data columns, through eight `columns` spellings (objectui#6598, moved + from objectstack#12649). That is ADR-0078's prohibited parsed-but-silently-inert + state. + + `validateTree` now emits a warning-severity `inert-expression` diagnostic when a + declared input's value is the `$expr` marker, with the fix in the message: write + the value as JSON (double-quoted strings and keys). Warning, not error, per the + objectui#5709 posture for inert authored keys — pages keep compiling and + rendering exactly as before; the silence is what changed. Escalating the + severity, widening the accepted literal grammar (e.g. materializing + single-quoted strings), and covering base props like `style` are contract + decisions deliberately left on objectui#6598. +- 4703651: html tier: braced attribute values now materialize the JS literal subset — single-quoted strings and unquoted identifier keys work (objectui#6614) + + The html tier is the untrusted-safe DATA tier: source is parsed, never executed + (ADR-0080), which makes it the only safe carrier for runtime AI- or + tenant-authored pages. But `interpretBrace` accepted only strict JSON inside + braces while the surface called itself JSX, so `columns={['name','amount']}` — + the spelling every JSX author and every AI author writes by habit — compiled to + the deferred `{ $expr }` marker that nothing downstream evaluates, and the + author's whole data binding vanished at render. A production page's `list-view` + rendered its row count and toolbar with zero data columns through eight + `columns` spellings before the author gave up (objectui#6598, moved from + objectstack#12649). That was a trap, not a contract. + + `interpretBrace` now materializes the JS **literal subset**. Exactly two + widenings over JSON, and nothing else: + + 1. **single-quoted strings**, in value position and in key position — + `title={'Accounts'}`, `columns={['name','amount']}`, `{{'pageSize': 25}}`; + 2. **unquoted identifier object keys** — `options={{pageSize: 25}}`, + `columns={[{field:'name',label:'Full Name'}]}`. + + Everything else JSON refuses is still refused, still compiles to `{ $expr }`, + and still draws the warning-severity `inert-expression` diagnostic: trailing + commas, comments, array holes, spreads, `undefined`/`NaN`/`Infinity`, + `+1`/`.5`/`1.`/`0x1f`, template literals, and every genuine expression — + identifiers, member access, calls, operators, ternaries. The subset contains no + identifier lookup and no operator, so there is nothing in it to execute: this + moves habitual spellings onto the materialized side, it does not move the + boundary between data and code. An authored `__proto__` key becomes an ordinary + own property, as `JSON.parse` gives it — never the prototype setter. + + Strict JSON is unchanged, structurally: `JSON.parse` still runs first and + untouched, so any value it accepts takes byte-identically the path it always + did, and the literal reader only ever sees input `JSON.parse` has already + thrown on. + + The `inert-expression` message changed with the grammar. It used to advise + "write it as JSON (double-quoted strings and keys)" and named + `columns={['name','amount']}` as the wrong form — advice that would now send an + author to edit working source. It names the accepted literal grammar instead. + + Maintainer ruling of 2026-08-28 (objectui#6614 Q1-A). ⛔ Two ruled items are + deliberately NOT in this change: escalating `inert-expression` from warning to + error (Q2 — it belongs at the save gate, once the framework wires the registry + manifest into `validate-jsx-pages`), and base-prop (`style`) `$expr` inertness + (Q3 — sequenced after this, so no warning is added for spellings this change + legalises). +- 9e37d9b: `binding` on a component input is framework-set, not author-declared: `@object-ui/types` gains `InjectedComponentInput`, the `'field'` binding arm is retired from `@object-ui/sdui-parser`'s `RegistryConfigLike`, and the `as ComponentMeta` cast at the injection seam in `@object-ui/core` is gone (objectui#6950; maintainer ruling of 2026-09-07, director decision batch #69; ADR-0049 enforce-or-remove). + + **What was measured.** `binding` was published — the manifest serializer forwards it — and read — `validateTree` records a binding site for it — while `ComponentInput`, the authoring type every registration writes against, did not declare it. The one writer in the tree, `ELEMENT_DATA_SOURCE_INPUT`, therefore carried a hand-written inline type and reached a registration's `inputs` through `as ComponentMeta` in `Registry.register`. Declared narrower than enforced, on a published type — and `ComponentInput`'s own docblock listed `binding` among the forwarded per-input keys. + + **The ruling** answered the product question the card asked — may an ordinary registration declare a binding input? — with no. So: + + - **`@object-ui/types`** exports `InjectedComponentInput`, an `interface … extends ComponentInput` with the required marker `binding: 'object'`. `ComponentInput` itself does not change: no member is added, and authoring `binding` on a registration stays an excess-property `tsc` error — now on purpose and documented at the interface. The two tombstone docblocks that listed `binding` as a forwarded key now say it is forwarded from the framework's injected input, not authored. + - **`@object-ui/core`** types `ELEMENT_DATA_SOURCE_INPUT` as `InjectedComponentInput` and splices it through a typed local; the cast is gone. Runtime behaviour is unchanged — the same key, `type`, `binding` and `description` reach the manifest, and `validateTree` still records the binding site. + - **`@object-ui/sdui-parser`** narrows `RegistryConfigLike.inputs[].binding` from `'object' | 'field'` to `'object'`. The `'field'` arm had zero writers — every `binding:` literal in `packages/`, `apps/` and `examples/` is `'object'`, 7 of 7 at this change's merge-base — and nothing on either side of the manifest resolved a field binding. **Breaking, deliberately:** a config that feeds `manifestFromConfigs` a `binding: 'field'` input is now a type error instead of a manifest entry the server would never resolve. `ManifestInput.binding`, the manifest reader's vocabulary, is not narrowed by this change. + + If a real need for author-declared bindings is ever measured, it is filed as a widening of `ComponentInput` with the vocabulary decided then — not by putting the cast back. +- cf1d29e: `ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one + (objectui#8067). + + A registration's `type: 'array'` said a value was a list and stopped there, so a member + that drifted from `@objectstack/spec` was invisible to every layer that reads a + declaration. `page:header.actions` is the measured cost: the contract declares + `z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` + objects, and the repo-wide parity gate in + `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the + whole life of the drift because both sides carried the key and neither could say what + was inside it. What settled it was a maintainer ruling, not a test — and even after the + fix, "these are ids" survived only as English in the registration's `description`. + + **What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind + vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an + `object` used as a map. One kind, or an array of them for a member contract that is a + union, with `type`'s semantics — a member passes when any declared arm accepts it. The + manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per + input instead of six. + + **Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 + is the precedent: five `ComponentInput` keys declared and read by nothing). The + repo-wide parity gate compares every declared `of` against the member kind + `ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; + `sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new + `member-type-mismatch` diagnostic naming the offending positions; and the generated + `sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is + `string[]` where it used to be `unknown[]`. + + **Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: + every container key's member position was probed with one value of each coarse kind and + a declaration written only where exactly one kind was accepted. A member contract that + admits several kinds — `record:highlights.fields` takes a field name or an inline field + object — is deliberately left undeclared and pinned with its reason, because picking one + arm there is a narrowing this repo leaves un-gated and picking all of them would + advertise shapes only a per-block pin can vouch for. + + **The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer + ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` + is the publication face's expression ceiling, and spec is the sole judge of values — + stands exactly as written. `of: 'object'` says the members are objects; which keys they + carry is still `description`'s job and `os validate`'s. + + **Nothing published before this changes.** An input that declares no `of` validates, + serializes and types byte-identically: `validateTree` checks no member, the serializer + emits no key, and the codegen emits the same `unknown[]`. +- 8d58f46: `validateTree` now reports a dashboard widget `options` key that no renderer + consumes as a `unconsumed-widget-option` **warning** naming the consumed set + (objectui#5709 ruling). The census behind the accepted set — the spec's five + declared query keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, + `stageOrder`) plus the `description` sub-caption convention key — is + re-measured on every test run against `@objectstack/spec` and the + `plugin-dashboard` renderer sources. The check fires only on dataset-bound + shorthand widgets (the spec-legal form) hosted by `dashboard` / + `dashboard-grid` nodes, and honours the spec's per-widget + `suppressWarnings: ['unconsumed-widget-option']` escape hatch. Warning + severity only: documents keep parsing, saving and rendering. Exported for + other surfaces: `checkDashboardWidgetOptions`, `CONSUMED_WIDGET_OPTION_KEYS`, + `DASHBOARD_WIDGET_HOST_TYPES`, `UNCONSUMED_WIDGET_OPTION`. + +### Patch Changes + +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. +- 305205a: `kind:'html'` page sources keep the space between a text run and an adjacent + inline element: `A <strong>x</strong> page` now compiles to `A `/`<strong>`/` + page` and renders as `A x page` rather than `Axpage` (objectui#5661). + + The parser collapsed each text run's whitespace to a single space — correct, and + what HTML itself does — and then `.trim()`ed it, which is not: HTML collapses a + whitespace run to one space, it does not delete it. The deleted space was + precisely the separator between a run and its inline sibling, so every authored + sentence carrying emphasis or a link in the tier the guide recommends by default + rendered with its words run together. It was silent: the page rendered, the + structure was right, no diagnostic fired. + + The rule is deliberately mechanical rather than a block/inline taxonomy invented + for a schema tree that has none: keep one leading space when a sibling precedes + the run, and one trailing space when a sibling element follows it. The parent's + own start and end still drop their edge space, so `<p> hi </p>` is unchanged. + + Its one bounded cost: a whitespace-only run BETWEEN two siblings survives as a + single space, so a pretty-printed `<ul>` gains one `' '` string child per gap + between its `<li>`s — one space per gap, never the source's newline and + indentation, never at the container's own edges, and never inside an item's own + text. That bound is pinned by a test rather than left as a claim. + ## 17.6.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index d76b96ed64..6224a0cb59 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/sdui-parser", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index ee6986d27e..c6a9e960e0 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,6527 @@ # @object-ui/types +## 17.7.0 + +### Minor Changes + +- 6a91586: Retire the `ActionCondition` `{ expression, then, else }` branch shape from + `ActionSchema.condition` (objectui#3917, maintainer ruling 2026-08-09 route B, + enforce-or-remove). + + `@object-ui/types` declared `condition` as a branch DSL — `expression` plus `then` / + `else` sub-actions — and shipped a zod mirror (`ActionConditionSchema`) that accepted it. + **Nothing ever read `expression`, `then` or `else`**: a repo-wide grep for + `condition.expression|then|else` has zero non-test hits. The only consumer of the key is + `ActionRunner.execute`, which reads it as a **predicate gate** (boolean / bare CEL / + `${...}` template / `{ dialect, source }` envelope). A branch object carries no `source`, + so the runner's normalizer read it as "no gate declared" and executed the action + unconditionally: the predicate was never evaluated, `then` / `else` were never dispatched, + and `os validate` / `os build` stayed green with zero diagnostics. Two docs pages taught + the shape with worked examples, so an author following the documentation + ("amounts over 1000 go to manager approval") got unconditional execution. + + What changes: + + - `ActionCondition` is removed from `@object-ui/types` (and from the barrel export). + - `ActionSchema.condition` is retyped to the predicate the runtime actually honours: + `boolean | string | { dialect?: string; source: string }` — the same three arms + `ActionRunner`'s own `ActionDef.condition` carries, and the same vocabulary `visible` + and `disabled` use. + - `ActionConditionSchema` is removed from `@object-ui/types/zod` (and from the zod + barrel); the `condition` key now validates against that predicate union. + - The two teaching sites (`content/docs/core/enhanced-actions.mdx` Conditional Execution, + `content/docs/api/schema-reference.md` ActionSchema table) are rewritten to the live + vocabulary: `condition` is a gate; a branch is expressed as separate actions with + mutually exclusive `condition`s. + + **The zod parse verdict flips in both directions**, measured on `origin/main` @ `2aff580b5` + against this branch: the branch object went from **accepted** to **refused** + (`invalid_union` on `condition`), and every live predicate spelling — `false`, + `'data.amount > 1000'`, `'${data.amount > 1000}'`, `{ dialect: 'cel', source: ... }` — + went from **refused** to **accepted**. The old schema required `expression`, so the shape + the runtime honours was the one the schema rejected. + + **Breaking for TypeScript authors of `ActionCondition` and for metadata authoring + `condition: { expression, then, else }`** — marked `minor` per this repo's + version-alignment rule, which reserves `major` for following `@objectstack` across a major + (AGENTS.md 版本号策略; same classification as the `MobileOverrides` retirement, + objectui#4919). Runtime behaviour is unchanged: an authored branch object did nothing + before and does nothing now. What changes is that the contract no longer claims otherwise + — the mistake now surfaces at authoring time as a type error and a named zod refusal, + instead of a silent no-op that type-checks, validates and runs the action anyway. +- a04d7c6: Mint the `box` component type — the class-transparent neutral block container + (objectui#3965, maintainer ruling 2026-08-29 方案 A). + + The JSON authoring vocabulary had no neutral block box, which is why the + deprecated `div` could never actually retire: every replacement the deprecation + notice names injects layout of its own (`container` adds width/centering and a + responsive padding ramp, `flex`/`stack` add a display mode and gaps, `grid` adds + `grid-cols-*`, `card` adds border/shadow and wraps children in a `CardContent` + element — all measured through the real `SchemaRenderer`). `box` closes that + gap with a three-clause contract, pinned in + `packages/components/src/renderers/__tests__/box-neutral-container.test.tsx`: + + 1. renders `children`; + 2. authored `className` passes through **verbatim**; + 3. **zero** injected classes. + + Deliberately unlike `div`, `box` reads `children` only — never `schema.body`. + The `div` renderer's `children || body` fallback is what made a mechanical + `div`→X swap silently drop content on `body`-authoring nodes while the element + count stayed unchanged; content moves into `children` at migration time (the + objectui#6771 B-ruling direction). + + Landed on both contract faces per the zod-mirror-parity pairing (objectui#6424 + family form): `BoxSchema` interface in `@object-ui/types`, its zod mirror in + `@object-ui/types/zod`, the `SchemaRegistry['box']` entry, and the registry + registration (`namespace: 'ui'`, `isContainer: true`). With `box` landed, the + catalog's 25 remaining `div`-authoring fixtures (80 nodes) migrate to it + mechanically with zero render difference, and the catalog ratchet closes to + zero tolerance for JSON-authored `div`. +- d88e20f: Removed the block schema family (objectui#4895, ADR-0049 enforce-or-remove, maintainer + ruling 2026-09-02 — option C1, retire the family in one change, no transition window). + + **Breaking on a published surface, deliberately.** These names are gone from + `@object-ui/types`, from both subpaths that carried them: + + - `.` (types): `BlockSchema`, `BlockSlot`, `BlockLibrarySchema`, `BlockEditorSchema`, + `BlockInstanceSchema`, plus the support types with no other reader — `BlockVariable`, + `BlockMetadata`, `BlockLibraryItem` — and `ComponentSchema`. + - `./zod` (runtime validators): `BlockVariableSchema`, `BlockSlotSchema`, + `BlockMetadataSchema`, `BlockSchema`, `BlockLibraryItemSchema`, `BlockLibrarySchema`, + `BlockEditorSchema`, `BlockInstanceSchema`, `ComponentSchema`, and the + `BlockComponentSchema` union over them — which was also `AnyComponentSchema`'s block arm. + + The zod half is the one that mattered. `AnyComponentSchema.safeParse({ type: + 'block-library' })` returned **success** on 17.6.0 for a node no page can render, so an + author who copied the documented shape was told green by the shipped validator and then got + the registry's `OBJUI-001` "Unknown component type" panel. Validated-then-broken is worse + than never-validated, because the green light is what the author trusted. All five + discriminants — `block`, `block-library`, `block-editor`, `block-instance`, `component` — + are now **refused**, pinned in `phase2-schemas.test.ts` alongside the theme refusals + retired the same way. + + Evidence the family was declared-but-unenforced: zero `ComponentRegistry.register(...)` + sites claimed any of the five keys (positive control `'table'` resolves to two), zero + renderers, and zero readers outside `packages/types/src`. The liveness pass this card's + earlier deferral was keyed to (objectui#6935) established that external consumption of this + package is structurally unmeasurable — the certainly-live control `TableSchema` returns the + same zero external consumers — so the ruling was taken on the evidence in hand rather than + on a deferral whose exit cannot fire. + + ⚠️ **Not this family, and not touched.** The live slotted record-page vocabulary — + `PageNodeSchema.kind === 'slotted'` with `slots?: PageSlotMap` (`packages/types/src/layout.ts`), + rendered by `usePageAssignment` / `PageBlockCanvas` / `PageBlockInspector` in + `@object-ui/app-shell` — shares the words "block" and "slot" with the retired family and + shares no declaration, type or file with it. Neither is the `type: 'component'` NAVIGATION + item kind (`{ type: 'component', componentRef }`, `NavigationItemSchema` in + `zod/app.zod.ts`), a different declaration in a different module. + + `@object-ui/components` carries one forced consequence: `renderers/feedback/empty.tsx` + annotated its `action` child as the retired `ComponentSchema` and now says `SchemaNode`, + the node type `SchemaRenderer` actually takes. + + `packages/types/src/blocks.ts` and `packages/types/src/zod/blocks.zod.ts` are kept as + ADR-0049 tombstones exporting nothing, and `block-family-retired-4895.test.ts` pins every + retired name out of them. `content/docs/blocks/block-schema.mdx` is deleted with the family, + and objectui#7023 — the narrower validator-only fix — dissolves into this retirement. +- 2d7304d: Retire the `MobileOverrides` type and its `mobileOverrides` mount point (objectui#4919, + maintainer ruling 2026-08-19, ADR-0049 enforce-or-remove). + + `MobileOverrides` published a six-key mobile override surface — `layout`, `columns`, + `useBottomSheet`, `fullScreen`, `touchTarget` and a three-value `navigation` vocabulary + (`'bottom-tabs' | 'hamburger' | 'drawer'`) — from `@object-ui/types` and, re-exported, + from `@object-ui/mobile`. Nothing read any of it. Measured on current `main`: the type had + exactly four mentions repo-wide — its own declaration, the single + `MobileComponentConfig.mobileOverrides` mount point, and the two barrel re-exports — and + the lower-case property name (the spelling a renderer would actually read) appeared only + in that declaration. No renderer, hook or adapter resolved it, and a sweep of the example + apps and the `objectstack` sibling checkout found zero authors. The three `navigation` + values were three spellings of the same no-op. + + The declared surface is removed rather than narrowed. The #3985 lineage's rule is "narrow + to the implemented values"; here the implemented set is empty, so that rule terminates in + deletion — a config that type-checks, builds and silently does nothing is the + declare-without-enforce shape the platform doctrine forbids. + + Removal rather than a `?: never` tombstone follows this package's own discriminator. A + tombstone exists to steer authors to a named live replacement — `crud.ts` `confirm` → + `confirmText` (objectui#4314), `data-display.ts` `hoverable` / `striped` → `data-table` + (objectui#5474) — or to keep a key loud that the docs had actively taught as working. + Neither applies: there is no replacement key to steer to, no documentation ever described + the surface, and there is no successor spelling. That is the same zero-pull, no-successor + shape as the retired `AccordionItem.icon` (objectui#4652) and `ToggleGroupItem.icon` + (objectui#4632), both of which were removed outright rather than tombstoned. + + **Breaking for TypeScript authors of `MobileOverrides` / `mobileOverrides` only** (marked + `minor` per this repo's version-alignment rule, which reserves `major` for following + `@objectstack` across a major — see AGENTS.md's 版本号策略, and the identical + classification used for `AccordionItem.icon`). Runtime behaviour is unchanged: an authored + `mobileOverrides` did nothing before and does nothing now. What changes is that the + contract no longer claims otherwise, so the mistake surfaces at authoring time — importing + the type is now a "has no exported member" error, and authoring the key on a + `MobileComponentConfig` object literal is an excess-property error, instead of a silent + no-op that type-checks and builds. + + If real mobile-override renderer work is ever wanted it re-enters deliberately, as designed + product surface on its own card, with the renderer landing in the same change as the + declaration — not by resurrecting this declaration. +- 636b236: `navigateOnSuccess` is relative-only, escapes the interpolated id, and is deprecated in favour of `submitBehavior` + + The url contract for this key was undeclared: it was same-origin-guarded (so a same-origin + ABSOLUTE value was accepted), it interpolated `{id}` / `{recordId}` without escaping the + substituted value, and nothing said which of those was intended. The maintainer ruled it on + 2026-08-17: `navigateOnSuccess` is the pre-ruling ancestor of the `submitBehavior` family + rather than a second dialect, so as a compat alias it runs under the semantics + objectstack#7496 ruled for that family. + + **Relative paths only.** A same-origin absolute such as `https://own-host/record/{id}` is + now refused like any other out-of-contract value, rather than accepted and navigated at + browser level. The destination is authored metadata, which is exactly where an address + somebody else chose gets copied in. Cross-origin and protocol-relative values were already + refused and still are; every relative shape that worked before still works. + + **The interpolated id is URL-escaped.** `/r/{id}` with an id of `a/b c` resolved to + `/r/a/b c`, silently growing a path segment, and a template of `{id}` let the id become the + whole destination. The substituted value now goes through `encodeURIComponent`, so a token + is a value in the path and never a way to add path structure. The template is the author's + and is untouched — only the id, which is data read off the written record, is escaped. + + Both halves are needed and neither implies the other: relative-only is a rule about where a + destination starts, so it cannot see structure injected further along; escaping runs only on + the substituted value, so it cannot see an absolute the author wrote out. + + This can only narrow what is reachable. Every destination the key now accepts is a relative + reference, and a relative reference cannot carry an authority, so it was already accepted by + the same-origin guard this replaces — no value that was refused is now followed. With every + accepted destination relative, the browser-level `window.location.assign` fallback at both + call sites became unreachable and was removed; an accepted destination goes to the injected + navigation seam, and the absent-seam fallback inside the shared hook is unchanged. + + **Deprecation.** `navigateOnSuccess` is marked `@deprecated` in favour of `submitBehavior`, + which already takes precedence over it and carries the richer `{{record.field_name}}` + interpolation. The `{id}` / `{recordId}` dialect keeps working for forms that already + declare it — the ruling converges the documentation and the semantics, not the spelling. +- d2fb6ef: **BREAKING (authoring): `ui:icon` names its glyph with `icon`, not `name`** + + `{ "type": "icon", "name": "check" }` no longer renders an icon. Write + `{ "type": "icon", "icon": "check" }`. Stored metadata authored before this + release needs converting — see the migration below. + + Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` + outside an `@objectstack` major sync); the break is real and is stated here. + + **Why** + + `name` is the SDUI identity key every authored node carries, alongside `id` — + it is not `ui:icon`'s private prop. So an ordinary node like + `{ type: 'icon', id: 'save_icon', name: 'save_icon' }` asked lucide for a glyph + called `SaveIcon`, missed, and rendered **nothing at all**: silent to a human, + and clean to a DOM gate, because a renderer that renders nothing spreads no + attributes to find. `action:*` already reads `icon`, so this is the vocabulary's + existing answer, and it leaves no node type on which the identity key is + unusable. + + **What changed** + + - `IconSchema` (types + its zod mirror) declares `icon: string` **required**, + exactly as `name` was required before it — a key rename at constant + strictness. `name` reverts to the optional identity inherited from + `BaseSchema`. The mirror previously *required* `name`, which is why the + renderer could not be migrated on its own: the published contract refused the + correct shape. + - `ui:icon` resolves its glyph from `schema.icon`. There is deliberately **no** + `icon ?? name` fallback: a key meaning "identity" or "glyph" depending on + whether a lucide lookup happened to hit is the ambiguity being removed. + - The registry's `inputs` entry and `content/docs/components/basic/icon.mdx` + moved in the same change as the resolver. + - All 98 authored icon nodes in this repo are converted. + + **The break is loud in three places, never silent** + + 1. `IconSchema` **refuses** a legacy node, with a message that names the rename + and points at the converter — not zod's default `expected string, received + undefined`. + 2. A legacy node that reaches the renderer unvalidated draws the visible + placeholder shipped in the previous release, and its `console.warn` now + carries the exact rename (`icon: "save_icon"`) plus the converter's name. + Its accessible name says so too, and it gains a + `data-objectui-icon-legacy-name-key` marker so a gate can tell + "unmigrated node" from "glyph that does not resolve". + 3. **Migration for stored metadata** — `migrateIconNodeKeys` from + `@object-ui/types`: + + ```ts + import { migrateIconNodeKeys } from '@object-ui/types'; + + const { document, converted, warnings } = migrateIconNodeKeys(storedPage); + if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n')); + if (document !== storedPage) await save(document); + ``` + + It walks the whole document and lifts `name` to `icon` on every icon node. + It is a one-shot conversion a deployer runs over stored documents — **not** a + read-path fallback; nothing calls it during rendering or parsing. It + **reports rather than guesses** for the two cases it will not touch: a node + already declaring both keys (`icon` wins, `name` stays the identity it is), + and a node naming no glyph at all. +- fc62bb4: `TableColumn.type` now has ONE canonical value set across all three ends that disagreed + (objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is + canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | + 'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted + anything; the renderer branched on a third set and could only read the key through an + `as any` cast. + + ## ⚠️ Accept-set narrowing — these spellings stop validating + + `TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now + `z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse + time with `type` named in the error path. Spellings that validated before and are **refused + now**, grouped by why they were being written: + + - **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, + `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is + the card's headline case: it validated, matched no renderer branch, and the column fell + through to plain text rendering with nothing reported. That silent fall-through is the + lenient-validation face that lets AI-authored metadata errors through, and it is now a + loud parse failure. + - **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, + `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of + `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, + not on the column: a column gets its dedicated widget from the field definition behind its + `accessorKey`, never from `type`. + + **Authored metadata in this repo needs no migration.** Measured before tightening, across + `examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files + plus the docs and playground sources): **zero** authored `TableColumn.type` values outside + the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column + position anywhere in the repository. If you author `type` on a table column, check it + against the eight; if the value describes the FIELD rather than the column, remove it. + + ## The renderer's undeclared vocabulary disappears instead of being declared + + `int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` + and `datetime-local` had its own editor branch, none of them declared. They arrived because + column-inference producers forwarded an object schema's field type **verbatim** into + `TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred + value onto the declared vocabulary at their emit seam via the new + `normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, + `datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the + column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) + and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote + the raw field type into the same slot. + + Dropping the annotation is behaviour-preserving at the only consumer that reads the key. + `data-table`'s inline editor branches on `date`, `datetime` and the numeric set and + otherwise falls through to a text input — which is exactly the `undefined` path. The + dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, + which resolves the field through `column.accessorKey` and never reads `type`. + + ## New public API + + `@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single + declaration the zod mirror builds its enum from, so the two cannot drift), the + `TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast + in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared + spelling is a tsc error rather than a silent widening. + + A value-level parity pin covers all three ends + (`packages/types/src/__tests__/table-column-type-canonical.test.ts` and + `packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). + objectui#5684's guard is key-set only and cannot see value drift — `type` was present on + both sides the whole time — which is how this instance survived while its siblings were + caught. A future inference value turning that pin red is by design; the note at the pin says + so, and names the two correct repairs. +- 41df893: `ObjectTrigger` and `ObjectRelationship` are removed from `@object-ui/types` — two + hand-written interfaces orphaned by the `ObjectSchemaMetadata` derivation + (objectui#5859, triage adjudication 2026-08-24; the derivation itself was + objectui#5362). + + **Breaking for anyone importing either name.** The two symbols are, verbatim: + + - `ObjectTrigger` — the `{ name, when, on, condition?, action, config? }` trigger + configuration + - `ObjectRelationship` — the `{ name, object, type, foreign_key?, cascade_delete? }` + relationship configuration + + Both existed solely to type members of the retired hand-written object-document mirror: + `triggers?: ObjectTrigger[]` and `relationships?: ObjectRelationship[]`. objectui#5362 + replaced that mirror by deriving `ObjectSchemaMetadata` from `@objectstack/spec/data`'s + `ServiceObject`, and the spec's object document declares neither member — so the two + interfaces have had nothing to type since. objectui#5362 deliberately left them standing + because cutting published exports is a separate decision from the ruled derivation; this + is that decision. + + Measured before removing, on `main`: zero references in this repo outside the declaration + and the `src/index.ts` re-export (`packages/`, `apps/`, `examples/`, `*.ts`/`*.tsx`, + `node_modules` and `dist` excluded), and zero in the sibling `objectstack` checkout, which + does import `@object-ui/types` in eleven files. Absence of a spec correspondence was + verified at type level against the installed `@objectstack/spec` 17.2.0 rather than + inherited from the card: neither `triggers` nor `relationships` is a key of + `ServiceObject`. `DataModelDesigner`'s `relationships` state is its own local model and + never referenced these types. + + **That measurement cannot see npm.** In-repo zero is not consumer zero — an external + application importing either name from `@object-ui/types` will fail to compile after this + release, and nothing in this repository can detect that. Both names are spelled out above + so a host can search its own sources for them. If you were importing either, the shapes + were client-side vocabulary with no runtime behaviour and no spec backing: copy the + interface into your own code, or model the concept against `@objectstack/spec`, which owns + the object document. + + Type-only change; nothing is emitted and no runtime behaviour moves. Ships `minor`, not + `major`, per the version-alignment convention in AGENTS.md — objectui's major tracks + `@objectstack`'s, and breaking changes of objectui's own carry `minor` with the semantics + stated in the body. +- 00f3eb5: `ObjectGanttSchema` declares the ten gantt keys `ObjectGantt` actually reads + (objectui#5903, triage 2026-08-24). Every one is a real, working, documented + feature — `readOnly`, `mobileReadOnly`, `markers`, `navigation`, `skipWeekends`, + `holidays`, `criticalPath`, `showBaselines`, `persistLayout`, `viewName` — and + none of them was discoverable from the published type, because all ten were read + as `(schema as any).K`. The cast was the load-bearing part: it kept the read + invisible to `tsc`, to the zod mirror and to the designer's registry `inputs`. + + Both halves move together. The TS declaration (`packages/types/src/objectql.ts`) + and its zod mirror (`src/zod/objectql.zod.ts`) gain the same ten keys at the same + requiredness — all optional — so the `zod-mirror-parity` ratchet stays at zero + drift for this pair and no `KnownDrift` entry is added. `navigation` is taken + from `@objectstack/spec`'s `NavigationConfigSchema` by reference rather than + restated, matching `ObjectGridSchema.navigation`. + + `ObjectGanttProps.schema` is retyped from `ObjectGridSchema` to + `ObjectGanttSchema`. That is what makes the declaration load-bearing: the ten + keys are not grid keys, so with the old prop type, dropping the casts would have + left the reads landing on `BaseSchema`'s index signature — the same invisibility + in different syntax. The grid-style `{ gantt: { … } }` block is unaffected; + `getGanttConfig` reads it through that index signature exactly as before, and the + registered renderer passes `schema: any`, so no runtime shape is turned away. + + Accept-set change, stated plainly: a **declared** key is now type-validated, so + `readOnly: 'yes'` is refused where it used to parse green — the same narrowing + objectui#5074 landed for `viewMode`. An **undeclared** key is still accepted: + `BaseSchema` is `.passthrough()` and carries an index signature (objectui#5155's + structural ceiling), so declaring these ten did not buy rejection of a + misspelling. `packages/types/src/__tests__/gantt-declared-keys.test.ts` pins both + halves so neither can be misread. + + The eleventh reported key, `label`, needed no declaration — `BaseSchema` already + carries it — so only its cast was dropped. +- 1ec291c: Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 + enforce-or-remove, maintainer ruling 2026-08-31, option B). + + `inputType` was held back when `min` / `max` / `step` / `placeholder` were retired, because + its defect was a different one. Those four were declared-and-UNREAD. `inputType` was + declared-and-DROPPED: the repository really did author it — `packages/plugin-markdown` + wrote `inputType: 'textarea'` on its `content` input, pinned by that package's own test — + while the manifest serializer dropped it. Retiring it therefore had to decide what that + registration should say instead, which is the fork the card reported and the ruling closed. + + FROM → TO: + + - `inputType?: string` → **tombstoned** (`?: never` on the interface, `retirementTombstone()` + named refusal on the Zod mirror). Put the control hint in `description`, which IS + published. + - `plugin-markdown`'s `inputType: 'textarea'` write → **deleted**, at zero capability cost. + + The write was measured as a no-op before it was deleted, and re-measured on this branch's + base rather than inherited from the card. A structural census over every `inputs:` array in + the repository (211 regions, all tracked TS/TSX/JS sources) scores `inputType` at exactly + ONE authoring site — the `plugin-markdown` registration — against `name` 953, `type` 969, + `label` 966, `description` 194, `enum` 119, `required` 86 and `binding` 4 in the same pass + over the same regions, so the instrument was not blind. The other 192 in-repo `inputType` + hits are a DIFFERENT face: `FormField.inputType` (`zod/form.zod.ts`), the text-input + renderer's prop, and `SchemaBuilder.inputType`, none of which sit on a `ComponentInput`. + The publication path is unchanged and was re-confirmed: `packages/sdui-parser/src/index.ts` + forwards exactly six keys per input — `name`, `type`, `required`, `enum`, `binding`, + `description` — so an authored `inputType` could not reach the published + `sdui.manifest.json` even in principle. + + Option A — teach `sdui-parser` to forward the key — is REFUSED on record. The only thing + that looked like demand for it was a write that had never taken effect, and a write nothing + reads is not demand for a feature. The neighbouring 2026-08-17 expression-ceiling ruling + (quoted on `ComponentInput.type`) is untouched and stays deferred, with its reopen + condition — a measured case of an author shipping a spec-rejected value objectui's silence + let through — unchanged. + + Deleting the member outright was again the option NOT taken, for the reason the four + siblings established: `ComponentInputSchema` is a non-strict `z.object`, so an undeclared + key is silently STRIPPED. The tombstone is what converts a write from OUTSIDE this + repository — the half objectui#5905 could not measure — into a named refusal carrying its + own remedy, with `code: 'invalid_type'` and the key named in the issue `path`. + + Accept-set change, stated plainly for reviewers: a document that sets `ComponentInput.inputType` + used to parse GREEN (the value was then dropped by the serializer) and now parses RED. That + is the intended effect and the reason this carries a contract-review label. + + Three pins were FLIPPED rather than deleted, so the closure stays asserted instead of + becoming a silent absence: `plugin-markdown`'s `index.test.ts` (which asserted the write) + now asserts the key's absence plus a `tsc` refusal at that package's own authoring site, + and the two fork-half controls in + `packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts` — one + type-level, one parse-level — now assert refusal where they asserted liveness. + + Stale wording corrected in the same pass, because this change falsifies it: `base.ts` and + `zod/base.zod.ts` both said the fork was "recorded for a ruling; until then this stays a + live, writable key", and `widget.ts` called it "the open fork". All three now record the + ruling. A reader who greps the source instead of the card thread was meeting an open fork + that no longer existed. +- 453dbaa: Retire `ComponentInput`'s four inert constraint keys — `min`, `max`, `step` and + `placeholder` (objectui#5905, ADR-0049 enforce-or-remove). + + All four were declared on `ComponentInput` and read by nothing, on either path. No consumer + reads them off a `ComponentInput` value, and the manifest serializer + (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, `type`, + `required`, `enum`, `binding`, `description` — so a value authored here could not reach the + published `sdui.manifest.json` even in principle. Re-measured on this branch's merge-base + rather than inherited from the card: a structural census over every `inputs:` array in the + repository (219 regions, all tracked files) scores `min` **0**, `max` **0**, `step` **0** + and `placeholder` **0**, against `name` 926, `type` 926, `description` 161, `enum` 114 and + `required` 87 in the same pass over the same regions — the instrument was not blind. + + FROM → TO, per key: + + ⚠️ The four are **TOMBSTONED, not removed** — the declaration stays, the key becomes + unwritable. An earlier draft of this list said "removed", which contradicted the paragraph + below it and described the option this change deliberately did NOT take. + + - `min: number` → **tombstoned** (`?: never`, named Zod refusal). Spell the numeric domain + out in `description`, which IS published (`'A positive integer — the contract rejects 0 + and fractional values'`). + - `max: number` → **tombstoned**. Same remedy. + - `step: number` → **tombstoned**. Same remedy. + - `placeholder: string` → **tombstoned**. Put the hint in `description`. ⚠️ + `BaseSchema.placeholder` — the node-level prop a renderer does read — is a DIFFERENT key + and is unaffected. + + The retirement kit: `?: never` on the interface (`packages/types/src/base.ts`), so authoring + one is a `tsc` error at the registration site; `retirementTombstone()` on the Zod mirror + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the + message. Deleting the members outright was the option NOT taken: `ComponentInputSchema` is + a non-strict `z.object`, which strips an undeclared key silently — one silent no-op traded + for another. Pinned in + `packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts`. + + Two limits worth stating rather than papering over: + + - The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes + these keys is **not measurable from here** (the same limit objectui#5674 recorded for + `PluginComponentInput`). Converting such a write from a silent drop into a named refusal + is exactly what the tombstone buys. + - The fifth key objectui#5905 named, `inputType`, is **NOT retired here**. + `packages/plugin-markdown` authors it (`inputType: 'textarea'`), so it is + declared-and-DROPPED — a different defect that needs a ruling, not a removal. That + ruling landed on 2026-08-31 and `inputType` is tombstoned in the follow-up change; this + note records the state as of THIS change, which is what a changeset is for. + + This is not a verdict that constraint slots on `ComponentInput` were a mistake. The + neighbouring `type` field carries a maintainer ruling of 2026-08-17 recording that giving + `ComponentInput` real constraint slots was **deferred, not rejected** — `min`/`max`/`step` + read exactly like the slots that ruling declined to add. What is retired is this inert + spelling; the ruling's own reopen condition still stands. +- 69a2163: **`@object-ui/types/zod` now accepts seven spellings its own TypeScript declarations already declared** + + Seven keys across five hand-written zod mirrors refused values the published TS + types invite and the renderer implements — `declared !== enforced` on a published + validator. The mirrors are widened to their declarations. Nothing is narrowed and + nothing previously accepted is rejected, so this is additive for every author and + every host: schemas that parsed before still parse. + + The newly-accepted spellings, so a host can search for them: + + | schema (`@object-ui/types/zod`) | key | now also accepts | + |---|---|---| + | `ButtonGroupSchema` | `variant` | `secondary`, `destructive`, `ghost`, `link` | + | `ButtonGroupSchema` | `size` | `icon` | + | `ObjectChartSchema` | `chartType` | `column`, `horizontal-bar`, `donut` | + | `FormSchema` | `validationMode` | `onTouched`, `all` | + | `SelectSchema` | `defaultValue`, `value` | `boolean` | + | `DataTableSchema` | `selectable` | `'single'`, `'multiple'` (alongside `boolean`) | + | `ViewSwitcherSchema` / `ViewTypeSchema` | `defaultView`, `activeView`, `views[].type` | `chart` | + + **Each one was decided by measuring the renderer, not by matching the declaration.** + Widening a mirror to its declaration is only correct where the running code + implements the missing spelling; where a spelling is dead, the right fix is to + withdraw it from the declaration (ADR-0049 enforce-or-remove), not to teach the + validator to accept something that renders nothing. The read sites: + `buttonVariants`' `cva` map (`components/src/ui/button.tsx`) carries all six + variants and all four sizes; `AdvancedChartImpl` normalizes `column` to `bar`, + maps `horizontal-bar` to a real `BarChart` layout and gives `donut` its own inner + radius; `useForm({ mode })` hands `validationMode` straight to react-hook-form, + whose `isOnTouch` / `isOnAll` branches implement `onTouched` and `all`; + `toControlValue` / `matchOptionValue` (#3090) round-trip a boolean option value + with its type intact; `resolveSelectionMode` implements `'single'` as + replace-on-select with no select-all header, distinct from `'multiple'`; and + `chart` is a rendered view type with its own `case` in both `ListView` and + `ObjectView`. + + Consumer-visible type effect: `z.infer` of these schemas widens accordingly. + Widening an input contract cannot break a caller that was already passing a + narrower value, but code that exhaustively switches on the inferred union — e.g. + a `switch` over `chartType` with no `default` — will want the new arms. + + Refs objectui#5927 (group A of the 17 measured mirror drifts). The remaining + classes are rulings rather than edits and stay in the `KnownDrift` ledger. +- 24e027e: `@object-ui/types/zod`: the zod const `StylePropsSchema` is renamed to `ClassNameStylePropsSchema` (objectui#5928). **The old name is gone** — there is no deprecated alias and no second spelling. Import `ClassNameStylePropsSchema`. + + **What moves on the published surface.** `StylePropsSchema` is removed from `@object-ui/types/zod`; the same object is exported under the new name with the same accept set, so nothing that parsed before parses differently and nothing refused before is accepted now. The break is the name alone: an import of `StylePropsSchema` no longer resolves. + + **Why the name had to move.** The const declares exactly two keys — `className` and `style`, the CSS passthrough attributes a node exposes. The TypeScript `StyleProps` (`base.ts`) is the Tailwind-SCALE vocabulary: `padding`, `margin`, `gap`, `backgroundColor`, `textColor`, `borderWidth`, `borderColor`, `borderRadius`. Measured on this branch's base with an AST read of both files: 2 keys against 8, sharing ZERO keys. In this package the `…Schema` suffix otherwise means "runtime mirror of the like-named declaration", so the shared name asserted a mirror relationship that does not exist — and building objectui#5684's parity registry by name pairing duly put the two together and reported drift on a pair that has no counterpart at all. + + **Where the non-pair is recorded now.** `zod-mirror-parity.test.ts` keys its existing `EXCLUSIONS` entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to `ClassNameStylePropsSchema`. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for. + + **No deprecation window, deliberately.** No consumer of the old name exists in this repository. Measured on this branch's base: `StylePropsSchema` had exactly three references — the definition, the barrel line, and the guard's own exclusion key — all three inside `packages/types` (lit control on the same query shape: `BaseSchema` matches 251 tracked files). A staged retirement would need named external-consumer evidence, and there is none. +- 2c3cd1b: BREAKING (`@object-ui/core`): `ActionRunner`'s legacy `ActionDef.onSuccess` + chained-callback channel is retired — `onSuccess` now has exactly the meaning the + contract declares (objectui#5934, maintainer ruling 2026-08-31). + + (The bump is `minor` by this repo's release model — objectui's major is pinned to + the `@objectstack` family major, and its own breaking changes ship as `minor` with + the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This + paragraph is that spelling-out: the break below is real and consumer-visible.) + + - **What breaks, by specifier**: `import type { ActionDef } from '@object-ui/core'` — + `ActionDef['onSuccess']` was `ActionDef | ActionDef[]` (chained callbacks the runner + dispatched through `executeChain` after a success). It is now derived from the pinned + spec: `ActionSchema.onSuccess`'s closed strict `{ navigate: string, openIn?: 'self' | + 'newTab' }` block. Code that assigned a callback `ActionDef` (or an array of them) to + `onSuccess` no longer compiles, and at runtime a callback-shaped value gets NO reading — + no handler dispatch, no navigation, the action's own result untouched. `onFailure` is NOT + changed: the spec declares no such key, so it keeps its one runner-native meaning. + - **Why this is safe to take**: the channel was unreachable from validated metadata — + `@objectstack/spec` (17.2.0 pin) strict-refuses a callback shape inside `onSuccess` at + parse (`invalid_type` on `navigate` + `unrecognized_keys`), so no published/saved + metadata could ever carry one — and a producer census with a positive control found zero + producers outside the channel's own test pins. Migration for an out-of-repo consumer that + drove the channel programmatically: put the follow-up actions in `chain` (the runner's + declared chaining key, unchanged), or author the spec's `onSuccess` navigation block. + - `@object-ui/types` (minor): `UIActionSchema` now declares `onSuccess`, derived from the + spec's `ActionSchema.onSuccess` — the renderer view spells the key the four action + surfaces forward, so the forwards type-check. + - `@object-ui/components` (patch): the four action renderers forward `onSuccess` without + the `as any` casts (no behavior change — same key, same value, now typed). +- 90665e0: **Removes a published export.** Retire the `MobileComponentConfig` type + (objectui#5942, ADR-0049 enforce-or-remove). The name is deleted from + `@object-ui/types` and from `@object-ui/mobile`, which re-exported it — after + this release `import type { MobileComponentConfig }` from either package is a + compile error, not a deprecation warning. + + `MobileComponentConfig` published a four-key "mobile component schema + extension" — `responsive`, `gestures`, `pullToRefresh` and `infiniteScroll` — + and nothing read it. Re-measured on current `main` before anything was deleted: + the type had exactly four code mentions repo-wide — its own declaration, one + doc-comment cross-reference, and the two barrel re-exports. It had **no mount + point at all**: no type mounted it as a property, nothing extended it, and no + renderer, hook or adapter annotated, cast to or imported it. A sweep of the + example apps and the `objectstack` sibling checkout found zero authors. Every + read-shape probe returned zero against a control lit in the same run. + + That makes it stricter than the usual case: not merely a surface whose values + were unimplemented, but a container with no path by which any authored value + could reach a renderer. objectui#4919 removed its last member + (`mobileOverrides`), which is what left the container itself inert. + + Removed outright rather than kept as a `?: never` tombstone, on this package's + own discriminator: a tombstone steers authors to a named live replacement key + (`crud.ts` `confirm` to `confirmText`; `data-display.ts` `hoverable`/`striped` + to `data-table`), or keeps loud a key the docs taught as working. Neither + applies — the whole interface goes, so there is no surviving object to hang a + `never` key on, and no documentation ever described it + (`skills/objectui/guides/mobile.md` teaches the hooks, never this type). Same + zero-pull, no-successor shape as `MobileOverrides` (objectui#4919) and + `AccordionItem.icon` / `ToggleGroupItem.icon`. + + ## Upgrading + + **No behaviour changes and there is nothing to migrate at runtime.** An object + authored against this type did nothing before and does nothing now; what + changes is that the contract no longer claims otherwise, so the mistake + surfaces at authoring time instead of silently type-checking. + + - **You imported the type only** (the only thing that was possible — nothing + accepted it as a value): delete the import. If you kept a local config object + annotated with it, drop the annotation; the object was never passed anywhere + that read it. + - **You actually wanted the behaviour:** it exists, and it is not being + retired. It lives in `@object-ui/mobile` as React hooks, which is where the + working code always was — `useResponsive` / `ResponsiveContainer` for + `responsive`, `useGesture` for `gestures`, `usePullToRefresh` for + `pullToRefresh`. `infiniteScroll` has no hook; it was never implemented in + any form. See `skills/objectui/guides/mobile.md`. + - **You want a declarative mobile config surface:** that re-enters deliberately + as designed product surface on its own card, with the renderer that reads it + landing in the same change as the declaration — not by restoring this + declaration. + + **Do not follow the compiler's suggestion.** TypeScript reports the removal from + `@object-ui/types` as TS2724 and appends `Did you mean 'ComponentConfig'?`. That + is a lexical near-match, not a migration target: `ComponentConfig` is the + renderer **registration** record (`{ type: string; component: T }`, extending + `ComponentMeta`) and has nothing to do with mobile configuration. The import + from `@object-ui/mobile` gets a plain TS2305 with no suggestion at all. + + Marked `minor`, not `major`, per this repo's version-alignment rule, which + reserves `major` for following `@objectstack` across a major (AGENTS.md + 版本号策略) — the same classification objectui#4919's identically breaking type + removal used. **Breaking for TypeScript consumers of the name only.** + + Follow-up, deliberately not widened into this change: `MobileResponsiveConfig` + and `GestureConfig` were consumed only by this container and are now + zero-consumer published types themselves. Filed as objectui#7519 for triage. +- 7e19d03: **Breaking for authored metadata:** `DashboardWidgetSchema` (the zod validator + in `@object-ui/types/zod`) is now `.strict()` — an undeclared widget key + REFUSES the parse with zod's `unrecognized_keys` issue naming every offending + key, instead of being silently deleted. + + Before this change the schema was a plain `z.object()`: a widget carrying + `zzcanary` / `categoryField` / `aggregate` parsed five-keys-in, three-keys-out, + verdict ACCEPT — the same "dropped without a word" failure the schema's own + docstring records from the pre-derivation hand copy, still live for every key + no contract declares (objectui#6002). Maintainer ruling 2026-08-25, Route 1 + two-step: objectui#6150 declared the 13 genuinely-consumed keys first (landed + as PR #6945), then this flip makes a stale or mistyped key loud everywhere the + contract is consulted (`objectui validate`, `safeValidateSchema`, the catalog + gate) instead of only inside one catalog test. + + **Who is affected — a widget authoring a key outside the declared surface:** + the retired pre-ADR-0021 inline analytics shape (`object` / `categoryField` / + `valueField` / `aggregate`) is the canonical case — it used to validate clean + with all four keys deleted; it now refuses with the keys named. The spec's + tombstoned keys (`actionUrl` / `actionType` / `actionIcon` / `aria` / + `responsive`) keep their specific removal messages — they are declared + `z.never()` members, so they do not degrade to a generic unknown-key error. + + **Not affected:** a `metric-card` COMPONENT node in a dashboard's widget slot. + Its props (`value` / `icon` / `trend` / `trendValue` — registry inputs, not + widget keys) stay legal: per the 2026-08-14 ruling (objectstack#8593) a + component node is owned by objectui's own passthrough `BaseSchema`, and + `DashboardComponentSchema`'s widget slot now routes component-enum types there + before the strict widget schema is consulted. The legacy + `{ id, component, layout }` envelope also still parses. The repo-wide corpus + preflight (575 JSON files, every dashboard-bearing doc fence, all designer + emit paths) measured **zero** newly-refused widgets. +- 864154e: The Field Designer no longer offers a formula-expression textarea, and no designer write + path emits a `formula` key (objectui#6043). + + **This is a behaviour change on an authoring surface: a control is removed.** A field's + `type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the + palette — but the expression itself is no longer authored here. Authors write formula + expressions in metadata-admin's field inspector, where they are checked. + + The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against + the installed `@objectstack/spec` 17.2.0: + + ``` + FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) + => success = false + => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?" + ``` + + so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because + the key was then stored, it blocked **every later save of that object**, not just the one + that introduced it. + + **The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges + the key name and never the expression LANGUAGE — measured, it accepts + `expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the + empty string is refused. Spec `expression` is CEL rooted at `record` + (`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity` + — bare field refs, which under the scope formulas bind evaluate to null silently. A rename + would therefore have converted a loud, immediate 422 into a formula that saves clean and + then quietly computes nothing, which is strictly worse than the bug it appears to fix. + + Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType` + inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and + app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a + dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a + feature, not this fix. `returnType` is likewise not authored here: it is only derivable by + inferring the CEL result type, and with no expression control there is nothing to infer + from. + + `formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already + carrying the key is stripped clean on its next save instead of staying blocked forever — + which matters more than usual here, because with the control gone an author would + otherwise have no way left to clear it. It is dropped rather than migrated to `expression`, + for the same reason the rename was refused. A `expression` authored in metadata-admin is + **not** touched: it is a real `FieldSchema` key and rides through the designer's + round-trip untouched. + + Also removes the now-unreachable `formula` read/write from + `views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for + the key that neither the card nor the parity gate named. + + The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s + `KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that + left a stale entry behind would be as red as a new offender. +- b023625: The field metadata payload no longer emits `sortOrder`, the key `FieldSchema` refuses by + name (objectui#6045). Field-level sibling of objectui#6223, same objectui#5761 family. + + Measured against the installed `@objectstack/spec` 17.2.0, whose `FieldSchema` accept set + is 71 keys: + + ``` + FieldSchema.safeParse({ type:'text', label:'L' }) => success = true (control) + FieldSchema.safeParse({ type:'text', label:'L', sortOrder: 3 }) => unrecognized_keys ["sortOrder"] + + FieldSchema.safeParse({ type:'text', label:'L', sortable: true }) => success = true (control) + FieldSchema.safeParse({ type:'text', label:'L', sortable: 3 }) => success = false + ``` + + The control is what makes that a key-by-key result rather than a schema refusing + everything, and the `sortable` pair is what shows the near-spelling is a *different + concept* — a boolean ("whether field is sortable in list views"), not this key's spec + name. + + **The resolution was deletion, not a rename**, which is objectui#4687's shape rather than + objectui#6041's. The spec has no field-level ordering key at all: it models field order by + **declaration order** in the object's `fields` record, so a designer that wants explicit + ordering reorders that record rather than carrying an index. There was nothing to map onto, + and nothing was invented to map onto. + + **It was latent, and that is confirmed on today's tree.** Neither of the two sites that + construct a `DesignerFieldDefinition` — `FieldDesigner`'s create/update handlers and + `MetadataFieldsPage.toDesignerField` — ever named the key, so `toFieldPayload` emitted + `sortOrder: undefined` and `JSON.stringify` dropped it. The key never reached the wire. It + was one reorder feature away from doing so, which is the objectui#4644 shape: a hard 422 + `INVALID_METADATA` that blocks every subsequent save of the object, with nothing in the UI + to say which key caused it. + + Removed in one go from the wire shape (`FieldMetadataPayload`), its writer + (`toFieldPayload`) and the UI model (`DesignerFieldDefinition`), so no declaration is left + behind that no writer fills and no schema accepts. + + **Breaking for TypeScript consumers**: `sortOrder` is gone from `DesignerFieldDefinition` + (`@object-ui/types`) and from `FieldMetadataPayload` (app-shell), so code that set either + stops compiling. + + Two keys share this spelling and are untouched, which is why the census was on the *shape* + — a field-metadata payload key `FieldSchema` refuses — rather than on the identifier: the + **object-level** `sortOrder` (`ObjectSchema`'s, removed from the object wire shape by + objectui#6223 and deliberately kept on the `ObjectDefinition` UI model) and the + **saved-view** `sortOrder` in `ObjectView`, which is per-view display order on a different + document entirely. + + The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes + with the fix — that ledger ratchets in both directions, so an entry left behind for a + resolved key is as red as a missing one. +- 75bd83d: `ObjectGanttSchema` declares the flattened `GanttConfig` face `ObjectGantt` + actually reads (objectui#6051). `getGanttConfig` has two branches: when + `startDateField` and `endDateField` are both present at the TOP level it builds + its config from top-level keys and returns early; otherwise it reads the `gantt` + block. Everything the first branch reads was undeclared — and unlike + objectui#5903's ten, none of it was hidden behind a cast. `BaseSchema` carries + `[key: string]: any` (objectui#5155's structural ceiling) and the helper's + parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as + `any` with no syntax anywhere to grep for. That is also why the census here is an + AST enumeration and not a compile-and-observe: an index signature absorbs every + literal name, so annotating the parameter compiles clean while enforcing nothing. + + **27 keys join the declared surface, each additive and each with a live read + site.** 24 flattened `GanttConfig` members — `colorField`, `borderColorField`, + `dependenciesField`, `parentField`, `typeField`, `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `tooltipFields`, `baselineStartField`, + `baselineEndField`, `groupByField`, `resourceView`, `assigneeField`, + `effortField`, `capacity`, `quickFilters`, `autoZoomToFilter`, `timeSegments`, + `interactions`, `exportFileName`, `timeZone`, `dependencyTypes` — plus the three + query keys the fetch path reads, `staticData`, `filter` and `sort`. Nothing is + declared that the renderer does not consume. + + **`GanttConfig` itself gains nine members and is a published type**, exported by + name from `packages/types/src/index.ts`: `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, + `timeZone`, `exportFileName`, `interactions`. The entry file's diff is empty only + because the export list already named the type — the widening happened at the + declaration. + + **The 28th measured key, `gantt` (the block face), is deliberately NOT declared** + — see the closing section. + + The 24 are DERIVED from `GanttConfig` rather than restated, so the flat spelling + cannot fork from the block spelling, and the invariant is pinned in the type + system: every key of `GanttConfig` must be declared at the node's top level. + Making that derivation possible moved nine members — `lockField`, `objectField`, + `summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, + `timeZone`, `exportFileName`, `interactions` — out of `plugin-gantt`'s + package-private `GanttConfigEx` and into `@object-ui/types`' `GanttConfig`. They + are a MOVE, not new vocabulary: the `gantt` block already honoured all nine, and + a type private to the plugin could be referenced by neither authoring face. + + Both halves move together, as in objectui#5903: the TS declaration and its zod + mirror gain the same 27 keys at the same requiredness (all optional), the + spec-modelled ones taken from `GanttConfigSchema.shape` by reference, so the + `zod-mirror-parity` ratchet stays at zero drift for this pair and no `KnownDrift` + or `UnmirroredDeclared` entry is added. The mirror builds the flat face and the + `gantt` block from one field map, so they are one schema expressed twice. + + Accept-set change, stated plainly. All 27 keys are additive — every one is + optional, and nothing previously legal loses its slot. What changes is that a + **declared** key is now type-validated, so `capacity: 'one'` and + `summaryExtent: 'parent'` are refused where they used to parse green. An + **undeclared** key is still accepted — `BaseSchema` is `.passthrough()`, so this + bought no rejection of misspellings. There is no narrowing anywhere in this + change. + + **`gantt` is severed on purpose (objectui#6475), not overlooked.** It is the 28th + key of the measured residue and a genuine read — `getGanttConfig`'s second branch + honours it in full — but it is the one key whose declaration would NOT have been + additive. It has no mirror entry today, so a block rides through `.passthrough()` + unvalidated; declaring it as `GanttConfig` means it gets parsed against the spec's + `GanttConfigSchema`, which REQUIRES `startDateField`, `endDateField` and + `titleField`, and `ObjectGanttSchema` reaches the CLI's `validate` / `check` + through `AnyComponentSchema`. A published CLI's refusal behaviour is decided on + its own card, where reviewers can see what they are approving; objectui#6475 + carries the full measurement, including the case FOR enforcing it (the renderer + already feeds that block to `GanttConfigSchema.safeParse` and warns, so enforcing + restores declared = enforced rather than inventing a contract). Today's behaviour + is pinned in the test file so the omission is a measured state, not a silent gap. + `packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts` pins both + halves so neither can be misread. + + Which face WINS is unchanged and was not decided here: the flat branch is checked + first and returns early, so a node carrying both spellings still renders the flat + one. (`plugin-map` had the opposite precedence ruled on in objectui#5018; no + equivalent ruling exists for gantt.) +- 971d387: ⚠️ **Behaviour change: an authored `FormSection.visibleWhen` that has been doing nothing + will now START HIDING SECTIONS.** Read this before upgrading if any of your metadata + authors a section predicate. + + `@objectstack/spec` declares `FormSection.visibleWhen` and this repo's spec bridge maps it + through, but every plugin-form layout renders a section header as a virtual + `section-divider` pseudo-field and none of them copied the predicate onto it. On the + object-view chain — the create/edit modal, the drawer, the split form, and the full-page + record form — the key was declared, mapped, carried, and then dropped one hop before + anything could evaluate it. The section rendered unconditionally, with no diagnostic + (objectui#6111). + + **Why nobody noticed, and why the fix is felt as a regression.** `visibleWhen` fails OPEN: + a section that renders is what you get when the predicate resolves TRUE, when the predicate + never arrives, *and* when the predicate faults. Those three worlds were indistinguishable, + so an app that authored a section predicate saw its section render and had no way to tell + that the rule was inert. Every such app has been running with the rule switched off, and + some will have been authored — or simply grown used to — that state. After this change the + predicate is evaluated for real, and sections that have always been visible will disappear + for the users the rule excludes. + + This is the intended ADR-0089 contract being delivered, not a new capability: the key was + already declared, already documented, and already honoured by the console form renderer. + The object-view chain was the one that silently ignored it. + + **Before upgrading**, audit any `sections[].visibleWhen` in your form-view metadata and + confirm each predicate says what you actually want, evaluated against `record` + + `current_user`. A predicate that was written speculatively, or left behind after a rework, + now takes effect. + + **Measured scope of the hide.** The predicate gates the section's HEADER row. The renderer + treats `section-divider` as presentational and holds no association between it and the + fields that follow it, so a false predicate removes the heading and the section's fields + keep rendering. The console renderer (`apps/console`) drops the whole `<section>`, fields + included. That divergence is real, is pinned honestly by this change's tests rather than + implied away, and is filed separately — it needs a renderer-side grouping contract, not + another line in a layout. + + Two hops were dropping the key and both are repaired: `ObjectForm` rebuilds each section + key by key when it delegates to Split/Drawer/Modal (and `ModalForm`'s own `groups` map does + it again), so a key those maps did not copy never reached the layout at all; and the six + `section-divider` synthesis sites across the four layout files. + + `@object-ui/types` gains the matching `ObjectFormSection.visibleWhen` declaration. +- ee851c3: The report authoring face declares what its own examples author (objectui#6121, + maintainer ruling 2026-08-25, Option A — fix the type producer, not the docs). + + - `ReportComponentSchema.exportConfigs` is now + `Partial<Record<ReportExportFormat, ReportExportConfig>>` instead of a TOTAL + `Record`. Configuring ONE export format no longer forces an author to declare + all five (`pdf`, `excel`, `csv`, `html`, `json`). The published runtime twin + was never total — `z.record(z.string(), ReportExportConfigSchema)` in + `@object-ui/types/zod` has all keys optional — so the TS declaration had been + stricter than the validator that actually judges authored JSON. This is a pure + relaxation: every literal that type-checked before still does. + + - `ChartDataSeries` gains the optional per-series family override `type` + (`'bar' | 'line' | 'area'`), with the same key added to its zod twin + `ChartDataSeriesSchema`. The renderer already reads it — + `normalizeChartSchema`'s `normalizeSeries` in `@object-ui/plugin-charts` + resolves the family as `str(raw.chartType) ?? str(raw.type)` — so `type` was + the author spelling of an override the type refused to declare. The union is + the three families that read honours, deliberately NOT the wider `ChartType`: + a wider union would advertise an override the normalizer drops in silence. + + NOT only a relaxation on the runtime side, and this is the half a consumer + needs before taking the bump. `ChartDataSeriesSchema` is a stripping + `z.object`, so a stored series carrying a non-family `type` — `type: 'pie'`, + say, copied from `@objectstack/spec`'s `ChartSeries`, whose `type` IS the + full `ChartType` — used to PARSE, with the unrecognised key dropped in + silence; it now FAILS. `ChartDataSeriesSchema` feeds `ChartSchema.series`, + so a consumer running `safeParse` over stored chart JSON newly gets + `invalid_value` at `series.N.type` where it previously got nothing. (Checked + against the package's own zod 4.4.3, both directions, with `type: 'line'` + and a series carrying no `type` as controls: both still parse.) What to do + about it: the rejected value never had an effect — `normalizeSeries` honours + exactly the three families and drops every other one with no error, no + warning and no output key — so the failure surfaces an override that was + already inert. Drop the `type` from the stored series, or, if the whole + chart really is that family, move it to the chart's own `chartType`, which + still takes the full `ChartType`. The TS side is widening-only; only the + published validator newly rejects. + + Both changes carry pins in + `packages/types/src/__tests__/report-schema-authoring-face.test.ts`: the + widenings fail if either is narrowed back, and the rejection above is pinned + openly as `rejects a family the normalizer would silently drop`. + + `ReportComponentSchema.dataSource` is NOT changed here — measuring the authorable + shape against the report runtime's actual read, which the ruling requires, + produced a fork the ruling did not cover. It is escalated on objectui#6121. +- 6414dfd: The two report data-source keys are retired on both faces (objectui#6121, + maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half; + ADR-0049 enforce-or-remove). + + **The accept set of a published validator moves** (`@object-ui/types/zod`): + + - `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value + parsed green and was then read by nobody. + - `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node + type no renderer is registered for at all. + + Both now carry `retirementTombstone(...)`: an authored value is refused at the + key's own path with `code: 'invalid_type'` and a message that names the key, says + why it is retired and points at the spelling that runs. Nothing that used to be + refused parses green. + + **The TypeScript face** — both keys become `?: never` rather than being deleted, + so an author who still writes one gets a `tsc` error at the authoring site + instead of a silently stripped key. They were annotated `DataSource` / + `DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which + no JSON document can author; that mis-annotation is the defect objectui#6121 was + filed for, since every example on `content/docs/core/report-schema.mdx` authored + a config object against it. + + **Why this is a retirement and not a rename.** No read site consumed either key: + `@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop + or from `SchemaRendererContext`, never off `schema.dataSource`, and the live + 9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences + measured zero in this repo and in the sibling `objectstack` checkout, whose + report metadata binds `dataset` throughout — the ruling's own deprecation-window + exit criterion. A stored document that still carries the key now fails loudly at + `safeParse` instead of being accepted and ignored; drop the key, and bind the + report through `dataset`. + + The replacement binding key the ruling names (`data?: ViewData`) is deliberately + NOT declared here, and is escalated on objectui#6121: `data` is already a live + key on `ReportComponentSchema` — the report ROW array, read by + `LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding + under that name would put two authoring contracts on one key inside one + renderer. + + Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`: + the `never` twins, the named refusals with their issue envelope, the `.describe()` + metadata channel, and controls that a report without the key still parses. +- a8d5c71: `@object-ui/types/zod`: the 58 `on*` handler keys declared `z.function()` now refuse BY NAME (objectui#6124) + + The zod mirrors declared 58 `on*` keys (26 distinct — `onClick`, `onChange`, `onOpenChange`, `onValueChange`, `onCardMove`, …) across `complex`, `data-display`, `disclosure`, `feedback`, `form`, `layout`, `navigation` and `overlay` as `z.function()`, a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` was already refused, with zod's bare `invalid_type … expected function, received object` naming the key and nothing else. + + Every one of the 58 sites is now a named refusal arm in the shape #5099 landed for `FieldConstraintsSchema.pattern.value` (`z.custom` + guidance, via `handlerKeyRefusal()` in `zod/tombstone.zod.ts`): the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established (`{ "type": "toast" }`, an `action:button` node with a declared action). The same text is the key's `.describe()` metadata — one string, two channels. Deleting the keys was measured and refused: under `BaseSchema.passthrough()` an undeclared key is not refused, it is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into the DOM listener slot where React throws at click. + + **Accept-set change (Clause ②).** A live function value — which parsed green before — is now refused on the JSON mirror too. The programmatic face reaches renderers through the TypeScript interface and React props, never through `safeParse`; on this tree the only runtime `safeParse` doors into these mirrors are the CLI validators and the exported `validateSchema` / `safeValidateSchema` helpers, none of which is fed a function-bearing object. Code that ran a host-supplied function through one of these mirrors must stop doing so. + + **TypeScript face, measured per key.** 36 keys whose function value reaches a renderer at runtime (read off `schema.*`, called as a React prop after `SchemaRenderer`'s spread, or spread onto a Radix root / DOM listener slot) keep their function type. 22 keys nothing reads carry the `?: never` tombstone (ADR-0049): `KanbanSchema.onColumnAdd` / `onCardAdd`, `CarouselSchema.onSlideChange`, `ChatbotSchema.onSendMessage`, `AlertSchema.onDismiss`, `ListItem.onClick`, `TreeViewSchema.onSelectChange` / `onExpandChange`, `ToastSchema.onDismiss`, `RadioGroupSchema` / `SwitchSchema` / `ToggleSchema` / `SliderSchema` / `CalendarSchema` / `ComboboxSchema` / `CommandSchema` `.onChange`, `InputOTPSchema.onComplete`, `BreadcrumbItem.onClick`, `SidebarSchema.onCollapsedChange`, `ButtonGroupButton.onClick`, `AlertDialogSchema.onConfirm` / `onCancel`. Assigning one of those is now a `tsc` error naming the key. + + Out of scope, per the ruling: the four non-`on*` `z.function()` keys (`cell`, `custom`, `validate`, `renderCellEditor`) stay as they are; `EventHandlersSchema` is objectui#6910's card. +- 88e9109: Declare the two consumed-but-undeclared field-metadata keys ruled on + objectui#6140 / objectui#6153 (maintainer 2026-08-25, Option A), and de-cast + the widget reads they legalize: + + - `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` (`@object-ui/types`) + — the inline-editor height `RichTextField` has always read through an + `as any` (default 8), following the `TextareaFieldMetadata` precedent. NOT a + spec key: `@objectstack/spec` `FieldSchema` refuses `rows` BY NAME + (`unrecognized_keys`) on all four of textarea/markdown/html/richtext, so it is + an objectui render hint that must not be written into authored object + metadata. The four inert editor keys (`toolbar`/`preview`/`minHeight`/ + `maxHeight`) stay deliberately undeclared and are pinned so. + - `SelectOptionMetadata.description` — secondary option text `LookupField` + searches on authored static options and emits from `recordToOption`. NOT a + spec key either: `@objectstack/spec` `SelectOptionSchema` is strict over + exactly `{label, value, color, default, visibleWhen}` and refuses + `description` BY NAME, and `FieldSchema` routes `options` through that schema, + so the key must never reach authored object metadata. + - `RichTextField` and `TextAreaField` (`@object-ui/fields`) now read their + metadata through the declared types instead of `field as any` (the spec-face + `maxLength` dual-read in `TextAreaField` stays as a documented structural + read). Behaviour unchanged; `rows` and option `description` are now legal to + author in an objectui **annotated literal** — never in an object document sent + to the platform. + + Both spec attributions above were corrected in place before release + (objectui#7537): as first written this changeset claimed each key was "aligned + with" a `@objectstack/spec` schema member that does not exist. Re-measured on + `@objectstack/spec@17.2.0`, each refusal is paired with a control that accepts + the same payload minus the key. Same correction as objectui#7014 / PR #7510 made + to the published JSDoc; the package bumps and the declared behaviour are + unchanged. +- 2c45966: Declare the 13 renderer-read keys that no shipped type declared (objectui#6150) + + **This is a published-surface change on `@object-ui/types` and its `zod` mirrors, + and it moves the accept set in TWO directions.** Read the next two paragraphs + before reading the list — they are what the change actually is. + + **Key membership is NOT widened — it was never narrow.** All eight touched mirrors + extend `BaseSchema`, which is `.passthrough()`, and `.extend()` carries that policy + through (measured on the built mirrors: `catchall` is `z.unknown()` on all eight). + So before this change every one of the 13 keys already parsed green and already + SURVIVED the parse — admitted unexamined, neither refused nor stripped. Nothing + that parsed before stops parsing because a key became known. + + **Value enforcement IS widened, which in the value dimension is a NARROWING.** For + the 12 keys that gained a zod mirror entry, the value is now validated against the + declared type: `{ type: 'text', content: 42 }` parsed green before and is refused + now, at `content`. That is the point of declaring them — `declared === enforced` — + but it is a behaviour change for documents that carried a wrong-typed value under + one of these 13 names. Keys OUTSIDE the 13 are untouched: an undeclared key of any + type is still admitted unexamined on all eight mirrors, pinned per mirror. + + The 13, each with the renderer read site the declaration records: + + | type | key | declared as | read at | + |---|---|---|---| + | `TextSchema` | `content` | `string` | `renderers/basic/text.tsx` — `{schema.content \|\| schema.value}` | + | `CarouselSchema` | `opts` | `Record<string, unknown>` | `complex/carousel.tsx` — `opts={schema.opts}` | + | `CarouselSchema` | `orientation` | `'horizontal' \| 'vertical'` | `complex/carousel.tsx` | + | `CarouselSchema` | `itemClassName` | `string` | `complex/carousel.tsx` — per-slide class | + | `FilterBuilderSchema` | `wrapperClass` | `string` | `complex/filter-builder.tsx` | + | `TreeViewSchema` | `nodes` | `TreeNode[]` | `data-display/tree-view.tsx` | + | `TreeViewSchema` | `title` | `string` | `data-display/tree-view.tsx` | + | `TreeViewSchema` | `onNodeClick` | `(node: TreeNode) => void` | `data-display/tree-view.tsx` — INVOKED | + | `CheckboxSchema` | `required` | `boolean` | `form/checkbox.tsx` — drives the `*` marker | + | `FileUploadSchema` | `buttonText` | `string` | `form/file-upload.tsx` | + | `FileUploadSchema` | `wrapperClass` | `string` | `form/file-upload.tsx` | + | `HoverCardSchema` | `align` | `OverlayAlignment` | `overlay/hover-card.tsx` | + | `ContextMenuSchema` | `trigger` | `SchemaNode \| SchemaNode[]` | `overlay/context-menu.tsx` | + + These compiled before only because `BaseSchema` ends with `[key: string]: any` + (objectui#5155), so the docs page was the single place in the repo recording each + capability, and the one place with no mechanical guard. + + Three declarations are deliberately not what "declare what is read" would produce + on its own, and each says so in its own doc comment: + + - `CarouselSchema.opts` stays an OPEN bag rather than the docs page's + `{ loop?, align? }` pair. The renderer forwards the whole bag to embla, so + narrowing it to two keys would refuse authored documents that work today. + - `ContextMenuSchema.trigger` is OPTIONAL although the docs page shows it + required; the renderer substitutes a placeholder, so trigger-less documents are + legal today. + - `TreeViewSchema.onNodeClick` gets NO zod mirror. It is invoked, not read as a + value, so it cannot appear in an authored JSON document; objectui#6152 ruled + that class is recorded in `zod-mirror-parity.test.ts`'s `RuntimeOnlyDeclared` + instead, and it is (the first pair to sit there without also sitting in + `UnmirroredDeclared`, so that file's two counts move with it). + + Two of the 13 declare a SECOND spelling for a slot that already had one — + `TextSchema.content` beside `value`, `TreeViewSchema.nodes` beside `data` — because + that is what the renderers read. Retiring either spelling is an ADR-0049 + enforce-or-remove question and is deliberately not decided here. Declaring `nodes` + also does not by itself make a `nodes`-only tree-view document legal: `data` stays + required on both faces. +- db3a600: `StackSchema` now SHIPS the members it declares (objectui#6151). Its emitted declaration + carried one property — `type` — where it was meant to carry twenty-five. + + The interface was written `extends Omit<FlexSchema, 'type'>`: "everything `FlexSchema` has, + with a different `type`". That spelling erases every named member. `Omit` of a type over a + key set is `Pick` over `Exclude` of `keyof` that type, and `keyof` a type carrying a string + index signature is `string | number` — the literal member names are absorbed. `FlexSchema` + inherits `BaseSchema`'s `[key: string]: any` (objectui#5155), so excluding `'type'` from + `string | number` still leaves `string | number`, and the `Pick` rebuilt a type holding the + index signature and none of the named members. Measured against the built `dist`: + `FlexSchema` declared 25 properties, `StackSchema` declared 1. + + Nothing errored, which is why it survived four releases: the index signature keeps every + absent key assignable and readable as `any`. The cost fell entirely on the tools that READ + the declaration. Editor completion on a `stack` node offered `type` and nothing else — no + `gap`, no `align`, no `justify`, no `children`. And a docs-versus-type sweep read + `stack.mdx` as documenting keys that do not exist: objectui#6143 flagged `gap`, `children` + and `className` there as divergences when the docs were right and the type was wrong. + + Fixed at the mechanism rather than by restating the members. The six flex/stack members now + live in a new exported interface, `FlexLayoutProps`, which does NOT inherit `BaseSchema`, + and `FlexSchema` and `StackSchema` each extend `BaseSchema` and `FlexLayoutProps`. No + `Omit` crosses the index signature any more, and the members are declared once rather than + duplicated. Extending `FlexSchema` directly was measured unavailable: an interface may + narrow an inherited property only to a subtype, and `'stack'` is not a subtype of + `FlexSchema`'s `type: 'flex'` (TS2430). + + `FlexSchema` is unchanged — its six member declarations moved byte-identically, and its + emitted member set is the same 25 names before and after. The only declaration whose shape + changes is `StackSchema`, which goes from 1 property to the same 25. + + **The one way this can newly error**, and why it ships as `minor`: keys on a `stack` node + were previously answered by the index signature as `any`, so `gap: 'large'` type-checked. + `gap` is now `number | undefined` and that line is a `tsc` error. Every value this newly + rejects is one the renderer never honoured — `stack.tsx` feeds `gap` to a Tailwind numeric + scale — so the change reports a defect that was already there rather than removing a + capability. All three in-repo packages that name `StackSchema` or `FlexSchema` + (`@object-ui/components`, `@object-ui/core`, the schema-catalog example) type-check green + unchanged. + + Guarded by `packages/types/src/__tests__/stack-schema-emitted-members.test.ts`, which + asserts against the EMITTED declaration rather than the source. That distinction is the + whole point: a source-level assertion passes on the broken code, because the index + signature answers for the missing key with `any`. The guard emits declarations with the + package's own tsconfig and asserts (1) `StackSchema` declares exactly what `FlexSchema` + declares, and (2) no member of the `LayoutSchema` union has lost any of `BaseSchema`'s + named members — so the next heritage clause that collapses under the index signature reds + for the whole class, not just for this one interface. +- 52a43de: `ChatbotSchema` names the `chatbot` node's local-display and legacy + auto-response keys — a new, additive published surface (objectui#6169, the + #6172 family ruling: every component node has exactly one named, importable + authoring-face type). + + `ChatbotSchema` (`@object-ui/types`) now declares ten keys that previously + existed ONLY inside an anonymous inline intersection local to + `packages/plugin-chatbot/src/renderer.tsx`'s `chatbot` registration, invisible + to anything outside that one file: + + - `showTimestamp`, `userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, + `assistantAvatarFallback`, `maxHeight` — display fields. + - `autoResponse`, `autoResponseText`, `autoResponseDelay` — the local + auto-response (demo/playground) fields, already live via a real consumer + (`packages/app-shell/src/console/ai/AiChatPage.tsx`). + - `onSend?: (content: string, messages: ChatMessage[]) => void` — the + send-callback, now typed against the published `ChatMessage` shape rather + than the plugin's internal runtime message type. + + Each was read-site-censused before being declared (renderer.tsx and/or + `useObjectChat.ts` reads every one); none were dead, so none took the + ADR-0049 retirement route. `disabled` — also present in the original + intersection — is NOT redeclared: it is already `BaseSchema.disabled` + (`boolean | string`), read generically for every node type, and redeclaring + it here would have narrowed away the inherited expression-string case. + + **What an external consumer can now do that they could not before:** import + `ChatbotSchema` from `@object-ui/types` and get these ten keys with real, + checked types — previously any reference to them required either duplicating + the anonymous type by hand or falling back to `any`. The Zod mirror + (`@object-ui/types/zod`) gained the same ten keys in lockstep, so a `chatbot` + node parsed through it is now validated on these keys rather than silently + passed through unchecked (`BaseSchema`'s Zod mirror is `.passthrough()`). + + `packages/plugin-chatbot`'s `chatbot` registration (`renderer.tsx`) now types + its `schema` prop as `ChatbotSchema` directly, dropping the anonymous + intersection. No behavior change: `renderer.tsx:87`'s + `body: schema.requestBody` forwarding — the subject of the already-merged + #6193 — is untouched, and the render function reads the exact same keys it + already read. + + This is additive (new optional keys on an interface that already carried a + `[key: string]: any` index signature, and a new Zod-validated subset of + previously-passthrough keys), so it ships as `minor` even though it changes + published type surface: objectui's major is pinned to `@objectstack`'s + (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes + ship as `minor` with the break spelled out — there is no break here to spell + out, only a widening from anonymous-and-unchecked to named-and-validated. + + Out of scope, deliberately: the `chatbot-enhanced` and `chatbot-floating` + registrations' own anonymous intersections (different key sets, a decision + for a separate card in the same family), and the `surface` row on + `content/docs/plugins/plugin-chatbot.mdx`'s Properties table, which names a + key no registration in this package currently reads (filed separately). +- e4559d1: `TimelineSchema` now declares the presentational keys the timeline renderer actually reads + (objectui#6170, maintainer ruling 2026-08-25 — the same family rule adopted on + objectui#6172: the exported type aligns to the measured authored + read set). + + Before this, `TimelineSchema` declared `events` (required), `orientation` and `position`, + and nothing else. `TimelineRenderer` is annotated `schema: TimelineSchema` and reads nine + keys off that node — `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`, + `rowLabel`, `scale`, `timeScale` — and **none** of the three that were declared. The docs + property table and the registration's own designer `inputs` had agreed with the renderer + all along; only the exported type disagreed. It was invisible to `tsc` because `BaseSchema` + carries `[key: string]: any`, so every undeclared key resolved as `any` and the annotation + constrained nothing. + + The most visible casualty was the docs page's own TypeScript example, which did not + compile: `Property 'events' is missing in type '{ type: "timeline"; variant: string; items: + … }' but required in type 'TimelineSchema'`. The page taught an authoring form its own + published type refused. + + **Declared now** (TS interface and the `@object-ui/types/zod` mirror together): `variant`, + `items`, `dateFormat`, `scale`, `timeScale`, `rowLabel`, `minDate`, `maxDate`. `onItemClick` + is deliberately left undeclared — it is a runtime slot `ObjectTimeline` installs, and this + package keeps callback-shaped keys off the authored surface. + + **`scale` is the canonical axis key.** It is `@objectstack/spec`'s `ui/TimelineConfig.json` + spelling and the one `resolveTimelineScale` reads first (`scale ?? timeScale`). The designer + now offers it, with all six buckets: `hour` / `quarter` / `year` have rendered correctly + since objectui#2942 but were offered by neither the designer (which listed three) nor the + exported type (which listed none), so they were authorable and undiscoverable. `timeScale` + stays as a deprecated alias so stored JSON keeps working; retiring it is routed separately. + + **`events` is now optional.** It was required, which is why the documented authoring form + did not type-check. That widening is the only non-additive change here — strictly more + programs compile and strictly more input parses than before. `events`, `orientation` and + `position` remain declared and remain read by nothing; a timeline authored with `events` + still renders an empty rail. Their removal is a breaking narrowing of a published type and + is routed through ADR-0049 enforce-or-remove as its own change, not smuggled into this one. + + Accept-set note for consumers: keys that previously resolved as `any` are now typed, so a + value the renderer never implemented — `variant: 'diagonal'`, `dateFormat: 'medieval'`, + `scale: 'fortnight'` — is a type error and a Zod rejection where it used to pass silently. + Nothing that renders today stops rendering. `BaseSchema`'s index signature is untouched, so + an undeclared key is still accepted by both halves (objectui#5155 / objectui#6269 own that + ceiling). +- 2c71482: One authority for `KanbanSchema` / `KanbanColumn` / `KanbanCard`: the bare names + now belong to `@object-ui/plugin-kanban` (objectui#6172, closing the + cross-package half of objectui#6155). + + **Breaking, deliberately — six published names are removed from + `@object-ui/types`.** Per this repo's own rule a breaking change ships `minor`, + with the break spelled out here. + + | removed from | old name | new name | + | --- | --- | --- | + | `@object-ui/types` | `KanbanSchema` | `DeclarativeKanbanSchema` | + | `@object-ui/types` | `KanbanColumn` | `DeclarativeKanbanColumn` | + | `@object-ui/types` | `KanbanCard` | `DeclarativeKanbanCard` | + | `@object-ui/types/zod` | `KanbanSchema` | `DeclarativeKanbanSchema` | + | `@object-ui/types/zod` | `KanbanColumnSchema` | `DeclarativeKanbanColumnSchema` | + | `@object-ui/types/zod` | `KanbanCardSchema` | `DeclarativeKanbanCardSchema` | + + Nothing else moved: every member, every optionality and the Zod mirror's whole + accept/reject behaviour are byte-for-byte the shape they were. `SchemaRegistry` + still maps `'kanban'`, `ComplexSchema` still carries the arm, and + `safeValidateSchema` accepts and refuses exactly what it did before. + + **Migration.** `import type { KanbanSchema } from '@object-ui/types'` becomes + either of two things, and which one you want is the whole point of the rename: + + - authoring a board that a **registered renderer** will draw — import the bare + name from `@object-ui/plugin-kanban`, which is unchanged; + - annotating or validating the **declarative** shape `@object-ui/types` mirrors + in Zod — import `DeclarativeKanbanSchema` (or the Zod + `DeclarativeKanbanSchema` from `@object-ui/types/zod`). + + **Why this direction.** The two declarations were structurally unrelated + dialects sharing three names, and `@object-ui/types` is the declared + zero-workspace-dependency bottom layer, so it cannot re-point at a plugin — + convergence had to remove a name from one side or the other. All four + registered kanban renderers (`kanban`, `kanban-ui`, `kanban-enhanced`, + `object-kanban`) consume the plugin's dialect and none consumes this one, and + objectui#6086 measured what happens when the bare name is the copy no renderer + reads: an IDE or agent auto-import silently authors a board that renders + nothing — a confident empty board rather than an abstention. So the surviving + bare name is the one a renderer honours. +- 5ef9c4f: The section grouping contract (objectui#6236, maintainer ruling 2026-08-27): a + `section-divider` row may now CLAIM its member fields — `FormField.fields: string[]`, the + same membership shape `FormFieldTab.fields` / `FormFieldPane.fields` already model — and + the form renderer then gates the WHOLE group on the divider's own visibility verdict + (`visibleWhen` / `visibleOn` / legacy `condition`). + + Before this, one authored `FormSection.visibleWhen` meant two different things: the + console renderer drops the whole `<section>` (heading and fields), while the plugin-form + chain's renderer treated `section-divider` as a purely presentational row and hid only + the HEADING, leaving the section's fields rendering (measured in objectui#6111, which + pinned that honestly rather than implying a guarantee it did not deliver). + + Ruled semantics, now pinned in `section-grouping-6236.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** (console precedent, 2026-08-22 + ruling after #5594) — a hidden section's values still submit. + - **A hidden section's fields skip client-side validation** — a user is never blocked by + an error pointing at a control they cannot see (the objectui#6110 defect shape); the + server-side contract remains the loud floor for genuinely-required data. A section + hiding mid-session also clears its members' stale errors, the way a field's own false + predicate already did. + - **A divider without a claim keeps the old contract** (its predicate gates only the + heading), so existing schemas are untouched. + + Both halves ride the mechanism the field-level predicate already uses (return `null`; + react-hook-form keeps the value and skips the unmounted control), so field-level and + section-level visibility cannot drift apart. The zod mirror (`FormFieldSchema`) declares + the key with the same scope note. + + `@object-ui/plugin-form` wires the producer half: all six `section-divider` synthesis + sites (ObjectForm's stacked simple path, ModalForm's sectioned and derived-fieldGroup + paths, DrawerForm's sectioned and derived-fieldGroup paths, SplitForm's panes) now stamp + the membership claim onto the divider they emit, from the RESOLVED member list — so an + authored `FormSection.visibleWhen` finally hides the whole section on the object-view + chain, matching the console renderer. The #6111 honest pin (`measured scope`) flipped + accordingly: it now pins heading-and-fields hiding together, and every per-layout DENIED + row asserts the claimed member as well as the heading. The derived-fieldGroup sites carry + the claim for uniformity but stay fail-open — the spec `fieldGroups` vocabulary has no + section-predicate slot to author. The tabbed arm's predicate slot (objectui#6237) is + designed to reuse this same grouping contract. +- 46f0bb4: The tabbed arm of the grouping contract (objectui#6237, same maintainer ruling as + objectui#6236): `FormFieldTab` gains the predicate slot the ruling named — + `visibleWhen?: string | { dialect?: string; source: string }` — so a section rendered as + a TAB PANEL (`ModalForm` `contentLayout: 'tabbed'`) can finally carry an authored + `FormSection.visibleWhen`. The tabbed layout synthesises no `section-divider` at all, so + the #6236 membership-claim mechanism had nothing to stamp the predicate onto and no slot + to copy it into; the predicate was silently dropped one hop before evaluation (measured + in objectui#6237's card). + + The form renderer evaluates the tab's predicate with the same record assembly the + field-level rules use (`ruleRecord` / `previousRecord` / host predicate scope, #6010), + fail-open, and when FALSE draws neither the tab's trigger nor its panel. Not drawing the + panel unmounts the claimed fields through the exact mechanism a field's own false + predicate uses, so the ruled hidden-group semantics are inherited rather than + re-implemented, and are pinned in `fieldtab-visiblewhen-6237.test.tsx`: + + - **Visibility decides what is DRAWN and nothing else** — a hidden tab's values still + submit. + - **A hidden tab's fields skip client-side validation** — a user is never blocked by an + error pointing at a control they cannot see; the server-side contract remains the loud + floor for genuinely-required data (#2959's trap, answered the same way for tabs as for + sections). A tab hiding mid-session clears its members' stale errors. + - **Deterministic re-selection**: a predicate hiding the ACTIVE tab activates the user's + pick if still visible, else the declared default, else the first visible tab — never an + empty panel — and the user's pick is restored the moment its tab is re-admitted. + - **No mid-interaction collapse**: whether the tabbed arm engages stays judged on the + DECLARED tabs, so a predicate hiding one of two tabs filters the strip (and hides the + tab's fields) instead of collapsing the modal into the stacked layout under the user's + cursor. With every tab hidden the strip is omitted; unclaimed fields still render. + - **A tab without the key keeps the pre-#6237 contract** (always drawn), so existing + schemas are untouched. + + `@object-ui/plugin-form` wires the producer half: `ModalForm`'s tabbed synthesis site now + copies the section's `visibleWhen` onto the tab it emits, and the #6111 layout matrix + gains the tabbed-modal rows (direct and via `ObjectForm` delegation). `TabbedForm` / + `WizardForm` still declare no section predicate in their own section configs — those arms + remain open on objectui#6237. +- 8f1d995: `ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it + + `@object-ui/types` and `@object-ui/core` each published a declaration of + `ComponentConfig`, so an auto-import picked between two different types by + alphabetical order. After the `ComponentMeta` convergence the remaining + difference was genericity and the `component` slot: `@object-ui/types`' was + non-generic with `component: any`, core's was `<T = any>` with + `component: ComponentRenderer<T>`. + + `@object-ui/types`' declaration gains that type parameter, **defaulted**, so + every existing spelling keeps its meaning exactly — bare `ComponentConfig` is + `ComponentConfig<any>`, whose `component` is `any`, as before. `@object-ui/core` + re-exports it instead of declaring its own. + + The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`, + `deprecated`) were not dropped: they moved to a named extension, + `RegistryComponentConfig`, which is what `Registry.getConfig`, + `getAllConfigs` and `getNamespaceComponents` return. Those return values are + type-identical to what they returned before, so every read path is unchanged. + + **Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core` + *and* touches one of those five registry-only keys through that annotation must + switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig` + no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's + versioning policy: objectui's own breaking changes ship as `minor` with the break + spelled out here, because the whole publishable set is one changeset `fixed` group + pinned to `@objectstack`'s major. +- 29754cf: Model `code-editor` and `bar-chart` in `AnyComponentSchema`, and repair three catalog fixtures + + Both types render — `@object-ui/plugin-editor` registers `code-editor`, + `@object-ui/plugin-charts` registers `bar-chart` — and neither had a Zod member, + so `safeValidateSchema` (and therefore `objectui validate`) refused every + document that named them, whatever the document said. `CodeEditorSchema` and + `BarChartSchema` are now declared in `@object-ui/types` and mirrored in + `@object-ui/types/zod`, derived key-for-key from what the two renderers + demonstrably read rather than from a view of what either component ought to + accept. + + Alongside them, three `examples/schema-catalog` entries that were wrong about + their own renderer: `basic-select`'s third option spelled its label `type`, so + the option rendered blank; `icon-toolbar`'s buttons carried only `icon`/`value`, + which `button-group` never reads, so all three rendered blank; and `basic-tabs` + gave its items no `value` and no `defaultValue`, so no panel could be selected. +- b84dc18: Three exported type names inside `@object-ui/types` had two authorities each; each now has + one (objectui#6349, first batch — the three intra-package collisions from the 46-name + census on objectui#6273). + + **`ActionSchema` — renamed, because the two shapes are unrelated.** `crud.ts` and + `ui-action.ts` both declared it. Measured member-by-member they share 9 keys out of 28 each: + `crud.ts` `extends BaseSchema` and pins `type: 'action'` (a UI node — a button in a + component tree), `ui-action.ts` extends nothing and types `type` as `ActionType` (a spec-v2 + action definition, with `name`, `locations`, `params`, `target`). Re-pointing either at the + other would silently hand a consumer a different type, so this took the rename branch + (objectui#5044 is the precedent for choosing the surviving name). `ui-action.ts`'s + declaration is now spelled **`UIActionSchema`** — the name `src/index.ts` has always + PUBLISHED it under, via `export type { ActionSchema as UIActionSchema }`, which is now a + plain re-export. **The package's public surface is unchanged**: `ActionSchema` still means + `crud.ts`'s legacy shape and `UIActionSchema` still means `ui-action.ts`'s, exactly as + before. Nothing outside `ui-action.ts` imported the old spelling — there is no `./ui-action` + subpath in `exports`, so the old name was never reachable from outside the package. + + **`BreadcrumbItem` / `BreadcrumbSchema` — re-pointed, because one copy was stale.** Both + were declared in `data-display.ts` and in `navigation.ts`. The data-display pair was not a + second dialect but a strict SUBSET: no key declared differently on either side, and missing + `BreadcrumbItem.icon` / `onClick` / `siblings` and `BreadcrumbSchema.maxItems`. Everything + that reads a breadcrumb was already on the navigation declaration — `registry.ts` maps the + `'breadcrumb'` component type to it, `src/index.ts` re-exports it under the bare names, + `zod/navigation.zod.ts` mirrors it (`icon`, `onClick`, `siblings`, `maxItems` included), the + `ui:breadcrumb` renderer consumes it, and the component's own documentation page documents + `icon` and `maxItems`. `data-display.ts` now re-exports the one authority. + + **What changes for a consumer.** The `@object-ui/types/data-display` subpath is published, so + its `BreadcrumbItem` / `BreadcrumbSchema` and the `DataDisplaySchema` union's breadcrumb + member widen to the navigation declaration — they gain the four keys above. Nothing narrows + and no key changes type, so every value that type-checked before still does; what the subpath + now declares is what the renderer already honoured and the docs already described. Graded + `minor` because a published `.d.ts` member changes shape, per this repo's version-alignment + rule (never `major`). + + The three `KNOWN_COLLISIONS` lines came down in the same change; that baseline + (`scripts/__tests__/one-authority-per-exported-name-6273.test.ts`) is shrink-only and fails in + both directions, so converging without deleting them would have been red too. 43 entries → 40. +- ac8abb0: The last two exported type names with two authorities *inside* `@object-ui/types` now have + one each (objectui#6349, second batch — the remaining intra-package collisions from the + census on objectui#6273). Both took the **rename** branch, because in both cases the two + declarations are genuinely different types; the surviving spelling is in each case the name + `src/index.ts` has always published that declaration under, so **no importable name + changes** and the barrel alias becomes a plain re-export. + + **`MenuItem` — renamed to `AppMenuItem` in `app.ts`.** `app.ts` declared a flat, + all-optional `interface` (`type?: 'item' | 'group' | 'separator'`, `label`, `icon`, `path`, + `href`, `children`, `badge`, `hidden`) — the `@deprecated` legacy navigation item that + `AppComponentSchema.menu`, `AppAction.items` and `menuItemToNavigationItem` read. + `overlay.ts` declared a discriminated **union**, `MenuCommandItem | MenuDividerItem`, whose + command arm requires `label` and whose both arms **tombstone** `type` as `type?: never` + (objectui#6523) — precisely the key `app.ts` declares as a three-value enum. Re-pointing + either at the other would have made an authored `type: 'separator'` legal on one side and a + type error on the other, so the two names had to part. `@object-ui/types` continues to + publish overlay's union as `MenuItem` and app's interface as `AppMenuItem`, exactly as + before. + + **`ValidationFunction` — renamed to `FieldValidationFunction` in `field-types.ts`.** + `data-protocol.ts` declares `(value, context?: ValidationContext) => boolean | string`; + `field-types.ts` declared `(value) => boolean | string | Promise<boolean | string>`. They + disagree at both ends of the arrow — different parameter lists, and a `Promise` return that + the data-protocol signature does not admit — so field-types' is not assignable to + data-protocol's in the direction that matters. `data-protocol.ts` had said so in prose ("may + differ from similarly named validation function types in other packages (e.g., in + `field-types`)") for as long as both existed. The published names `ValidationFunction` + (data-protocol's) and `FieldValidationFunction` (field-types') are unchanged. + + Both `KNOWN_COLLISIONS` lines come down in the same change; that baseline fails in **both** + directions, so converging without deleting them is red too. 38 entries -> 36. +- 9d86e1d: Retire the `timeScale` alias on the timeline node — `scale` is the only axis spelling + (objectui#6355, maintainer ruling 2026-08-27). + + **BREAKING for authored metadata.** `timeScale` was this renderer's pre-spec spelling of the + Gantt axis bucket. `scale` is canonical — it is `@objectstack/spec` `ui/TimelineConfig.json`'s + axis key and the key the renderer preferred (objectui#6170 ruling, 2026-08-25: `timeScale` + goes the alias-retirement route, not a silent second spelling). objectui#6355's ruling + retires it immediately, with no phased window, while the project is at startup stage. + + **What breaks, and how you will find out.** A timeline document that spells `timeScale` is + now **refused**, loudly, at the authoring boundary: + + - `TimelineSchema.timeScale` is declared `?: never` — writing it is a type error; + - the Zod twin declares `z.never().optional()` — parsing a document that carries the key + fails with `invalid_type` / `expected: never` on the `timeScale` path. + + The fix is a rename: `timeScale` → `scale`. The accepted values are unchanged (`hour`, + `day`, `week`, `month`, `quarter`, `year`), so no value needs rewriting. + + **Why a tombstone rather than deleting the key.** `BaseSchema` is `.passthrough()` on the + Zod side and carries `[key: string]: any` on the TS side, so an *undeclared* key is accepted + unvalidated by both halves. Deleting `timeScale` outright would have let the retired spelling + parse green and type-check green while the renderer no longer read it — the Gantt axis would + silently fall back to the `month` default, the chart would change bucket, and nothing would + error. That is the silent axis breakage objectui#2942 closed, running in the other direction, + and it is the specific outcome this retirement is shaped to prevent. Keeping the key declared + as `never` on both halves is what makes the removal audible. Absent stays valid on both, so a + document that never wrote the alias is untouched. + + Also in this change: + + - `resolveTimelineScale` drops the `?? schema.timeScale` fallback read; its parameter narrows + to `{ scale?: unknown }`. + - The designer drops its deprecated `timeScale` input. The `scale` input already offers all + six buckets. + - `ObjectTimeline` now emits the resolved axis under `scale` when it composes the schema it + hands to the renderer. It previously wrote the alias, which would have made **every** + object-bound Gantt fall through to the `month` default the moment the fallback read went — + silently, since that is a composed schema no author ever sees. Writing `scale` after the + spread also restores the precedence the surrounding code intends: a `timelineConfig.scale` + now actually beats a flat `schema.scale`, where under the alias the resolver's + `scale ?? timeScale` ordering let the flat key win. + - The two in-repo authors are migrated in the same change: the schema-catalog + `gantt-style-timeline.json` fixture and the registration's own `examples.gantt` block. + - Docs drop the `timeScale` row and gain a retirement callout; + `packages/components/.../TIMELINE.md`'s Gantt table now documents `scale` with the full + six-value vocabulary it has accepted since objectui#2942 (its row still claimed three). + + Version note: `minor`, not `major`, per AGENTS.md §版本号策略 — objectui's major tracks the + `@objectstack` major and all publishable packages share one `fixed` group, so a breaking + narrowing is declared `minor` with the break spelled out here. +- 99a3c2d: `BaseSchema` declares `bind`, the data-scope binding path, on both halves — the TypeScript + interface and its Zod mirror (objectui#6357). + + `bind` was read by ten production sites and declared by no schema shape. It resolved as `any` + through `BaseSchema`'s index signature and rode `.passthrough()` on the validator, while three + separate documents taught it as an authorable key of *every* node: this repo's own `AGENTS.md` + §4 ("Every node in the UI tree follows this shape (`@object-ui/types`)"), the published + agent-facing `skills/objectui/rules/protocol.md` ("Every UI component node MUST follow this + shape"), and `content/docs/fields/grid.mdx`. So the agent-facing protocol told authors to write + a key the published types did not know existed. + + The census chose the home rather than guessing it. Nine reads go through + `useDataScope(schema.bind)` — `list` and `tree-view` in `@object-ui/components`, and the + `object-*` widgets in `plugin-charts`, `plugin-dashboard` (×2), `plugin-grid`, `plugin-kanban`, + `plugin-list`, `plugin-timeline`. A tenth is `plugin-grid`'s `gridNeedsDataSource` predicate, + where a present `bind` is one of the escape hatches that makes a missing data-source adapter + legitimate. Two more sites destructure the key out so `SchemaRenderer`'s prop spread cannot + write `bind="data.revenue"` onto the DOM. Per-component declaration was measured and rejected: + it costs nine copies of one key and buys nothing extra, because neither half can refuse the key + on a non-reader either way. `placeholder` is the standing precedent for a cross-cutting key + declared on `BaseSchema` and honoured only by a subset. + + **Accept-set narrowing, on the value and not the key.** `bind: 42` type-checked and parsed green + before this change and is refused by both halves now. It only refuses what already crashed: + `useDataScope` is `(path?: string)` and resolves via `path.split('.')`, so a non-string `bind` + threw a `TypeError` at render time. Every `bind` authored in this repo is a string, and the + declaration is optional, so nothing that renders today stops. + + **What this does NOT change**, stated because the pin would otherwise be read as more than it is: + an *undeclared* key is still accepted by both halves, so this did not buy rejection of a + misspelling such as `bindTo` (objectui#5155 / objectui#6269 own that ceiling). And `data-table` + still does not call `useDataScope`, so a `bind` on it is still ignored and still renders a header + over an empty body with no error — a documented silent failure that this declaration neither + causes nor cures, since the key was accepted on every node before it existed. + + `ObjectPivotTable` drops its local `bind?: string`: its `PivotTableSchema & {…}` intersection + extends `BaseSchema`, so the member was a true duplicate. Two other local declarations are left + in place and ratcheted rather than removed — their containing types never reference `BaseSchema`, + so deleting the member would delete the declaration rather than inherit it. +- 3190414: `TableColumn` declares `fitContent`, the content-hugging flag `data-table` has + honoured all along (objectui#6424, maintainer ruling 2026-08-28, Option A — the + card's second key, in the shape #6615 landed `headerIcon` in). + + The key was undeclared-but-honoured: `data-table` skips `fitContent` columns in + the auto-width pass and renders them as a `width:1%` + `whitespace-nowrap` cell + with no `overflow-hidden` clamp — but the published declaration refused the key, + so a typed author writing `{ accessorKey: '_actions', fitContent: true }` got a + compile error for a key the renderer implements, and `TableColumnSchema.parse` + silently STRIPPED it, while the same key placed by an untyped producer worked. + The runtime admitted a vocabulary the declaration refused — the second de-facto + contract AGENTS.md #0.1 forbids, here with the CONSUMER out of step. + + Retiring the reads instead was excluded BY MEASUREMENT, not preference: shipped + source authors the key (`ObjectGrid` writes `fitContent: true` on the injected + row-actions `_actions` column) and `data-table-fit-content.test.tsx` pins the + result. Retiring would re-clip inline row-action buttons. + + - `TableColumn.fitContent?: boolean` — serializable metadata, unlike the + `React.ReactNode` slot `headerIcon` is. + - `TableColumnSchema` mirrors it as `z.boolean().optional()`: the flag now + SURVIVES parse instead of vanishing, and a non-boolean is a loud refusal + naming the key rather than acceptance-without-validation. Pinned by output + survival, not parse acceptance — acceptance was green before, while the flag + was stripped. + - `StaticTableColumn` / `StaticTableColumnSchema` tombstone it (`?: never` + + `z.never().optional()`), per #5474's lockstep rule: every rich key needs a + deliberate static-side decision, and the static renderer has no auto-width + pass to opt out of (its measured read set is the five live keys). Authoring + it on a static `table` column is a loud parse refusal naming the key, not a + silent strip. + + No runtime behaviour changes in `data-table` itself — the reads were already + live; the declaration and the parse road now agree with them. The two + `(col as any).fitContent` sites drop with the declaration, but that removal is + bookkeeping rather than the fix: `col` is already `any` at both sites, widened + by the file's own `col: any` normalization, so the casts were redundant at + compile time today. They become load-bearing the moment those render callbacks + are typed — which is the standing instrument gap, not closed here. +- 4e480f5: `TableColumn` declares `headerIcon`, the icon node `data-table` has rendered into the + header cell all along (objectui#6424, maintainer ruling 2026-08-27, Option C per-key). + + The key was undeclared-but-honoured: `data-table` renders `col.headerIcon` before the + header text, and `ObjectGrid` writes it for `showColumnTypeIcons` — but the published + declaration refused it, so a typed author writing `{ accessorKey: 'x', headerIcon: icon }` + got a compile error for a key the renderer implements, and `TableColumnSchema.parse` + silently STRIPPED it, while the same key placed by an untyped producer worked. The runtime + admitted a vocabulary the declaration refused — the second de-facto contract AGENTS.md + #0.1 forbids, here with the CONSUMER out of step. + + - `TableColumn.headerIcon?: React.ReactNode` — a runtime slot like `cell`, not + serializable metadata. + - `TableColumnSchema` mirrors it (`z.any()`, passthrough): the node now SURVIVES parse + instead of vanishing. Pinned by output survival, not parse acceptance — acceptance was + green before while the icon was stripped. + - `StaticTableColumn` / `StaticTableColumnSchema` tombstone it (`?: never` + + `z.never().optional()`), per #5474's lockstep rule: every rich key needs a deliberate + static-side decision, and the static renderer never read this one. Authoring it on a + static `table` column is a loud parse refusal naming the key, not a silent strip. + + No runtime behaviour changes in `data-table` itself — the reads were already live; the + declaration and the parse road now agree with them. + + The card's second key, `fitContent`, is deliberately NOT declared and NOT retired here: + the ruled measurement found real authors (`ObjectGrid`'s row-actions column authors + `fitContent: true` on `main`), so per the ruling that arm goes back to the decision box + with the reading rather than into this PR. + + Scope of the enforcement claim, measured: the declaration, the parse road, and the + renderer's behaviour now agree; the renderer's internal column reads remain any-mediated + (the `col: any` normalization in `data-table.tsx`) — a standing instrument gap, not + closed here. +- 38a123c: Land objectui#6425's per-key ruling for `ObjectDataTable`'s authored column + override keys (maintainer, 2026-08-27): + + - **Declare `format`, `options`, `currency`** on `TableColumn` and its + `TableColumnSchema` zod mirror, in the same stroke. All three are honoured + by `object-data-table`'s cell pipeline — `format` / `options` were + documented author overrides the published types refused (a typed author got + a compile error and the zod parse silently stripped the key); `currency` + shipped in production but was never promised. The zod mirror now passes the + keys through instead of stripping them; `StaticTableColumn` and its mirror + tombstone all three under the #5474 lockstep rule (the static renderer + reads no field-meta overrides). + - **Retire `decimals`**, immediately: zero readers measured anywhere + (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads + `precision`), so no authored `decimals` could reach a render. The authored + read is removed and the key falls into `AuthoredColumnOverrides`' derived + refusal band — render output is pinned unchanged. + - **Re-arm plugin-grid's #6004 `options` retirement with an explicit + tombstone**: that refusal rested on the key's NON-membership (excess-property + freshness), which declaring `options` on `TableColumn` silently ended. + `ObjectGridRetiredOptionsTombstone` (`?: never`, intersected into both + `ObjectGridColumnDraft` and `ObjectGridColumn`) restores the refusal by + assignability; #6004's verdict itself is unchanged. + - **`referenceTo` is deliberately NOT declared as spelled** — it stays held, + owned by objectui#6597 (fix the spelling chain or withdraw the README + line). The remaining hold is that card's scope, not unfinished work here. +- d7acad6: **plugin-gantt: the `gantt` block now outranks the flat top-level spelling, and + the losing face's keys are named instead of dropped.** + + `getGanttConfig` checked the flattened top-level spelling first and returned + early, so a node carrying both spellings rendered the flat one and every key + inside an authored `gantt` block was discarded with **no diagnostic** — not even + the `GanttConfigSchema.safeParse` warning, which sat behind that early return. + + `plugin-map` had the identical two-faces shape ruled the other way (maintainer + ruling on objectui#5018, 2026-08-17, landed in PR #5156): the block wins, with a + dev-mode warning naming the ignored top-level keys. objectui#6469 inherits that + ruling, so the two sibling view plugins now answer the same question the same + way. + + What changes: + + - A node carrying **both** spellings now renders the **`gantt` block's** values. + The block is taken **whole** — the flat keys are not merged into it. + - In dev, `[ObjectGantt] … these top-level keys are IGNORED: …` names every + shadowed flat key, once per distinct shadowing. + - Nothing else moves. A node with only the flat spelling, or only a block, is + read exactly as before. + + **Producer-safe:** `ObjectView` (`case 'gantt'`) and `ListView` (`case 'gantt'`) + both flatten `options.gantt` onto top-level keys and emit **no** `gantt` key, so + every gantt reached through either view layer still takes the flat branch, and + the new warning cannot fire on that path. This is the same producer check the + `plugin-map` flip pinned, re-run on today's `main`. + + This **supersedes** the precedence sentence in the objectui#6051 changeset + (`.changeset/6051-gantt-flat-config-declared-keys.md`), which recorded the flat + branch winning — accurate for that change, which deliberately did not touch + precedence, and reversed by this one. +- 713db46: **`ObjectGanttSchema` now declares the `gantt` BLOCK face — and the spec's + required trio enforces at validate/check time.** + + The `gantt` nested-block spelling of a gantt config (`{ type: 'object-gantt', + gantt: { … } }`) had **no mirror entry at all**: it rode through + `BaseSchema`'s `.passthrough()` entirely unvalidated, and the published + TypeScript never taught the shape either — an author writing `gantt: { … }` + got no completion, no type checking, no error on a misspelt member. It was + the 28th and last of the keys `getGanttConfig` (`plugin-gantt/src/ObjectGantt.tsx`) + reads off the schema that objectui#6051 (PR #6472) did not declare — severed + into its own card because, unlike the other 27, declaring it changes what a + published tool refuses. + + Both faces now declare it: + + ```ts + // packages/types/src/objectql.ts + gantt?: GanttConfig; + + // packages/types/src/zod/objectql.zod.ts + gantt: SpecGanttConfigSchema.extend(GanttConfigExtensionFields).optional(), + ``` + + `GanttConfig` derives from the spec's `GanttConfigSchema`, which has required + `startDateField`, `endDateField` and `titleField` since rc.6. + + **What the CLI now refuses that it accepted before:** `ObjectGanttSchema` is a + member of `AnyComponentSchema`, so it reaches `safeValidateSchema` and + therefore the CLI's `validate` and `check` commands. A `gantt` block missing + any of the three required fields — previously accepted silently — is now + **refused**, naming the missing field. A block carrying all three, or a + schema with no `gantt` block at all, is accepted exactly as before. + + **This is a `declared = enforced` restoration, not new requiredness.** + `getGanttConfig`'s block branch already fed the block to + `GanttConfigSchema.safeParse` and logged `[ObjectGantt] Invalid gantt + configuration` to the console on failure — a block missing the trio was + already non-functional at runtime, silently. What changes is *when* the + author is told: runtime console warning becomes an authoring-time refusal. + + Maintainer ruling, objectui#6475 (2026-08-27), **Option A** — enforce as-is, + immediately, no warning window (the startup-stage no-gradualism rule, + objectstack#12668: transitions do not get phased windows without named + external-user evidence, and none exists here). A census of every `gantt` + block reachable through `ObjectGanttSchema` in this repository — the + `examples/schema-catalog` fixtures, `content/docs/plugins/plugin-gantt.mdx`, + and the published `skills/objectui/guides/page-builder.md` guide — found + **zero** blocks missing the trio. +- bf3a03c: `ToastSchema` now declares the two trigger-button keys the `toast` renderer actually reads + (objectui#6496, triage scope cut 2026-08-26 — the same declare-what-runs family as + objectui#6170). + + `renderers/feedback/toast.tsx` renders a `<Button>` that raises the toast, and reads two + keys off the node to do it: `variant={schema.buttonVariant}` and + `{schema.buttonLabel || 'Show Toast'}`. `ToastSchema` declared **neither**, on the TS face + or in the `@object-ui/types/zod` mirror. The registration's own designer `inputs` offered + `buttonLabel` (with `defaultValue: 'Show Toast'`), so the designer shipped a control for a + key the published type did not have; `buttonVariant` was read by the renderer and named by + nothing at all. `SonnerSchema` — the sibling with the identical trigger mechanism — + declared both all along, so only one of the two components was expressible. + + The visible cost was on objectui#6250: with `buttonLabel` undeclared, its seven corrected + toast demos could not author a per-demo trigger label the way the corrected sonner demos + could, and all seven render the default `Show Toast`. Those demos are unblocked by this. + + **`buttonVariant` is declared as the six Button variants, on both faces.** The model this + card was told to copy disagrees with itself: `SonnerSchema` spells the key `z.string()` in + the zod mirror and `'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'` + in TS. Matching by symmetry picks nothing, so the shape was taken from what the value + *reaches* — the renderer passes it straight into `<Button variant={…}>`, whose prop type is + `VariantProps<typeof buttonVariants>['variant']`, exactly those six. The TS face is the + correct one and both faces here carry it. + + An open `string` is not merely under-validation. Measured on `cva` 0.7.1: an unrecognised + variant key contributes **no** variant class, and `defaultVariants` applies only when the + value is *absent* — so `buttonVariant: 'primary'` renders a button with no background and + no text colour, silently, while `buttonVariant: undefined` renders the default look + correctly. `primary` is the likeliest wrong spelling precisely because the default + variant's own class is `bg-primary`. And `buttonVariant: ''` is silently resolved to + `default` by the same falsy fallback — the one wrong value that does not *look* wrong, + which an open `string` would accept and never signal. That the declared six are the Button's own vocabulary + is pinned in both directions in `components/src/__tests__/toast-button-variant-parity.test.ts` + — `@object-ui/types` has zero deps and cannot import the Button, so the list there is + necessarily hand-copied, and that file is what stops it being a copy that can drift. + + **`SonnerSchema`'s own two faces are left disagreeing.** Its mirror stays `z.string()`. + That is a real defect on a published surface, and it is filed as objectui#6541 rather than + fixed here — + this card's face is `ToastSchema`, and narrowing a second published key is its own + accept-set change with its own consumers to measure. + + **Direction 2 of the finding is untouched.** `action` and `onDismiss` are declared on + `ToastSchema` and read by no renderer; they sit immediately adjacent to this edit and are + byte-identical after it. They are enforce-or-remove on a published type and belong to the + objectui#6124 unsatisfiable-mirror census feeding the objectui#6182 handler-dialect + decision; they are deliberately not pinned here either, so that family's ruling lands + without a test of this card's to negotiate with. + + Accept-set note for consumers: both keys are **optional** and materialise no default, so + nothing that renders today stops rendering and no stored toast JSON becomes invalid. Two + keys that previously resolved as `any` through `BaseSchema`'s index signature are now + typed, so `buttonLabel: 42` and a `buttonVariant` outside the six are a type error and a + Zod rejection where they used to pass silently — values that never rendered correctly in + the first place. `BaseSchema` is untouched, so an *undeclared* key is still accepted by + both halves (objectui#5155 / objectui#6269 own that ceiling); declaring these two bought + validation of the declared keys, not rejection of misspellings. +- 29cb85b: `MenuItem` is now a discriminated union, and all three menu renderers read the keys it + declares (objectui#6523, objectui#6346, maintainer ruling 2026-08-27 — "one answer for the + whole `MenuItem` family"). + + **The break, spelled out.** `MenuItem` (`@object-ui/types`, shared by `ui:dropdown-menu`, + `ui:context-menu` and `ui:menubar`) used to be a single object with `label: string` + required unconditionally. It is now `MenuCommandItem | MenuDividerItem`: a command item + (`label` required, plus `icon`/`disabled`/`onClick`/`shortcut`/`children`) or a divider + (`{ separator: true }`, nothing else). The union — not `label?: string` — is deliberate: it + is what the data actually is, and it keeps the command arm's label protection intact rather + than weakening it repo-wide to accommodate the divider. Both arms also tombstone `type` + (`type?: never` / `z.never().optional()`): the retired `{ type: 'separator' }` (and its + sibling `{ type: 'label' }`) is now a **declared refusal** at parse time, not a silent strip. + A consumer's own `MenuItem[]` authored with either retired spelling now fails + `MenuItemSchema.safeParse` and fails `tsc` under the published `.d.ts`; a consumer authoring + the declared `{ separator: true }` divider now **succeeds** for the first time — before this + change it failed a strict parse too, because `label` had no way to be omitted. + + **Renderer accept behaviour changes to match.** `dropdown-menu` and `context-menu` used to + branch on the undeclared `item.type === 'separator'`; an author who instead wrote the + DECLARED `{ separator: true }` got a value that validated, published, and rendered a blank + menu row (the divider fell through to the ordinary item branch with no `label`). Both + renderers now branch on `item.separator`, matching `menubar` — which had this right all + along and is the evidence the type, not those two renderers, was correct. Their registry + `defaultProps` and `description` strings stop teaching the retired dialect; the 4 places it + appeared in this repo (2 schema-catalog fixtures, 2 registry `defaultProps`) are migrated. + + **The item handler moves to the declared key (objectui#6346).** All three renderers now + fire `item.onClick` — the key `MenuItem` has always declared (TS source, built `.d.ts`, and + the Zod mirror all agreed) but that `dropdown-menu`/`context-menu` never read (they read an + undeclared `item.onSelect` instead) and that `menubar` wired nowhere at all. An author who + followed the published type and set `onClick` got a value that validated, published, and + never fired; that is fixed. `renderMenuItems`/`renderContextMenuItems` also tighten from + `items: any[]` to `items: MenuItem[]` — the widening that let the mismatch type-check in the + first place. Migration cost measured **zero** in this repo: no fixture, doc or test authored + `onSelect` on a menu item before this change. + + **Rider, recorded as parity not new capability.** `menubar` now also renders the declared + `shortcut` string — `dropdown-menu` and `context-menu` already drew it, so this aligns the + third container rather than expanding the surface. + + Everything that rendered correctly before this change still renders the same way; the + narrowing only refuses spellings that were already unprotected (silently stripped or never + read at all). +- 3e028c8: One tombstone registry for the designer seam's retired field keys + (objectui#6527). Three independently maintained `RETIRED_FIELD_KEYS` literals + — the metadata-admin read door (`object-fields-io.ts`), `MetadataService`'s + carry-over and `MetadataFieldsPage`'s carry-over — become derivations from a + single registry in `@object-ui/types` (`RETIRED_FIELD_KEY_TOMBSTONES` + + `retiredFieldKeysFor(site)`), naming each retired key, the card that retired + it, and its PER-SITE applicability. + + Per-site behaviour is unchanged — this is a consolidation, and each site's + effective strip set is pinned equal to its pre-consolidation literal. The two + deliberate asymmetries a naive union would have destroyed are now recorded as + data and pinned: + + - `formula` stays stripped by the two write-side carry-overs and is NOT + stripped by the read door — ruled on objectui#6526 (option B): the + `ObjectFieldInspector` migration path (objectui#6043) stands, and the + registry test makes that ruling mechanical. + - `sortOrder` stays a single-site strip at `MetadataService`'s carry-over, + now explicitly recorded as the registry's one DEFENSIVE entry (objectui#6045 + measured that no shipped writer ever populated a field-level one). +- ce503e5: **Accept-set NARROWING on a published surface.** `SonnerSchema.buttonVariant` in the zod + mirror (`@object-ui/types/zod`) was `z.string()`; it is now + `z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost', 'link'])`. Values + outside those six — `'primary'`, `'danger'`, `'Default'`, `''` — used to validate and now + fail. Stated as a narrowing rather than as a fix, because it removes values the published + mirror accepted (objectui#6541). + + **What was wrong.** The same key on the same component shipped as two disagreeing published + faces: an open string to anyone validating (`@object-ui/types/zod`), and a closed + six-member union to anyone type-checking (`@object-ui/types`, `SonnerSchema.buttonVariant` + in `feedback.ts`). The TS face was already correct — only the mirror is changed here, so + this is the mirror being made to agree with a declaration that sat beside it all along. + + **Why the wide face was wrong and not merely wide.** `renderers/feedback/sonner.tsx` passes + the value straight into `<Button variant={…}>`, whose vocabulary is exactly those six keys + of `buttonVariants`. Measured on `cva` 0.7.1, an unrecognised key contributes **no** variant + class, and `defaultVariants` applies only when the value is absent *or falsy*: + + ``` + buttonVariants({ variant: undefined }) -> "… bg-primary text-primary-foreground …" default look + buttonVariants({ variant: 'ghost' }) -> "… hover:bg-accent …" real variant + buttonVariants({ variant: 'primary' }) -> "…" NO colour at all + buttonVariants({ variant: '' }) -> "… bg-primary …" silently 'default' + ``` + + So the mirror was validating values the renderer visibly breaks on: `'primary'` — the + likeliest wrong spelling, since the default variant's own class is `bg-primary` — rendered a + button with no background and no text colour, and `''` was silently reinterpreted as + `default`. Nothing that renders correctly today stops validating. + + **Blast radius, measured.** The key stays optional, so every published `sonner` node that + omits it keeps parsing. The two fixtures in the repo that set it + (`examples/schema-catalog/src/schemas/components-feedback-sonner/{error,promise-based-toast}.json`) + use `destructive` and `outline` — both inside the six. No consumer was found relying on a + seventh spelling. + + **Model inherited, not invented.** objectui#6496 landed exactly this spelling on + `ToastSchema` for the same trigger mechanism, matched to `ButtonProps['variant']` as ground + truth. This card applies the settled shape to the sibling that still disagreed with itself. +- 4ca30d0: Two widget prop types anchor their `schema` to exported schema types that extend + `BaseSchema`, instead of hand-rolled inline literals with no `BaseSchema` in + their ancestry (objectui#6576, maintainer ruling 2026-08-31 option A; folds + objectui#6914). + + - `@object-ui/types` exports `ObjectGallerySchema` (`type: 'object-gallery'`) + and `ObjectDataTableSchema` (`type: 'object-data-table'`), each `extends + BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors + of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema` + declares the two keys the widget was reading behind casts — `drillDown` + (`DrillDownConfig`) and `onRowClick` — which no declaration carried before. + - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is + `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is + writable (`visibleWhen`, a real base member, was a compile error on the + literal) — and NARROWS in one place: `type` is now required and pinned to + `'object-gallery'`. `data` stays `Record<string, unknown>[]`. + - `@object-ui/plugin-dashboard`: `ObjectDataTableProps.schema` (not exported + from the plugin index) is `ObjectDataTableSchema`. The literal's own + `[key: string]: any` is gone, so a wrong-typed base member (`visible: 42`) and + a wrong-shaped `drillDown` are refused, and `type` is pinned to + `'object-data-table'` instead of bare `string`. + + Unchanged on both, stated plainly: an UNKNOWN key still compiles, because + `BaseSchema`'s index signature is inherited (objectui#5155, open). No runtime + behaviour changes; the widgets render exactly as before. +- 7a5da14: `DetailViewSection.headerColor` is now the closed six-token vocabulary on both halves of + the contract — the TypeScript declaration and the `@object-ui/types/zod` mirror — instead of + `string` / `z.string()` (objectui#6594, maintainer ruling A of 2026-08-26 recorded at + objectstack#12126). The six are `muted`, `muted/50`, `accent`, `primary/10`, `secondary/10` + and `destructive/10`: exactly what `@object-ui/plugin-detail`'s `HEADER_COLOR_CLASSES` + resolves (objectui#6178) and exactly what `@objectstack/spec` declares on its strict + `record:details` section schema (objectstack PR #12616). + + ## ⚠️ Accept-set narrowing — these spellings stop validating + + `DetailViewSectionSchema.headerColor` was `z.string().optional()`, so **any string parsed + green** while the renderer contributed no class for most of them. It is now + `z.enum([...]).optional()`: a value outside the six is refused at parse time with + `headerColor` named in the error path, and is a `tsc` error at every authoring site typed + against `DetailViewSection`. + + **Authored metadata in this repo needs no migration.** Measured before tightening, across + the whole tracked tree: `headerColor` occurs in **ten files, none of them authored + metadata** — the renderer and its tests, the two declaration files changed here, and two + markdown notes. `examples/`, `content/`, `apps/`, `e2e/` and `docs/` contain **zero** + occurrences (positive control: `sections` and `detail-view` both hit in those directories, + so the census reached them). Nothing in the repo authors a value outside the six. + + ## The renderer's `bg-*` pass-through is deliberately NOT declared + + `headerColorClass` also hands a value that is already a complete `bg-*` class through + untouched. Ruling A rejected declaring that (option B, "the capability illusion"): whether + such a class renders depends on the host app's Tailwind build, so declaring it would promise + a capability the contract cannot keep. It stays a renderer affordance — still supported by + the renderer, never invited by the contract. The three renderer tests that exercise + off-contract values (`bg-accent`, `not-a-token`, `constructor`) now route them through a + documented `offContract()` seam in `DetailSection.headerColor.test.tsx`, which is the visible + consequence of the narrowing rather than a workaround for it: metadata still arrives as JSON + over the wire, where no compiler was involved, so the renderer must keep behaving sanely. + + ## The three ends cannot drift + + `packages/plugin-detail/src/__tests__/headerColor.contractPin-6594.test.ts` pins the resolver, + the TypeScript declaration and the zod mirror against the ruled vocabulary — the resolver's + key set one-to-one at runtime, the declaration by invariant type equality, the mirror by + reading its own enum options. It fails in **both** directions: a seventh token on any one end, + or one of the six dropped from any one end, turns it red, and the comparator itself is pinned + against synthetic inputs so the guard has been shown to fail rather than only to pass. + + ## Shape, and where it departs from the nearest precedent + + The nearest precedent is objectui#5853 (`.changeset/5853-tablecolumn-type-canonical-union.md`), + which narrowed `TableColumn.type` on the same three-ends pattern and **exported** a + `TABLE_COLUMN_TYPES` tuple for the zod mirror to build its enum from. That shape is not + available here and the difference is structural, not a preference: `packages/types/src/views.ts` + is a **type-only** module, so a tuple there would add a runtime export to the package barrel + (a value export cannot ride the barrel's `export type` block) and a runtime import edge from + the zod entry into `views.js`. #5853 had a second reason to export — producers needed its + `normalizeTableColumnType()` at their emit seam — and `headerColor` has no producer that needs + a runtime value. The literals are therefore written on each half and the anti-drift guarantee + is carried by the pin above, which also covers the third end a shared tuple could not reach: + the renderer, in a package `@object-ui/types` must not depend on. +- 2c1c967: `ObjectGridSchema`'s zod mirror declares `title`, the deprecated legacy + caption/export-file-title fallback the interface has declared all along and + `ObjectGrid` reads at both of its `schema.label || schema.title` sites + (objectui#6639, census-directed maintainer ruling 2026-08-29, declare branch: + authored `object-grid.title` nodes exist, so the key is declared rather than the + read retired — dropping the read would have silently cost those nodes their + caption). + + The gain is the typed refusal: the mirror's `.passthrough()` base was already + admitting any `title` unexamined, and it now enforces the declared `string`. + `zod-mirror-parity.test.ts`'s `UnmirroredDeclared` ledger records the key as + worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 + reclassified is what the seeded "121" now means). +- 3561bd2: **Breaking for authored metadata:** `DataTableSchema.toolbar` is RETIRED + (objectui#6881, maintainer ruling 2026-08-31). A `data-table` node that authors + `toolbar` no longer validates — the parse fails loudly on the `toolbar` path + with the remediation in the message — and the TS member is a `?: never` + tombstone, so the same document is refused at compile time. + + **What was measured.** The key was declared on both published faces — + `data-display.ts` (`toolbar?: SchemaNode[]`, "Table toolbar actions/content") + and the Zod mirror (`SchemaNode | SchemaNode[]`) — documented, mirrored, and + read by NOTHING: `data-table.tsx`, the registered renderer for + `type: 'data-table'`, contains the word only in two prose comments and never + reads `schema.toolbar`. The sibling `emptyAction` slot on the same interface IS + mounted through `SchemaRenderer`, so the census zero is a reading, not a blind + query. An author who wrote a toolbar got a green document and a blank result, + with no signal anywhere that said so — the declared-vs-enforced failure mode + that is worst for AI-authored metadata, which has nothing but the declaration + to go on. + + **Who is affected — a `toolbar` authored directly onto a `data-table` node, + in either spelling:** + + ```json + { "type": "data-table", + "columns": [{ "header": "Name", "accessorKey": "name" }], + "data": [], + "toolbar": [{ "type": "button", "label": "Refresh" }] } // ← was tolerated, rendered nothing + ``` + + now fails validation with: + + > RETIRED (objectui#6881) — never mounted by the data-table renderer; use the + > built-in toolbar chrome (searchable / exportable), or compose nodes beside + > the table + + The single-node spelling `"toolbar": { … }` — which only the Zod mirror ever + accepted; the TS face always refused it — is refused the same way, so the two + faces now agree by refusing both. + + **Who is NOT affected.** A document that never wrote the key is untouched + (`absent` stays valid), and every other `SchemaNode` slot — `emptyAction` + included — is unchanged. No fixture, example, catalog entry, doc page or app + in this repository authored the key (measured: all five + `components-complex-data-table` catalog schemas are toolbar-free, and every + other `toolbar` occurrence repo-wide is an i18n key, an ARIA role, or an + unrelated React prop of the same name). + + **Migration:** use the built-in toolbar chrome (`searchable` / `exportable`), + or compose your own nodes beside the table. Per the ruling, a real toolbar + slot must arrive as a redesigned proposal WITH its enforcing reader — published + zero-consumer capability gets no sunk-cost exemption. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated key. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- bf97b98: feat(types): declare `renderCellEditor` and schema-level `cellClassName` on `DataTableSchema` + + `data-table` has read both keys on its production path all along — `renderCellEditor` + through a `(schema as any)` cast, `cellClassName` by destructuring it into the class of + its three utility cells (the selection checkbox, the row number, the row actions). + Neither was declared, so authoring either one was unchecked: a misspelling produced no + error and no widget, and no editor completion offered them. + `DataTableSchema` now declares both, and the cast in `data-table.tsx` is gone rather + than replaced. + + What you can write after this change that you could not write before, exactly: + **nothing new runs.** Both keys had the same effect yesterday, because + `BaseSchema`'s `[key: string]: any` already admitted them at any type at all. What + changes is that they are now *checked* and *documented*: + + ```ts + const schema: DataTableSchema = { + type: 'data-table', + columns, data, + cellClassName: 'px-2 py-1 text-sm', // utility cells only (see below) + renderCellEditor: ({ column, value, commit, cancel }) => + column.type === 'select' + ? <MyPicker value={value} onSelect={commit} onDismiss={cancel} /> + : null, // null → fall through to the built-in editor + }; + ``` + + ⚠️ **One reject direction, deliberate.** Because the keys were previously absorbed by + the index signature as `any`, authored values of the *wrong shape* also compiled and + silently did nothing. They are now compile errors: + + - `cellClassName` is declared `string`, matching `BaseSchema.className` and + `TableColumn.cellClassName`. The renderer passes it through `cn()`, which would + also swallow `['a','b']` or `{ a: true }` — those spellings now fail to compile. + One authored spelling for a class slot is the contract. + - `renderCellEditor` is declared as the function `data-table` actually calls. A + non-function value (or a function with an incompatible context/return type) now + fails to compile instead of being ignored at runtime. + + ⚠️ **What the schema-level `cellClassName` actually styles.** It is NOT the + table-level twin of the per-column key: the two reach **disjoint** cells. Measured on + the render, the schema-level key is folded into the **utility** cells only — the + selection-checkbox cell, the row-number cell and the row-actions cell — while every + **data** cell folds `TableColumn.cellClassName` and nothing else. Row density is + therefore a pair of settings (`ObjectGrid` sets both), and the schema-level key alone + leaves data cells at the primitive's default `p-4`. The docblock, the zod `describe` + and `content/docs/components/complex/data-table.mdx` all say this now. + + No runtime behaviour changed anywhere, and nothing was retired. The zod mirror + (`@object-ui/types/zod`) gains both keys in the same stroke, so the validator accepts + what the published types now invite. +- b0d308d: Retire `ChartDataSeries.data`, and correct `ChartSchema.categories`' prose to the read it + has always had (objectui#6896, ADR-0049 enforce-or-remove; maintainer ruling 2026-08-31). + + ⚠️ **BREAKING for anyone authoring a static `ChartSchema` node**, shipped as `minor` + because this repository's `major` is a cross-repo pin to `@objectstack`'s major rather than + a severity dial. The break is announced here, which is the channel that carries it. + + ## `ChartDataSeries.data` — RETIRED + + `data: number[]` was **required** on every authored series and read by nothing. + `normalizeChartSchema`'s `normalizeSeries` (`@object-ui/plugin-charts`) reads `dataKey` / + `name`, `label`, `chartType` / `type`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis` + and `color` — `data` is not among them. Rows come from the chart node's **chart-level** + `data`, a key `ChartSchema` never declared at all, which survives only because `BaseSchema` + carries an index signature and so suppresses excess-property checking on chart literals. + The declaration therefore demanded numbers no reader consumed, and no author could omit + them. + + FROM → TO: + + - `data: number[]` (required) → **`data?: never`**, an ADR-0049 retirement tombstone. Put + the rows on the chart node's chart-level `data`, name the column with the series' `name` + (or `dataKey`), and put the category axis on `xAxisKey`. + + The accept set moves in **both** directions, and both are deliberate: + + - **narrowing** — `{ name: 'Revenue', data: [1, 2, 3] }` was accepted and is now refused; + - **widening** — `{ name: 'Revenue' }` was refused (`data` was required) and is now + accepted, which is the shape the renderer has always read. + + Deleting the member outright was the option **not** taken: `ChartDataSeriesSchema` is a + non-strict `z.object`, which strips an undeclared key in silence — one silent no-op traded + for another. The tombstone keeps the key declared and unwritable, so an authored value is a + **named refusal carrying its own remedy**: `?: never` on the interface (a `tsc` error at the + authoring site) and `retirementTombstone()` on the Zod mirror (`code: 'invalid_type'`, the + key named in the issue `path`, the migration note as the message). Per the ruling — + 创业阶段不渐进 — the tombstone is immediate: there is no deprecation window and no + dual-reading period. + + ## `ChartSchema.categories` — NOT retired; its prose was wrong + + The key keeps its behaviour. It is read as an **alternative series list**, consulted only + when `series` is absent, each entry normalized through the same series normalizer where a + bare string means `{ dataKey }` — so the strings name **columns to plot**. The category axis + comes from `xAxisKey` / `xAxis`. The docblock said "X-axis labels/categories", so an author + following the documentation got a different chart from the documented one. Prose follows + machine: the docblock, the `ChartDataSeries` header (which promised numbers "positionally + aligned with the chart's `categories`", a model that never existed) and the Zod + `.describe()` were corrected to the read. No behaviour changed, and `categories` remains + writable. + + ## The census behind the retirement, re-measured on this branch + + Re-measured at merge-base `2c3cd1b75` rather than inherited from the card, with a control + that had to hit in the same query — the instrument was not blind: it scores + `packages/types/src/__tests__/report-schema-authoring-face.test.ts` **4** authoring sites. + + ⚠️ One correction to the record the ruling rests on. The ruling states *zero* authorship of + a populated `series[].data` outside tests across `packages/` / `apps/` / `examples/`. The + re-measurement finds **one such site inside those roots** — + `packages/types/examples/data-display-examples.json` (2 series) — plus **four outside** + them, in documentation: `content/docs/api/schema-reference.md` (3) and + `content/docs/core/report-schema.mdx` (1). + + Every one of the five is documentation or an unreferenced example, **not a consumer**: none + is imported, type-checked, parsed by a test or rendered anywhere, `packages/types` does not + publish its `examples/` directory, and each authors the *documented* model — month names in + `categories` next to inline `series[].data` — which renders an empty chart today, because + `data` is dropped and `categories` is ignored whenever `series` is present. They are + instances of the divergence this change closes rather than users of a working inline-data + model, so the ruling's conclusion is unaffected. Their migration is filed separately; until + it lands, an author copying them now trips the tombstone and reads the remedy instead of + being silently dropped. + + Pinned in `packages/types/src/__tests__/chart-inline-data-retired.test.ts` — both channels, + the announcement itself, and a counter-probe that builds the deletion this retirement did + not choose and measures the contrast in the same run — and in + `packages/plugin-charts/src/normalizeChartSchema.test.ts`, where the `categories` read now + has behaviour coverage it never had. +- d4493fd: Repair the `filter-builder` mirror: the field key is `value`, the type + vocabulary is the value families the component actually folds a column into, + and a filter group is `{ id, logic, conditions }` (objectui#6939, maintainer + ruling recorded 2026-09-02 — one of the eight groups on that card, dispatched + as its own PR per the ruling). + + Three independent mis-declarations sat in one member, and each is a key-name or + vocabulary **move** rather than a missing optional key: + + 1. **`FilterFieldSchema` required `name`.** Every read site matches an entry by + `value` — `fields.find((f) => f.value === …)` in `getOperatorsForField`, + `changeField`, `getInputType` and `renderValueInput`, `fields[0]?.value` in + `addCondition`, and `<SelectItem value={field.value}>` in the field dropdown + (`packages/components/src/custom/filter-builder.tsx`). `name` had zero read + sites, and `FilterBuilderProps.fields` in that same file already declares + `Array<{ value, label, type? }>`. `@object-ui/fields`' `deriveFilterFields`, + the real producer that builds this list from an object schema, emits `value` + too, and the published doc + (`content/docs/components/complex/filter-builder.mdx`) has declared + `value: string` all along. + 2. **Its `type` enum was `string | number | date | boolean | select`.** `string` + is a phantom, and `text`, `datetime` and `time` — three of the six + `FilterValueFamily` members the component folds a column into — were all + refused. + 3. **`FilterGroupSchema` was `{ operator, conditions }`.** The gate is + `isValidGroup`, which tests `Array.isArray(v.conditions)` and + `v.logic === "and" || v.logic === "or"` and nothing else. + + All five `components-complex-filter-builder/*` catalog entries author + `{ value, label, type }` fields and a `{ id, logic, conditions }` group — the + registration's own `inputs` / `defaultProps` spelling — so the mirror refused + them while the renderer drew them. Re-measured on `origin/main` at `3e01cb55f`, + both faces untouched: four refusals at the root (`search-interface` roots at + `stack`, so `objectui check` counts four for this row, and the `filter-builder` + it wraps is refused on its own), and five renders this change leaves + byte-identical (11 / 76 / 65 / 11 / 57 elements, same tag census, same + `textContent` SHA-256). + + **This is `minor`, not `patch`, and the reason is that the accept set MOVES.** + The ruling grades this class "patch where the accept set only widens toward what + already renders"; that grading does not hold here. Three refusal classes are + created, each of which was a legal document before: + + - **`fields[].name` as the field key.** `{ name, label, type }` now refuses: + `value` is required and `name` is not declared, so a field entry spelled the + old way has no identity at all. A document carrying BOTH keys still validates + (the undeclared one is stripped), which is the migration path. + - **`type: 'string'`.** Refused outright. It reached the text control only + through the unrecognised-word fallthrough in `valueFamilyForFieldType` — + measured indistinguishable from a nonsense spelling — so nothing that read the + key ever saw it; but a document that carried it did validate before and does + not now. + - **`{ operator, conditions }` as the group shape.** Refused: `logic` is + required. This is the loudest of the three at render time — a group spelled + that way already failed `isValidGroup`, fell back to `EMPTY_GROUP` and drew an + **empty board** (76 elements and three condition rows became 11 and none), so + the mirror was blessing a shape that never rendered. + + The widening half, for completeness: `fields[].value`, `type: 'text'` / + `'datetime'` / `'time'`, and the `{ id, logic, conditions }` group all become + legal. On the TypeScript twin the same move applies to `FilterField.value` / + `FilterField.type` and to `FilterGroup.logic`, so a consumer reading + `field.name` or `group.operator` stops compiling. `major` is not available in + this repository (`check-changeset-no-major`), so a breaking change is `minor`. + + **Two places this departs from a literal reading of the ruling, both measured + and both flagged for contract review rather than made quietly:** + + - **`select` is RETAINED** in the type vocabulary. The ruling's six-member list + inherits the finding card's description of `select` as "extra"; it is not. + `selectLikeTypes = ["select", "status"]` gives it its own operator bucket + (`equals` / `in` / `notIn`) and its own value control — measured, a `select` + column draws the option-driven Select and no `<input>` at all, against a text + box for an unrecognised spelling. Dropping it would refuse a spelling this + mirror accepts *today* and the renderer draws distinctly, which is a fresh + instance of the class objectui#6939 exists to close. + - **The group's `id` is declared OPTIONAL.** `isValidGroup` never consults it + and nothing reads `filterGroup.id`; deleting it from an authored group renders + byte-identically. Requiring it would invent a refusal the renderer does not + make. It stays *declared* because a plain `z.object` strips unknown keys in + silence, so an undeclared `id` would be admitted unvalidated — declaring it + buys the type check (`id: 42` now refuses) for a key the catalog authors, + `EMPTY_GROUP` emits and `onChange` round-trips. + + **What this does NOT reach, stated rather than left as an absence.** Two of the + four census entries — `product-search` and `with-conditions`, plus the + `filter-builder` nested in `search-interface` — still refuse afterwards, on a + FOURTH divergence the ruling does not address: they author + `conditions[].operator` as `eq` / `gt` / `lt`, while `FilterOperatorSchema` is + the spec's canonical `equals` / `greater_than` / `less_than`. Swapping only + those three spellings makes both entries parse, which is pinned, so the claim + "the three ruled divergences are gone from all four" is measured. That + vocabulary is a genuine fork needing its own ruling — the builder's dropdown ids + are `notEquals` / `greaterThan`, which this mirror also refuses, while the + canonical spellings it accepts render a **blank** operator trigger — and it is + reported on objectui#6939 rather than decided here. Seven further live field + types (`status`, `currency`, `percent`, `rating`, `lookup`, `master_detail`, + `user`) each have their own bucket and control and are still refused; they were + refused before this change as well, so that gap is pre-existing rather than a + regression introduced here, and it is reported on the same card. The published + doc for this component already offers all fourteen spellings and already marks + `type` OPTIONAL, which the mirror still does not — a third declaration that + agrees with the renderer, pinned here so the gap is measured rather than + asserted, and left for the same review. +- 240b80f: Rename `KanbanColumn.items` to `cards`, in both halves of the published surface + (objectui#6939, maintainer ruling 2026-09-02). + + **Breaking, deliberately.** `KanbanColumn` declared its card list as `items` in + `complex.ts` and in the zod mirror `complex.zod.ts`. Every board reads `cards`. + Measured on `origin/main` `78a3cc238`: `KanbanImpl.tsx` reads `.cards` on 12 + lines, `KanbanEnhanced.tsx` on 8, and `bucketCardsIntoColumns` twice more as + `col.cards || []`; `.items` had **zero** read sites in either board (a + same-shaped `.title` control on the same two files returns 8 and 3, so those + zeros are readings and not a mis-shaped probe). Both catalog entries, the + plugin docs and `content/docs/api/schema-reference.md` all author `cards`. + + The consequence was `declared !== enforced` on a published mirror: every + authored kanban document failed `safeValidateSchema` with `: Invalid input` + while rendering perfectly, which is how the type sat in objectui#6318's + "carries a registered component type but did not validate" bucket. + + **Why the rename went this way and not the other.** Renaming the twelve read + sites to `items` was considered and rejected: `bucketCardsIntoColumns` reads + `col.cards || []`, so the `items` spelling buckets every column to zero cards. + Measured through the render harness in + `examples/schema-catalog/test/kanban-column-cards-6939.test.tsx`, the + `basic-kanban-board` entry goes from 64 elements reading `To Do2 … Design new + feature …` to 45 elements reading `No cards3 columnsTo Do0 …` — an empty board. + The declaration, not the corpus, was the wrong side. + + **Migration.** If you author `KanbanColumn` objects against `@object-ui/types` + or validate them through `@object-ui/types/zod`, rename `items` to `cards`. + Documents that already author `cards` — which is every document in this + repository, and what the boards have always rendered — need no change and now + validate. Documents authoring `items` are refused rather than silently drawn as + an empty board. + + `@object-ui/plugin-kanban` is unchanged; it already declared `cards`. +- 9e37d9b: `binding` on a component input is framework-set, not author-declared: `@object-ui/types` gains `InjectedComponentInput`, the `'field'` binding arm is retired from `@object-ui/sdui-parser`'s `RegistryConfigLike`, and the `as ComponentMeta` cast at the injection seam in `@object-ui/core` is gone (objectui#6950; maintainer ruling of 2026-09-07, director decision batch #69; ADR-0049 enforce-or-remove). + + **What was measured.** `binding` was published — the manifest serializer forwards it — and read — `validateTree` records a binding site for it — while `ComponentInput`, the authoring type every registration writes against, did not declare it. The one writer in the tree, `ELEMENT_DATA_SOURCE_INPUT`, therefore carried a hand-written inline type and reached a registration's `inputs` through `as ComponentMeta` in `Registry.register`. Declared narrower than enforced, on a published type — and `ComponentInput`'s own docblock listed `binding` among the forwarded per-input keys. + + **The ruling** answered the product question the card asked — may an ordinary registration declare a binding input? — with no. So: + + - **`@object-ui/types`** exports `InjectedComponentInput`, an `interface … extends ComponentInput` with the required marker `binding: 'object'`. `ComponentInput` itself does not change: no member is added, and authoring `binding` on a registration stays an excess-property `tsc` error — now on purpose and documented at the interface. The two tombstone docblocks that listed `binding` as a forwarded key now say it is forwarded from the framework's injected input, not authored. + - **`@object-ui/core`** types `ELEMENT_DATA_SOURCE_INPUT` as `InjectedComponentInput` and splices it through a typed local; the cast is gone. Runtime behaviour is unchanged — the same key, `type`, `binding` and `description` reach the manifest, and `validateTree` still records the binding site. + - **`@object-ui/sdui-parser`** narrows `RegistryConfigLike.inputs[].binding` from `'object' | 'field'` to `'object'`. The `'field'` arm had zero writers — every `binding:` literal in `packages/`, `apps/` and `examples/` is `'object'`, 7 of 7 at this change's merge-base — and nothing on either side of the manifest resolved a field binding. **Breaking, deliberately:** a config that feeds `manifestFromConfigs` a `binding: 'field'` input is now a type error instead of a manifest entry the server would never resolve. `ManifestInput.binding`, the manifest reader's vocabulary, is not narrowed by this change. + + If a real need for author-declared bindings is ever measured, it is filed as a widening of `ComponentInput` with the vocabulary decided then — not by putting the cast back. +- 5ad86dd: **Breaking for authored metadata:** `TextSchema.value` is RETIRED (objectui#6951, + maintainer ruling A1 of 2026-09-04; objectui#7016; ADR-0049 enforce-or-remove). + A `text` node that authors `value` no longer validates: the parse fails loudly on + the `value` path with the explanation in the message, the TS member is a + `?: never` tombstone so the same document is refused at compile time, and the + renderer no longer reads the key. Write `content`. + + **What was measured, on this branch's base.** `TextSchema` declared two spellings + for its one content slot — `content` (read first) and `value` (the fallback limb + of `{schema.content || schema.value}` at `renderers/basic/text.tsx:162` and + `:167`) — both declared by objectui#6150, whose docblock called the pair "a + dialect, not a design" and deferred the choice. The ruling's premise, that + `value` is the minority spelling, was measured before any edit over the four + roots it named: **776 `content`-only `text` nodes, 25 `value`-only, 0 authoring + both** across `examples/` (674 / 13), `apps/` (59 / 0), the `examples/` + directories under `packages/` (0 / 1) and `content/docs/**` (43 / 11) — a + thirty-to-one majority for `content`, so the retirement went ahead as ruled. + (A further 14 `{ value, label, type: "text" }` objects in the filter-builder + catalog entries are field descriptors whose `type` is a field type, not `text` + nodes, and were excluded by kind.) + + **Who is affected — a `value` authored on a `text` node:** + + ```json + { "type": "text", + "value": "Hello" } // ← was tolerated (rendered as the fallback) + ``` + + now fails validation with: + + > RETIRED (objectui#6951) — `value` is no longer part of TextSchema; write + > `content`. It was a second spelling of the one content slot, read only as the + > fallback limb of `schema.content || schema.value`, and was retired under + > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling A1, + > 2026-09-04). The renderer reads `content` alone now, so an authored `value` + > would render nothing. Rename the key; the string is unchanged. + + **Two published faces, one retirement.** The TypeScript interface `TextSchema` + (`@object-ui/types`, `layout.ts`) declares `value?: never`; the Zod mirror + `TextSchema` (`@object-ui/types/zod`, `layout.zod.ts`) declares `value` as a + `retirementTombstone()`, so the key stays DECLARED and is refused BY NAME — + a plain deletion would have let an authored `value` ride `BaseSchema`'s + `.passthrough()` into a silent blank, which is worse than the tolerated + fallback it replaces. The `value?: string` members of `TextSpanSchema` and + `TabsSchema` in the same file are other schemas' contracts and are unchanged. + + **`@object-ui/components`** — the `text` renderer renders `{schema.content}` at + both arms (the `|| schema.value` limb is gone from each), and the `context-menu` + renderer's built-in fallback trigger node now spells `content`. Nothing else in + the package moves. **`@object-ui/plugin-dashboard`** — its three placeholder + `text` nodes ("chart type is not supported yet", "Custom widget — set + `component`…", the retired-widget notice) spell `content` so they keep rendering; + their wording is unchanged and still pinned. + + **Who is NOT affected.** A document that already wrote `content` is untouched; + `content`, `variant`, `align` and `className` are unchanged; `absent` stays + valid (`{ "type": "text" }` still parses). Every in-repo document that authored + `value` on a `text` node was rewritten to `content` in the same change: nine + `examples/schema-catalog` entries, `packages/types/examples/zod-validation-example.ts`, + eleven doc fences under `content/docs/`, and the `@object-ui/components`, + `@object-ui/react` and `@object-ui/types/zod` README samples; the catalog is now + pinned tree-wide against the retired spelling. + + **Migration:** rename `value` to `content` on every `text` node; the string is + unchanged. If a document authored both, `content` was already the value that + rendered — delete `value`. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spelling. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 16a725f: **Breaking for authored metadata:** `TreeViewSchema.data` is RETIRED (objectui#6951, + maintainer ruling B1 of 2026-09-04; ADR-0049 enforce-or-remove). A `tree-view` + node that authors `data` no longer validates: the parse fails loudly on the + `data` path with the explanation in the message, the TS member is a `?: never` + tombstone so the same document is refused at compile time, and the renderer no + longer reads the key. Write `nodes` — or bind the tree with `bind`, which is + unchanged and still read first. + + **What was measured, on this branch's base.** `TreeViewSchema` declared two + spellings for its one inline-nodes slot — `nodes` (read second) and `data` (read + third: `boundData || schema.nodes || schema.data || []` at + `renderers/data-display/tree-view.tsx:105`), both declared by objectui#6150. + `data` had been REQUIRED until objectui#6939 / PR #7533 made it optional, so + this retirement starts from a declared-and-optional member on both faces. The + in-repo corpus at the retirement: seven `tree-view` nodes under + `examples/schema-catalog` and `packages/types/examples` plus one `content/docs` + fence — six on `nodes`, two on `data` (`packages/types/examples/data-display-examples.json` + and `content/docs/api/schema-reference.md`), both rewritten; no package source + authored either spelling. + + **Who is affected — a `data` authored on a `tree-view` node:** + + ```json + { "type": "tree-view", + "data": [{ "id": "root", "label": "Project" }] } // ← was tolerated (read third) + ``` + + now fails validation with: + + > RETIRED (objectui#6951) — `data` is no longer part of TreeViewSchema; write + > `nodes` (or bind the tree with `bind`). It was the second spelling of the one + > inline-nodes slot, read only as the last limb of + > `boundData || schema.nodes || schema.data || []`, and was retired under + > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling B1, + > 2026-09-04). The renderer reads `bind` then `nodes` now, so an authored `data` + > would render an empty tree. Rename the key; the array is unchanged. + + **Two published faces, one retirement — and why a tombstone, not a deletion.** + The TypeScript interface `TreeViewSchema` (`@object-ui/types`, `data-display.ts`) + declares `data?: never`; the Zod mirror `TreeViewSchema` (`@object-ui/types/zod`, + `data-display.zod.ts`) declares `data` as a `retirementTombstone()`. `BaseSchema` + already declares `data?: any` (`z.any().optional()` on the mirror), so DELETING + the member would not have refused the key — it would have ADMITTED it, + unvalidated, through the base member, and the renderer would have drawn an empty + tree. The tombstone on the extended schema shadows the base member on both + faces; the pin measures the base accepting the very document the extended + schema refuses. + + **What the ruling kept, deliberately.** `nodes` stays OPTIONAL and no "at least + one of" presence rule was added: `{ "type": "tree-view", "bind": "treeNodes" }` + is a legal, rendering document (`bind` is the first source the renderer reads), + and a bare `{ "type": "tree-view" }` stays legal as PR #7533 left it. + `TreeNode.data` — the per-node payload on each tree node — is a different + member on a different schema and is untouched. + + **`@object-ui/components`** — the `tree-view` renderer's read is + `boundData || schema.nodes || []`; nothing else in the package moves. + + **Who is NOT affected.** A document that already wrote `nodes` (the four + `components-data-display-tree-view/*` catalog entries and the nested tree in + `components-complex-resizable/editor-interface.json`) is untouched; `title`, + `bind`, the selection / expansion keys and `className` are unchanged. The + catalog is now pinned tree-wide against the retired spelling. + + **Migration:** rename `data` to `nodes` on every `tree-view` node; the array is + unchanged. If a document authored both, `nodes` was already the value that + rendered — delete `data`. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spelling. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 4dfdcc3: **Breaking for authored metadata:** the `exportOptions` member of the ListView + zod mirror (`ListViewSchema` in `@object-ui/types/zod`) is now `@objectstack/spec`'s + own `ListViewSchema.shape.exportOptions`, bound by reference rather than + restated (objectui#6956). A `list-view` document that authors the retired `'pdf'` + format — in either spelling, `exportOptions: ['csv', 'pdf']` or + `exportOptions: { formats: ['pdf'] }` — or a sixth key on the object form + (`{ formats: ['csv'], compression: 'gzip' }`) no longer validates through this + package's mirror. It never validated at the platform's publish gate: + `@objectstack/spec` 17.0.0 removed `'pdf'` from the format enum (objectstack#8010; + PDF export itself was declined as objectstack#1301 NOT_PLANNED) and made the + object form strict, so the mirror was passing locally what the platform refuses + with an `os migrate meta --from 16` prescription — an author saw green here and + a refusal upstream. `streaming`, the fifth spec key, is now declared on this face + (the renderer honoured it; no local declaration carried it). + + **What was measured, on this branch's base.** The mirror declared a pre-#8010 + shape of its own — `'pdf'` in both branches, no `streaming`, a non-strict + `z.object` — and `ListViewInferred` is `z.input` of that mirror, so the + `ListViewSchema` TYPE the ListView renderer is written against disagreed with + its sibling `ObjectGridSchema['exportOptions']` (the clean five-key + `ListViewExportOptions`), and the renderer could only read `streaming` through + `as any`. Against the installed pin (`@objectstack/spec@17.2.0`, not a working + tree), `ListViewSchema.shape.exportOptions` from `@objectstack/spec/ui` lifts + `['csv', 'xlsx']` to `{ formats: ['csv', 'xlsx'] }`, refuses `['csv', 'pdf']` + with the migration prescription, refuses `{ formats: ['csv'], compression: 'gzip' }` + (strict), and accepts `{ formats: ['csv'], streaming: true }` with the value + intact. The mirror now IS that schema object, so the four verdicts are the + spec's by construction; `export-options-spec-parity.test.ts` pins the identity, + the four verdicts, and the survival of `streaming` through a parse. + + **The TS face follows.** `ListViewSchema['exportOptions']` is now the spec's + INPUT type: `ListViewExportFormat[] | ListViewExportOptions` — the bare array + stays admissible on input because nothing on the render path parses, and the + object arm IS the same `ListViewExportOptions` that `ObjectGridSchema` and + `NamedListView` carry. One spec key, one type, on every local authoring surface; + `'pdf'` is a compile-time refusal in both spellings. + + **Who is NOT affected.** A document authoring `['csv', 'xlsx']`, + `{ formats: ['csv', 'json'] }` or any combination of the five spec keys is + untouched; absent stays valid; the member's description is now the spec's own + text. The spec's parse-time lift of a bare array to `{ formats }` now runs for + whoever parses through this mirror as well. + + **Migration:** delete `'pdf'` (the surviving formats are `'csv'`, `'xlsx'` and + `'json'`); delete any key outside `formats` / `maxRecords` / `includeHeaders` / + `fileNamePrefix` / `streaming`. `os migrate meta --from 16` lists the mechanical + edits for existing sources. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated value. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 446d93d: **Breaking for authored metadata:** `MarkdownSchema.sanitize` and + `MarkdownSchema.components` are RETIRED (objectui#6972, ADR-0049 + enforce-or-remove). A `markdown` node that authors either key no longer + validates: the parse fails loudly on that key's path with the explanation in + the message, and both TS members are `?: never` tombstones, so the same document + is refused at compile time. The two keys do not share a disposition — triage + recorded the asymmetry — so each is argued below. + + ## `sanitize` + + **What was measured, on this branch's base.** `sanitize` was declared + `?: boolean` with `@default true` on both published faces — `data-display.ts` + and the Zod mirror — documented, and read by NOTHING. Worse than an ordinary + inert key, it implied a switch that does not exist: sanitization is + **unconditional**. `rehypePlugins` in `plugin-markdown/src/MarkdownImpl.tsx` is + a module-level `const` array whose last link is `[rehypeSanitize, sanitizeSchema]`, + handed to `ReactMarkdown` as-is — no ternary, no `if`, no runtime assembly. + `MarkdownRenderer` forwards exactly `content` and `className`, and + `MarkdownImplProps` accepts only those two. A repo-wide grep for + `schema.sanitize` over `packages/` and `apps/` returns nothing, against a + control of 20 `.tsx` files reading `schema.content` in the same query shape, so + the zero is a reading, not a blind query. An author writing `sanitize: false` + believed they turned XSS filtering off; one writing `sanitize: true` believed + they turned it on. Neither was true. + + **Why remove and not enforce.** The enforce arm of enforce-or-remove for this + key is a switch that DISABLES XSS sanitization, which is not an acceptable + outcome; for `sanitize` the ruling collapses to remove (triage on + objectui#6972). + + **Who is affected — a `sanitize` authored onto a `markdown` node:** + + ```json + { "type": "markdown", + "content": "# Hello", + "sanitize": false } // ← was tolerated, changed nothing + ``` + + now fails validation with: + + > RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a + > fixed last link of the markdown renderer's rehype chain, and no value of this + > key ever switched it. There is no authored spelling that disables XSS + > sanitization; delete the key. + + ## `components` + + **What was measured, on this branch's base.** `components` was declared + `?: Record<string, any>` ("custom components for markdown elements") on both + faces and read by NOTHING: the `components` map `MarkdownImpl` hands to + `ReactMarkdown` is its own module-level `mdComponents` (the mermaid / metadata + fence overrides), never merged with anything off the schema, and + `grep -rn "schema.components"` over `packages/` and `apps/` returns nothing + against the same `schema.content` control. The premise the PM declared + falsifiable — *no host path consumes a `components` map* — was re-measured + before this half was written: `MarkdownImplProps` has no such prop, `LazyMarkdown` + receives only `content` and `className`, and no plugin API, app-shell or runner + site passes one. + + **Why remove and not wire, and why not a runtime slot.** A map of React + component overrides is not a value a JSON document can author — the same shape + as the handler keys objectui#6124 retired ("JSON has no function value"). The + `runtime-slot` disposition keeps a TypeScript twin callable when a host-supplied + value actually reaches a renderer; nothing reaches this one, so there is no twin + to keep and the TS face refuses it outright. This half is the PM's disposition + under a declared veto window on objectui#6972, not a maintainer ruling; the PR + stays draft for contract review. A real override slot must arrive as a proposal + WITH its enforcing reader, not by reviving this key. + + ```json + { "type": "markdown", + "content": "# Hello", + "components": { "h1": "h2" } } // ← was tolerated, changed nothing + ``` + + now fails validation with: + + > RETIRED (objectui#6972) — never read: the markdown renderer forwards only + > `content` and `className`, and a map of React component overrides is not a + > JSON-authorable value. Delete the key; the fenced mermaid / metadata block + > overrides are the renderer's own fixed map, not an authoring surface. + + ## Both keys + + **Two published faces.** `@object-ui/plugin-markdown` re-exports `MarkdownSchema` + from `@object-ui/types` (one authority since objectui#6172) rather than + declaring a copy, so the retirement reaches its consumers through the same + declaration — which is why this changeset names the plugin as well: no plugin + source changes, but the type its published face exposes narrows, and its own + test now pins that the refusal arrives there. + + **Who is NOT affected.** A document that never wrote either key is untouched + (`absent` stays valid), `content` and `className` are unchanged, and the + renderer's behaviour is byte-identical — it sanitized unconditionally before + and does now, and its fenced-block overrides are the same fixed map. One + in-repo fixture authored either key + (`packages/types/examples/data-display-examples.json#examples.markdown`, + `"sanitize": true`); the key is deleted from it and the fixture is now pinned + to parse green. No fixture, catalog entry, doc snippet, skill or app in this + repository authored `components` on a markdown node. + + **Migration:** delete the keys. There is nothing to replace either with — the + behaviour `sanitize` claimed to control is always on, and no authored spelling + overrides markdown elements. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated key. It is not `major` per + this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 98d4108: Converge the two named select-option types onto one spec-derived base (objectui#7014, Q1). + + `SelectOptionMetadata` (`field-types`, the object-metadata read model) and `SelectOption` + (`form`, the SDUI form vocabulary) each restated the select-option vocabulary by hand. + Both now extend the new `SelectOptionBase`, which derives the spec's keys from + `@objectstack/spec/data` **by reference** and writes out only the divergences. A key the + spec adds now reaches both faces with no edit here; a key it removes becomes a compile + error at the sites that read it, instead of a hand copy that goes on compiling while the + contract moves underneath it. + + **What widened.** Exactly one key, on one face: `SelectOptionMetadata` gains + `default?: boolean`. It is a spec key that face could not describe before — ruled + `enforce` on the object-field face (objectstack#7246), where the engine seeds a new + record from the option marked `default: true` — and it arrives OPTIONAL, so every + document that face accepted before is still accepted. + + **What narrowed.** Nothing. Both faces resolve to member-for-member what they resolved to + before (`SelectOption` identically; `SelectOptionMetadata` identically plus `default`), + pinned invariantly against the pre-convergence member lists in + `select-option-tier1-convergence-7014.test.ts` so a future "unification" cannot quietly + drop a key. `SelectOption.value` keeps its deliberate widening past the spec's machine + identifier (numeric/boolean values for standalone forms, objectui#3090), now named in an + `Omit` instead of restated. + + **The convergence is an EXTENSION, not a replacement.** objectui legitimately carries + keys the spec does not: `description` (`LookupField` searches it, objectui#6153) on the + metadata face, and `disabled` / `icon` on both. The spec's `SelectOptionSchema` is strict + over exactly `{label, value, color, default, visibleWhen}` and refuses each of those + three **by name**, so they are declared as objectui dialect with that refusal written + into the published JSDoc rather than described as spec-aligned. These are read-model keys + and must never reach an authored object document — a field's `options` are routed through + the strict schema, so one of them fails the whole field. + + `SelectOptionBase` is exported from `@object-ui/types` because it appears in the + `extends` clause of both published interfaces. +- 4388f71: **The overlay family's `trigger` slot now declares the node array its Zod mirror, its runtime and its own shipped defaults already accept** (objectui#7081). + + `trigger` on `DialogSchema`, `AlertDialogSchema`, `SheetSchema`, `DrawerSchema`, `PopoverSchema`, `HoverCardSchema` and `DropdownMenuSchema` widens from `SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face — the spelling `ContextMenuSchema` and `TooltipSchema` already carried. Each member keeps its optionality (the first four optional, the last three required). `SchemaNode` itself is unchanged. + + This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/overlay.zod.ts` already spelled every one of these keys `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: every overlay renderer hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and every registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying a renderer's own default into a typed document is no longer a type error against the type that shipped it. The seven docs pages' `trigger` rows follow the declaration. + + Triage on the card (2026-09-03): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). +- c93b4d5: `disabled` accepts a predicate string — `boolean | string`, the `BaseSchema` union — on + the 18 concrete schemas that used to narrow it back to `boolean` (objectui#7087, + maintainer ruling 2026-09-01: option 1, scoped to `disabled`). + + `visible` and `disabled` are twins: objectui#4581 widened both on `BaseSchema` on the same + evidence — `SchemaRenderer` evaluates both through `evaluator.evaluateCondition` rather + than reading either as a boolean. After that widening, 0 of the 124 `extends BaseSchema` + interfaces redeclared `visible`, while 18 still carried a pre-widening + `disabled?: boolean` of their own, with matching `z.boolean()` mirrors. So + `disabled: "${data.status === 'locked'}"` — the capability the renderer implements and + the base type advertises — was a type error and a zod refusal on `ButtonSchema`, + `InputSchema`, `TextareaSchema`, `SelectSchema`, `CheckboxSchema`, `RadioGroupSchema`, + `SwitchSchema`, `ToggleSchema`, `SliderSchema`, `FileUploadSchema`, `DatePickerSchema`, + `CalendarSchema`, `InputOTPSchema`, `FormSchema`, `ComboboxSchema`, `ActionSchema`, + `CollapsibleSchema` and `ToggleGroupSchema`. + + Those 18 redeclarations are removed, on both faces. The interfaces inherit + `BaseSchema.disabled` the way they always inherited `visible`; the zod mirrors inherit + `base.zod.ts`'s `z.union([z.boolean(), z.string()])` through `.extend()`'s merged + `.shape`, so there is no second spelling of the union to drift from — the route + `ChatbotSchema` took in objectui#6169. + + **Additive for authors**: a predicate string is now accepted where it was refused; every + boolean that parsed before parses unchanged, and a number is still refused at path + `disabled`. Runtime behaviour does not change — the renderer already evaluated both twins. + + **Out of scope, per the ruling**: `label` (29 narrowings) and `description` (32) carry + `string | I18nLabel` i18n semantics and wait for their own ruling; the independent + `disabled?: boolean` declarations on shapes that do not extend `BaseSchema` + (`SelectOption`, `RadioOption`, `FormField`, `ComboboxOption`, `AccordionItem`, + `ToggleGroupItem`, and the rest of that family) are not narrowings and are untouched. +- 8ad218d: `AlertDialogSchema` now declares the four keys the `alert-dialog` renderer actually reads + (objectui#7104): `content` (the dialog body, `SchemaNode | SchemaNode[]` like every sibling + overlay), `cancelText` and `actionText` (the footer's two button labels — each button renders + only when its label is set; there is no renderer default) on BOTH faces, and `onAction` (the + confirm button's click handler) as a RUNTIME SLOT in the objectui#6124 shape: callable on the + TypeScript face, refused by name in the zod mirror because JSON has no function value. + + Until now none of the four was declared anywhere. They were accepted only through + `BaseSchema`'s `[key: string]: any` and the mirror's `.passthrough()` — no editor completed + them, no page named them, and a wrong-typed value rode through unexamined — while the keys + the type DID declare for the same affordance (`cancelLabel` / `confirmLabel` / + `confirmVariant`) are read by nothing, so a document written strictly against the shipped + type rendered an empty footer. The renderer's own registered `inputs` and `defaultProps` were + already written in the read dialect; this change makes that single de-facto contract legible + instead of minting a second one (AGENTS.md #0.1: one strict contract, not N dialects). + + **Accept-set change on the published zod mirror — breaking, shipped as `minor` per this + repo's version-alignment policy (majors track `@objectstack`).** Declared keys are validated + even under `.passthrough()`, so three documents that parsed green yesterday are refused + today, each at its own path: `cancelText` or `actionText` carrying a non-string + (`cancelText: 123` — the renderer drew it as button text), `content` carrying a value that is + not a node or node array (an object without `type`), and `onAction` carried at all (a JSON + author cannot write a function; a string or object there was accepted and forwarded to the + button, where it did nothing or threw at click). A document in the read dialect with + well-typed values parses exactly as before and its values now survive the parse typed. + Undeclared keys still pass through unchanged. On the TypeScript face, `cancelText: 123` is + now a compile error at the key where the index signature used to absorb it. + + **No renderer change; no runtime behaviour changes.** The `alert-dialog` renderer, its + `inputs` and its `defaultProps` are untouched. The three declared-but-unread keys are + deliberately NOT retired here — that is a narrowing with its own card and its own grade; + their per-key liveness readings are on objectui#7104. + + Docs: `content/docs/components/overlay/alert-dialog.mdx` now publishes the read dialect in + its Schema block and no longer lists `actions?: BaseSchema[]`, a key no surface ever carried. + The four schema-catalog examples that page embeds still author `actions` and render an empty + footer — filed as objectui#7693, not converted here (the conversion is lossy). +- 5f78953: `ChartSchema` declares the data model it renders — chart-level `data` and `xAxisKey`, with + the bare-string `xAxis` folded onto the latter — and `ChartDataSeries` accepts both binding + dialects (objectui#7113 option B, 项目总监席 总监批 #28 2026-09-01 「同意」; and + objectui#6939's `chart` row, maintainer ruling 2026-09-02 「同意」 — both rulings + independently instructed declaring these two keys, so they land as one change). + + ⚠️ Shipped as `minor`, not `patch`, because two document classes that validated before now + REFUSE. objectui#6939 grades this class "patch where the accept set only widens toward what + already renders"; this change is not a pure widening, so it takes the level objectui#6896 + set for the same transition in this same file — the mirror starting to refuse — and for the + same reason: this repository's `major` is a cross-repo pin to `@objectstack`'s major rather + than a severity dial, so the break is announced here, which is the channel that carries it. + + ## What now refuses (the narrowing, named) + + **Three** classes validated before and refuse now. The first two survived only on + `BaseSchema`'s `.passthrough()`; the third was silently STRIPPED by the non-strict + `ChartDataSeriesSchema` object. + + ```jsonc + // 1. chart-level `data` that is not an array of row objects + { "type": "chart", "chartType": "bar", "data": "oops" } // now: [data] expected array + { "type": "chart", "chartType": "bar", "data": [1,2,3] } // now: [data.0] expected object + + // 2. a non-string `xAxisKey` + { "type": "chart", "chartType": "bar", "xAxisKey": 123 } // now: [xAxisKey] expected string + + // 3. a non-string `series[].dataKey` ⚠️ THIS ONE DRAWS A REAL CHART TODAY + { "type": "chart", "chartType": "bar", + "series": [{ "name": "a", "dataKey": 123 }] } // now: [series.0.dataKey] expected string + ``` + + ⚠️ **Class 3 is the sharp one and is called out separately.** Classes 1 and 2 are malformed + documents whose chart was already broken. Class 3 is not: at base it parsed to + `series: [{ name: 'a' }]` (the non-string `dataKey` stripped in silence) and + `normalizeChartSchema` renders it — `str(123)` is `undefined`, so the read falls back to + `name` and yields `series: [{ dataKey: 'a' }]` (`normalizeChartSchema.ts:239`). So this is a + narrowing away from a document that **renders today**, which is precisely the distinction + objectui#6939's grading language turns on. `dataKey: null` behaves identically. Measured on + both states; the declaration itself is right, and this note is the disclosure it was owed. + + ## Corrected: what class 2 actually did + + An earlier draft of this changeset said `xAxisKey: 123` "drew an EMPTY CHART". The read + sites do not support that: `ChartRenderer.tsx:133` takes `schema.xAxisKey` raw and the rows + still reach `data` at `:164`, while the normaliser drops the key (`str(123)` is `undefined`). + Measured through `normalizeChartSchema`, the result keeps the series and loses only the + category binding — **a drawn chart with a broken category axis**, not an empty one. Class 1 + (`data` malformed) is the one that leaves nothing to plot. + + ## Also changed on the published surface: combinators + + Both consts now carry a check (`ChartSchema` the `xAxis` fold, `ChartDataSeriesSchema` the + at-least-one-binding refinement), and on zod 4.4.3 that makes three combinators **throw** + where they previously returned a schema: + + ``` + ChartSchema.pick(…) / .omit(…) / .partial() -> throws "cannot be used on object + ChartDataSeriesSchema.pick(…) / .omit(…) / … schemas containing refinements" + ``` + + `.extend()` with a NEW key still works and preserves the fold and the refinement; + `.optional()`, `z.discriminatedUnion`, `z.toJSONSchema` and `safeValidateSchema` are all + unaffected. Nothing in this repository calls the throwing combinators on either const, and + the published surface already ships refined mirrors (`objectql.zod.ts`, `complex.zod.ts`, + `form.zod.ts`, `app.zod.ts`), so the class is not new — but it is a real behaviour change on + a published export and it belongs in the release note rather than in a reviewer's file. + + ## What now validates (the widening) + + `series: [{ dataKey: 'revenue' }]`. `normalizeSeries` reads + `str(raw.dataKey) ?? str(raw.name)`, so `dataKey` alone has always been a complete binding + — but the mirror REQUIRED `name` and refused it. That is why both catalog chart fixtures + (`advanced-line-chart.json`, `area-chart.json`) failed validation: they are the `chart: 2` + entry in `objectui check`'s 28-file census. `name` is now optional, `dataKey` is declared, + and a series binding to NEITHER is refused by name at `series.N.name` — the same path the + required flag used to report, so the diagnostic did not move. + + ## `xAxis` folds; it does not become a second name + + `xAxis: 'month'` is accepted at input and is ABSENT from the output, having landed on + `xAxisKey`. When both are written the canonical key is kept and the alias dropped — not a + precedence rule minted here, but the one already running at `normalizeChartSchema.ts:292`, + where `xAxisKey` is the first limb of `str(schema.xAxisKey) ?? xAxisSpec?.field ?? + str(xAxisRaw)`. No chart that renders today changes what it renders. + + ⚠️ The `xAxis` **config object** (`{ field, format, title, showGridLines }`) is NOT folded. + Only the bare string is a sibling spelling of `xAxisKey`; the object's presentation keys + survive separately into `out.xAxis` (`normalizeChartSchema.ts:289-291`), and folding it + would discard them. + + ## Not done, deliberately + + objectui#6939's `chart` row also says "`series[].data` stops being required". On this base + it already is not: objectui#6896 replaced it with `retirementTombstone(...)` — + `z.never({ error }).optional()` — which is optional AND refuses any authored value by name. + Implementing the clause literally would re-widen a retired key and reverse a landed ruling, + so it is not done. + + ## FROM → TO + + ```ts + // ChartDataSeries + - name: string; + + name?: string; + + dataKey?: string; + + // ChartSchema + + data?: Array<Record<string, any>>; + + xAxisKey?: string; + ``` +- 639114c: Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122). + + ⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.** + The exported `ObjectSchemaClientExtensions` narrows from + + ```ts + export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' } + ``` + + to + + ```ts + export type ObjectSchemaClientExtensions = Record<never, never>; + ``` + + Two breaking consequences for a consumer that names the type directly. **(1)** It + no longer declares `editMode`; the key is now carried by the spec's + `ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against + the extension type ALONE loses it. **(2)** `interface` → type alias also ends + **declaration merging**: a consumer that reopened + `declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }` + to add its own client-side member no longer compiles, because an alias cannot be + reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule — + objectui's own breaking changes are graded `minor` with the semantics stated in + the body, since any `major` in the fixed group would push all 39 packages off + `@objectstack`'s major. + + **`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured: + the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty, + declared as the same `'page' | 'modal'` union objectui carried). Its local copy + is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin + prescribed for this event, leaving the client delta empty. Nothing is removed + from the product: `editMode` stays authorable and stays typed on + `ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a + local member — and a published, spec-validated object document may now carry it, + which at 17.2.0 was refused by name. + + **`user:profile` is retired across all three sites.** 17.3.0 dropped it from + `PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly + `['user:profile', 'element:form']`, gained set empty). objectui went on knowing + it in three places, so all three moved together: the Studio palette exclusion + ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the + regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing + user-reachable went with it — neither type had a renderer, `user:profile` had + only the dashed "Component Placeholder" scaffold, and the app shell's own + profile affordance is a React slot, never this block type. A page schema still + naming it now draws the loud "Unknown component type" panel rather than a silent + grey box, which is this repo's standing treatment for a type outside the + supported surface. + + **`record:details` sections document the eight keys 17.3.0 added.** + `group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`, + `description` and `headerColor` are now declared on a section entry (4 → 12 + members). Six of the eight are already honoured by `DetailSection`, so the + `sections` input description now teaches all of them, and says plainly which two + are not read here. Designer controls for them are a separate feature and are + deliberately not added. + + **`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` → + `^17.3.0`, and this is the second half of its `minor`.** The package's emitted + `dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which + `17.0.0` does not export, so the old range was a claim the artifact did not + support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]` + and names `^17.3.0` as the lowest version carrying every symbol the package + references. Breaking for a consumer pinned below 17.3.0: it can no longer + resolve this package. That is the range stating the truth rather than a new + restriction — the artifact already required those symbols — and it is the + remedy the gate itself prescribes ("Raise that package's range to the lowest + version that exports the symbol… Do not add a tolerant re-declaration on this + side: the range is the claim, and the claim is what is wrong", objectui#5793). + `@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0` + and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is + this repo's normal state, not an exception. + + ⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing + and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main + workflow that cannot red a pull request, and `main` is green on it — the last + eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1 + with CI's own `--cross-check` invocation, and exits 0 with this raise, judging + 278 (subpath, symbol) pairs across 19 published packages either way. Its blocking + copy runs on the publish path, so leaving it would have surfaced as a cancelled + release rather than as a red check. The correction is recorded on objectui#7688. +- 1f31d3a: **Retired: `DetailViewSection.hideEmpty`.** The `record:details` section key is + gone from `@object-ui/types` and `RecordDetailsRenderer` no longer reads it. + Emptiness on a detail section is now decided entirely by `DetailSection`'s + auto-hide heuristic — hide empty rows only while the section still has at least + one filled row, never on an all-empty section — with the reader's + "Show N empty fields" toggle as the escape hatch. + + **Minor, not major, and deliberately so.** The key was never authorable on any + validated page: `@objectstack/spec` `RecordDetailsProps` REFUSES it, returning + `unrecognized_keys: ['hideEmpty']` on the `sections[]` element (measured on the + installed 17.2.0, against a `columns: 2` control that parses and whose value + survives). So a spec-compliant document could not carry the key, and a document + that carried it anyway failed to parse before it ever reached the renderer. + What this release removes is a *declaration* that invited authors — and code + generators reading the published `.d.ts` — to write a key the platform refuses. + That narrows a published type surface, which is what makes it a minor rather + than a patch; it retires no capability anyone could exercise. + + One key had four contracts and three answers: `@object-ui/types` declared it, + `RecordDetailsRenderer` honoured it, the `DetailViewSectionSchema` zod mirror + omitted it, and the spec refused it. The maintainer converged the four on the + spec's answer (2026-09-01): the spec keeps refusing, the mirror stays absent, + and the declaration and the read are retired. All four are now pinned together + in `record-details.hideEmptyRetired-7129.test.tsx`. + + Going with it is the paradox the key carried: `DetailSection` tested + `!section.hideEmpty`, so an authored `hideEmpty: false` was indistinguishable + from an unauthored section and overrode nothing. There is no longer a lever to + misread. + + **Supersedes one paragraph of the `record:details` empty-section changeset in + this same release.** Its closing "What does not change: an authored `hideEmpty` + keeps its exact former meaning" no longer holds — an authored `hideEmpty` of + either polarity is now inert, and the release notes should read that way. + Everything else in it stands: the unauthored default is unchanged, and so is + the label-graveyard guard. + + **Migration:** delete `hideEmpty` from any `record:details` section you author. + A section that used `hideEmpty: true` to hide an all-empty block will now show + that block's skeleton — headings, field labels and one empty-value placeholder + each. That is the platform's answer for a sparse record, and it is a UI + decision, not something metadata should have to make. + + **Not affected**, despite the shared name: `record:reference_rail`'s own + `hideEmpty` prop, which is a different surface and still live; and the + `detail.hideEmptyFields` i18n label behind the toggle. +- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one + spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). + + **BREAKING, deliberately, with no deprecation window.** + + Two published contracts declared OPPOSITE dialects for the same four lookup keys, and + `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object + contract *refuses* outranked the one it *declares*: + + | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) | + |---|---|---| + | camelCase | **declared** | compile error (`TS2561`) | + | snake_case | refused (`unrecognized_keys`) | **declared** | + + `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the + snake members are **removed**: + + | before (removed) | after | + |---|---| + | `display_field` | `displayField` | + | `description_field` | `descriptionField` | + | `lookup_filters` | `lookupFilters` | + | `id_field` | `idField` | + + **Migration.** Rename those four keys wherever you author lookup or user field metadata + — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema` + that returns them. The old spellings are no longer read: a def still carrying + `display_field` falls back to the referenced record's generic name heuristic rather than + the field you named. + + `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name + hydration — committing a record field other than the id as the lookup's stored value — + which is picker behaviour with no `FieldSchema` twin, and none owed. + + **Not renamed** (outside this ruling's four keys, still snake on the widget bag): + `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`, + `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's + `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def. + + Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose + snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the + inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s + `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict + class; its gate is restated to assert the class no longer exists rather than passing + vacuously. +- 20c04b2: A gantt timeline whose rows are malformed now refuses to draw, naming the row, + instead of crashing the render (objectui#7164, maintainer ruling A+). + + `TimelineRenderer`'s gantt branch used to read the authored rows twice — once + defensively in `findUnusableGanttDate`, once bare in `calculateDateRange` — and + every input in the gap threw a `TypeError` mid-render from ordinary JSON: + `items: [null]`, a row whose `items` is `5` / `true` / `{}` / an array-like + object, or `items` itself not an array. The three readers (the date scan, the + range computation and the render loop) now consume ONE verdict from + `classifyGanttRows`, and a malformed shape renders the existing `role="alert"` + refusal through a new diagnostic key, + `timeline.gantt.unusableRange.malformedRow` — "items[0] is null, which is not + a row shape" — never the `malformedDate` copy, which named the wrong fault. + The key lands in `en` and the nine sibling locale packs. + + `@object-ui/types` (minor — the accept set narrows): `TimelineSchema.items` no + longer declares `z.array(z.any())`. Every element must be an object, and a + gantt row's own `items`, when present, must be an array, so `validate` refuses + `items: [null]` and `items: [{ items: 5 }]` at authoring time — before they + reach a renderer. Feed items (`vertical` / `horizontal`) carry no `items` key + and parse exactly as before; every in-repo `type: 'timeline'` fixture parses + green on both sides of the change. Rows with no bars (`items: []`, a row + without `items`) stay the ordinary empty state and still draw. +- b652514: Mixed id/object action arrays are refused; use all ids or all objects + (objectui#7182, maintainer ruling 2026-09-02, option C). + + An `actions` array on `page:header` or `record:quick_actions` (and the bar's + spec-declared `actionNames`) is either **all action ids** or **all inline + `ActionDef` objects**. A mixed `['convert', { … }]` array is now refused on both + surfaces: none of its authored actions is rendered, and the console names the + offending index (`… refused at index 1 — element 1 is an inline action object + but element 0 is an action id …`). Before this change the two renderers + disagreed on exactly that input — `page:header` normalised per element and drew + both halves, `record:quick_actions` switched on the whole array and rendered + nothing for the id — so one authored array meant two things depending on which + surface drew it, and the mixed form is precisely what a half-migrated page under + the objectstack#11592 ids ruling produces. + + **Breaking, deliberately, and narrowing.** `@objectstack/spec` has always + declared `PageHeaderProps.actions` as `z.array(z.string())` — the spec already + refuses an object element at validation, naming its index — and + `RecordQuickActionsProps` declares `actionNames` (ids) only. What narrows is + the renderers' undeclared tolerance: an all-object array still passes through + (transition tolerance for the migration, retired on its own card once the last + inline array is converted), a mixed one no longer does. **Migration:** convert + each array whole — every element an id naming an action declared on the + object — never one element at a time. + + New on `@object-ui/types`, beside `actionRendersAt`: the pure + `resolveDeclaredActionIds(elements, registeredActions)`, with the + `DeclaredActionsResolution` / `DeclaredActionsRefusal` result types (the shape + classifier stays module-internal: called with no registry, the function already + returns the registry-independent verdict a renderer needs before its lookup). Both + renderers call it; the whole-array switch in `record-quick-actions.tsx` and the + per-element normalisation in `containers.tsx` are gone. The rule is closed: a + string is an id, a non-null non-array object is an inline definition, and any + other element (`null`, a number, a nested array) is refused at its index too. + An all-id array resolves by `name` in authored order, first registration + winning on a duplicate name; ids that name nothing are reported back with their + index for the caller to warn about once its lookup has settled. + + **Three further behaviour changes ride on the one rule, all on published + packages:** on `page:header`, a padded id (`' convert '`) no longer resolves — it + was previously trimmed before the lookup, and ids are now compared exactly as + authored; on `page:header`, a blank `''` id is now reported by the unresolved-id + warning instead of being silently skipped; on `record:quick_actions`, an all-id + `actions` array with no object bound now renders nothing (the ordinary empty + placeholder) instead of handing the bare strings to the action engine as action + definitions. +- adbda1b: feat(types): `renderCellEditor`'s context gains `pendingRow` — the persisted row merged with its staged, unsaved edits (#7188) + + The inline cell editor's context on `DataTableSchema` (declared by #6882) carried the row + once, as `row` — the **persisted** record. A widget that scopes itself by a sibling field + (a `dependsOn` lookup) had no way to see a parent that was edited in the same row but not + yet saved, so it kept listing candidates for the old parent. + + The context now carries the row twice, deliberately: + + - `row` — unchanged: the persisted record, what the data source last returned. + - `pendingRow` — **new**: `row` shallow-merged with the row's staged, unsaved edits. The + same object as `row` when nothing is staged. + + `row` was **not** redefined to mean the merged record — that would silently change an + already-published member, and a host that needs the persisted value would have lost its + only source. Both are addressable. The zod mirror's `renderCellEditor` is `z.function()` + and encodes no parameter shape; its description records the delta and names the member + on `DataTableSchema` as the authority. The #6882 exact-shape pin was extended (not + weakened) in the same change, with a control proving the pin can tell the seventh member's + presence from its absence. +- 2e32ed4: `ObjectFormSection` no longer declares `className` / `gridClassName` (objectui#7200 — + the declared-but-inert remainder of objectstack#13626). + + **Breaking, deliberately.** A TypeScript literal annotated `ObjectFormSection` (or an + `ObjectFormSchema.sections` entry) that carries `className` or `gridClassName` is now a + compile error at the authoring site. Before this change the two members were declared + with doc comments promising a wrapper / grid class, while — since objectstack#13626 + retired the seven renderer reads (`@object-ui/plugin-form` 2026-09-01) — nothing + delivered it: an author could write either key, have it type-check, and get nothing. + + The authored-metadata type now agrees with `@objectstack/spec`, whose `FormSectionSchema` + is a strict object declaring neither key, and with the ruling's rationale (maintainer + 2026-09-01, verbatim): "retire the reads … Declaring the keys was weighed and not adopted: + it would formally invite free Tailwind strings into authored metadata, the exact class + the boundary exists to keep out." A `?: never` tombstone was not used: `ObjectFormSection` + has no zod mirror (`ObjectFormSchema` in `zod/objectql.zod.ts` does not declare + `sections`), so there is no parse door to refuse at, and a tombstone is still a + declaration in completion and in the published `.d.ts`. + + **Not changed.** The five per-layout section config types in `@object-ui/plugin-form` + (`ModalFormSectionConfig`, `SplitFormSectionConfig`, TabbedForm's `FormSectionConfig`, + `WizardStepConfig`, `DrawerFormSectionConfig`) keep their `className` / `gridClassName`: + their renderers read them for programmatic React mounts, which the authorable boundary + does not govern. The form ROOT `className` (`ObjectFormSchema.className`) is a different + key on a different node and is unaffected. Runtime behaviour is unchanged — JSON metadata + carrying either key was already ignored. + + **Migration.** Remove the two keys from any `ObjectFormSection` literal; they did nothing. + Style sections through the host application's own CSS or the form ROOT `className`. + Section *layout* stays authorable through `columns`. +- 554f2b6: `ObjectCalendarSchema` declares the record-source ladder its renderer already + reads — `data`, `staticData`, `objectName` — on both faces, in the shape + objectui#6939 landed on `object-map` and `object-gantt` (objectui#7313). + + `ObjectCalendar` resolves its records through the shared ladder + (`resolveRecordSourceConfig` in `@object-ui/core`, called from + `plugin-calendar/src/ObjectCalendar.tsx`): `data` first, then `staticData`, + then `objectName`. The published TypeScript interface REQUIRED `objectName` and + declared neither `data` nor `staticData`; the published Zod mirror did the same. + So an `object-calendar` node authored on `staticData` — the route the plugin + page documents twice — rendered correctly and was refused by + `safeValidateSchema`, and could not be annotated with its own type + (`TS2741: Property 'objectName' is missing`). + + - `objectName` becomes optional on the TypeScript interface and on the mirror + in the same stroke; it stays the object-provider key. + - `data` (`ViewData` / `ViewDataSchema`) and `staticData` (`any[]`) are + declared on both faces, spelled exactly as `ObjectGanttSchema` spells them. + - The member ends in `requireRecordSource('object-calendar')`: a node + authoring NONE of the three is refused by name — one root-level issue, + `params.code = 'RECORD_SOURCE_REQUIRED'`, the map/gantt message naming + `data`, `staticData` and `objectName` — instead of by a missing `objectName`. + + **A widening.** A node authoring `staticData` or `data` without `objectName` + now validates (it always rendered — the read is + `resolveRecordSourceConfig(schema)`, keyed on the three). Every document that + validated before still validates: `objectName` alone still parses, an empty + one included, because presence is `!== undefined`. The one shape the + refinement refuses (none of the three) was refused before too, at + `objectName`. The two static-data examples in + `content/docs/plugins/plugin-calendar.mdx` are now annotated + `ObjectCalendarSchema` and compile under the doc-snippet gate. +- 669d71b: **Breaking for authored metadata:** `ObjectKanbanSchema.groupField` is RETIRED + (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the `object-kanban` + renderer actually reads — `groupBy` and `limit` — are now DECLARED and validated + on both published faces: the TypeScript interface in `objectql.ts` and the Zod + mirror in `zod/objectql.zod.ts`. + + **What was measured, on this branch's base (`53ded82b`).** + `packages/plugin-kanban/src/ObjectKanban.tsx` reads `schema.groupBy` at thirteen + sites (lane materialisation at `:601` / `:625` / `:640`, card moves at `:747` / + `:865`, and their effect deps) and `schema.limit` at two (`:264`, + `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`, and the effect deps at `:291`). + `groupField` has ZERO read sites anywhere under `packages/plugin-kanban/` — + against a control of those thirteen `groupBy` reads in the same query, so the + zero is a reading, not a blind grep. Yet the declaration REQUIRED `groupField` + and declared neither `groupBy` nor `limit`. Measured from source: the + documented, tested, working shape — `{ type: 'object-kanban', objectName, + groupBy, limit }` — failed `ObjectKanbanSchema.safeParse` and + `safeValidateSchema` on the missing `groupField`, and only ever reached the + renderer through `BaseSchema`'s `[key: string]: any` and `.passthrough()`, + admitted unexamined. An author who followed the declaration wrote `groupField` + and got a board that grouped nothing, with no diagnostic on either face. + + **Who is affected — an `object-kanban` node authoring `groupField`:** + + ```json + { "type": "object-kanban", + "objectName": "task", + "groupField": "status" } // ← validated, compiled, grouped nothing + ``` + + now fails validation at `groupField` with: + + > RETIRED (objectui#7322) — `groupField` is not read by the object-kanban + > renderer; author `groupBy`. (The view-level `kanban.groupField` alias is + > unaffected.) + + and is refused at compile time: `groupField?: never`. The tombstone is + load-bearing rather than decorative — `BaseSchema` carries `[key: string]: any`, + so DELETING the member would let the retired spelling type-check green and go on + doing nothing. **Migration:** rename the key to `groupBy`; the value — the field + whose value places a record in a lane — is unchanged. + + **What now validates that did not before:** the documented shape. `groupBy` is + REQUIRED (the retired contract required a lane field too; the renderer's + `if (!schema.groupBy)` branches are defensive early-returns, not a lane-less + mode, and every documented and tested `object-kanban` node authors the key) and + must be a string; `limit` is an optional positive integer. A wrong-typed + `groupBy: 42` or `limit: "twenty"` — previously admitted unexamined — is now + refused at its own path. + + **Who is NOT affected.** The VIEW-LEVEL kanban config is untouched: + `kanban.groupField` there is a live legacy alias of the spec's `groupByField` + (`packages/core/src/utils/normalize-list-view.ts` maps it; `plugin-list`'s + `ListView` and `plugin-view`'s `ObjectView` still read it). `groupField` is dead + only on the `object-kanban` NODE. The declarative `kanban` node (`KanbanSchema`) + is untouched, `BaseSchema`'s unknown-key policy is byte-identical (an undeclared + key still passes through), and the renderer is unchanged — boards authored the + documented way rendered before and render now. One in-repo fixture authored + `groupField` on this node (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`); + it now authors `groupBy`. No doc snippet, catalog entry, skill or app in this + repository authored `groupField` on an `object-kanban` node. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the retired key. It is not `major` per this + repo's fixed-group convention (objectui's own breaking changes ship as `minor`; + the group's major tracks `@objectstack` — AGENTS.md 版本号策略, mechanically + enforced by `scripts/check-changeset-no-major.mjs`). +- ed27d7c: The eight `on*` handler keys PR #7339's census could not see now refuse by name + (objectui#7344 — the objectui#6182 ruling of 2026-08-25 that the handler-expression + string dialect is not a supported authoring form, executed in the objectui#6124 shape). + + **The accept set of published validators moves** (`@object-ui/types/zod`): + + - Four mirrors declared the string dialect (`z.string()`) — `AppActionSchema.onClick`, + `ReportBuilderSchema.onSave` / `.onCancel`, `DetailViewSchema.onBack` — so an authored + `onBack: 'goBack'` parsed green and then reached a slot that CALLS it + (`DetailView.handleBack`), throwing `onBack is not a function` at click. + - Three declared `z.any()` — `ActionSchema.onClick`, `DetailSchema.onBack`, + `CRUDDialogSchema.onClose` — wider than the callable the TypeScript face declares, so + any JSON value parsed green (the objectui#7069 direction). + - One, `CalendarViewSchema.onEventClick`, was `z.function()` in a multi-line spelling the + anchored census missed. + + All eight now carry `handlerKeyRefusal(key, disposition, label)`: an authored string, an + authored object and a live function are each refused at the key's own path with + `code: 'custom'` and a message that names the key, says why JSON cannot author it and + points at the node-type spelling. Nothing that used to be refused parses green. + + **The TypeScript face, measured per key** — a function type only where a runtime consumer + reads a function, else `?: never`: + + - Runtime slots (callable kept): `DetailViewSchema.onBack` (now `() => void`, the prop + `DetailView` invokes — it declared `string`), `DetailSchema.onBack`, `ActionSchema.onClick`, + `CalendarViewSchema.onEventClick`. + - Retired (`?: never`): `AppAction.onClick` (no reader touches `onClick`, on the action + or on `items[]` — `AppComponentSchema.actions[]` itself IS read, by + `@object-ui/runner`'s `LayoutRenderer`, so the array being unread was never the + reason this key is inert; corrected at objectui#7721), + `ReportBuilderSchema.onSave` / `.onCancel` (no `report-builder` renderer is registered), + `CRUDDialogSchema.onClose` (no `crud-dialog` renderer is registered). + + The three `views.zod.ts` `z.string()` keys that are event NAMES (`onViewChange`, the two + `onChange`, PR #6899) are untouched; their describe text says so and the new pin reads it. + `zod-mirror-parity.test.ts` records the three new runtime-slot drift rows; + `content/docs/core/app-schema.mdx` spells its `AppAction.onClick` row `never` and drops the + string example, the two edits the #7340 docs pin and `check:doc-snippets` require. +- 52c8cf7: `DrillDownConfigSchema` is the zod mirror of `DrillDownConfig`, and both + declarations that carry `drillDown` reference it — `ChartSchema` + (`zod/data-display.zod.ts`) and `ObjectDataTableSchema` (`zod/objectql.zod.ts`) + — so the published validator under `@object-ui/types/zod` reads the key for the + first time (objectui#7352). + + `DrillDownConfig` has been declared on the TypeScript face since objectui#6058 + seeded the parity ledger, and objectui#6576 declared it on a second type. No zod + mirror existed, so under `BaseSchema`'s `.passthrough()` a + `drillDown: { enabled: 'yes' }` parsed green and rode through to a widget that + reads `enabled` as truthy — `declared !== enforced` on a published surface. + + Accept-set change on the published validator, stated plainly: + + - NARROWS: a `drillDown` whose declared key holds a value outside its declared + type (`enabled: 'yes'`, `mode: 'jump'`, `maxRows: '50'`, `report: 'pipeline'`) + is now refused BY NAME on a `chart` or `object-data-table` node, where it + previously rode through untouched. + - Unchanged: every value the TypeScript declares still validates, including the + `{ enabled: true }` / `{ enabled: true, mode: 'record' }` blocks the dashboard + renderer synthesises, `report`'s two structural forms, and an inline report's + extra keys (the declaration's index signature is `.catchall(z.unknown())`). + - Output shape, worth knowing before you read a parsed `drillDown.report`: the + member is a union, and its two arms differ in what they KEEP. The inline arm + carries the declaration's index signature as `.catchall(z.unknown())`, so extra + report keys survive; the named-reference arm is a plain object, so a value that + reaches it keeps only `name` (`{ name: 'x', columns: [] }` is accepted, and + parses to `{ name: 'x' }`). Both were accepted and unvalidated before, and + neither is refused now. + - Unchanged: `PivotTableSchema.drillDown` has no zod mirror at all, and + `DataTableSchema` declares the key on neither face — both are untouched here. + - New export on `@object-ui/types/zod`: `DrillDownConfigSchema`. + + `DrillDownConfigSchema` is deliberately NOT `@objectstack/spec/ui`'s + `ChartDrillDownSchema`: that object models the chart-only subset strictly and + refuses `mode` and `report` by name, both of which are live keys on the table / + pivot / metric widgets that share `DrillDownConfig`. +- 52c8cf7: `ObjectGallerySchema` and `ObjectDataTableSchema` are members of + `ObjectQLComponentSchema` on both faces — the TS union in `objectql.ts` and the + zod union in `zod/objectql.zod.ts` — so `AnyComponentSchema`, and with it + `validateSchema` / `safeValidateSchema` / `objectui validate`, has an arm for + `object-gallery` and `object-data-table` nodes (objectui#7363). + + PR #7355 (objectui#6576) minted both schemas beside the other `Object*Schema` + members and deliberately left the unions alone. Until now a document carrying + either node was refused as matching NO arm — exactly as before the schemas + existed — and a wrong-typed declared key on it could never be diagnosed by name. + + Accept-set change on the published validator, both directions, stated plainly: + + - WIDENS: a well-formed `object-gallery` / `object-data-table` node now + validates instead of being refused for having no arm. + - NARROWS in effect: a malformed one (`searchable: 'yes'`, `imageField: 42`, + `onRowClick` authored as JSON) is now refused BY NAME by the arm, where it was + previously refused only as "no arm matches". + - TS face: `Extract< ObjectQLComponentSchema, { type: 'object-gallery' } >` + resolves to `ObjectGallerySchema` instead of `never`; likewise for + `object-data-table`. `SchemaByType` has no in-repo consumer, and the wider + `AnySchema` union already carried `BaseSchema`, so nothing narrows there. + + No renderer behaviour changes; both nodes rendered before and render the same. +- c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and + `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781; + maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove). + They are the three keys the manifest serializer does not forward, and nothing read them on any + publication or consumption path. + + No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s + serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`, + `type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three + never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type + has no slot for them; the registry's data-source seam reads `name` only; and neither the designer + nor the app-shell inspectors consult registry `inputs` at all. A structural census over every + `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type` + 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on + nearly every registration, read by nothing. + + FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on + the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an + undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162 + authored values in silence. The tombstone is what makes the refusal loud and by name. + + - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror. + Migration: delete the key. An input is identified by its `name` on every path that reaches it; + nothing ever rendered a label for it. + - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the + key. The renderer's own fallback read IS the default; tell the author about it in `description`, + which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class, + since nothing reads the value.) + - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key. + No designer surface ever hid an "advanced" input; there is nothing to write instead. + + The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one + is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema` + (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with + `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message + (one string, both channels). Pinned in + `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a + tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`. + + Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a + `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses + RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone + `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule + requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized + `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT` + drops its `label`. The patch entries on the other packages record exactly that: their registrations + stop authoring inert keys, with no runtime or published-manifest change. + + The nine test files that read `defaultValue` off a registration were re-pinned against the + renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the + declaration that went away; two assertions that only restated the shadow default were dropped with + the reason on the line. + + The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys + is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop + into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` / + `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has + ruled on that face; that it now has no reader either is recorded as objectui#7911. +- 51eb515: **Removes two published exports.** Retire the `MobileResponsiveConfig` and + `GestureConfig` types (objectui#7519, ADR-0049 enforce-or-remove). Both names + are deleted from `@object-ui/types` and from `@object-ui/mobile`, which + re-exported them — after this release `import type { MobileResponsiveConfig }` + or `import type { GestureConfig }` from either package is a compile error, not a + deprecation warning. + + Each had exactly one consumer: the `responsive` and `gestures` members of + `MobileComponentConfig`, which objectui#5942 retired. Re-measured on current + `main` before anything was deleted, each was a declaration plus the two barrel + re-exports and nothing else — no type mounted either, nothing extended, + annotated, cast to or imported them outside the barrels, and the example apps + and the `objectstack` sibling checkout had zero authors. A value written against + either could not reach a renderer or a handler by any path. That is the same + declared-surface-with-no-consumption-path shape as `MobileComponentConfig` + itself and `MobileOverrides` (objectui#4919) before it, one level down. + + Removed outright rather than kept as `?: never` tombstones, measured against + this package's two-prong discriminator (a tombstone steers authors to a named + live replacement key, or keeps loud a key the docs taught as working). Prong 1: + neither has a replacement key — the behaviour they named lives in hooks, and + `SpecGestureConfig` is a different contract, not a successor. Prong 2: the only + release-note lines naming either are the objectstack#4115 rename-ledger rows + and, for `GestureConfig`, the objectui#3363 reclaim note; none taught a + renderer or dispatcher reading them, and no member carried a published + `@default` (contrast `triggerIcon`, tombstoned by objectui#7654 on exactly that + evidence). Structurally there is also no silent-strip hazard for a tombstone to + guard: whole interfaces go, nothing ever parsed them, and the mobile module has + never had a `zod/` twin to host a `retirementTombstone()`. The compiler was the + only channel these names ever had, and the refusal now lives there. + + ## Upgrading + + **No behaviour changes and there is nothing to migrate at runtime.** An object + authored against either type did nothing before and does nothing now; what + changes is that the contract no longer claims otherwise, so the mistake surfaces + at authoring time instead of silently type-checking. + + - **You imported a type only** (the only thing that was possible — nothing + accepted either as a value): delete the import. If you kept a local object + annotated with it, drop the annotation; it was never passed anywhere that read + it. + - **You wanted per-breakpoint layout:** it exists and is not being retired — + `useResponsive` / `ResponsiveContainer` / `useBreakpoint` in + `@object-ui/mobile`. `ResponsiveValue` and `BreakpointName` stay exported from + both packages. + - **You wanted to bind a gesture to a handler:** `useGesture` in + `@object-ui/mobile` takes `{ type: GestureType, onGesture, threshold?, + longPressDuration?, enabled? }`. `GestureType` and `GestureContext` stay + exported from both packages. + - **You want a declarative mobile config surface:** that re-enters deliberately + as designed product surface on its own card, with the renderer that reads it + landing in the same change as the declaration — not by restoring these + declarations. + + **Do not follow the compiler's suggestion for `GestureConfig`.** Measured against + the built declarations: `import type { GestureConfig }` from either package now + fails as TS2724 with `Did you mean 'SpecGestureConfig'?`. That is a lexical + near-match, not a migration target. `SpecGestureConfig` is the retired + `@objectstack/spec` `ui/touch` **tuning** record (`{ type, label, enabled, + swipe, pinch, longPress }`) that `useSpecGesture` reads; it has no `action` + member and does not bind a gesture to anything. `MobileResponsiveConfig` fails + as a plain TS2305 with no suggestion from either package. + + Marked `minor`, not `major`, per this repo's version-alignment rule (AGENTS.md + 版本号策略), which reserves `major` for following `@objectstack` across a major — + the same classification objectui#5942 and objectui#4919 used for identically + breaking type removals. **Breaking for TypeScript consumers of the two names + only.** The in-repo consumer count is zero; consumers outside this repository + that import either name from either package are not visible from here, which is + why this entry is graded on the published-surface change and not on that count. +- c354ce5: **`BaseSchema.visible` / `.hidden` / `.disabled` now declare the CEL envelope object the renderer already evaluates, as one named wire type** (objectui#7530, maintainer ruling 2026-09-04, option A). + + Each of the three keys goes from `boolean | string` to `boolean | ExpressionWire` on both faces, where `ExpressionWire` is `string | { dialect?: string; source: string }` — the exact string-or-envelope union `FormField.visibleWhen` and its `*When` / `*On` siblings already carried, and the exact accept set of `@object-ui/core`'s `toPredicateInput` / `hasDeclaredPredicate`. The Zod mirror's `z.union([z.boolean(), z.string()])` becomes `z.union([z.boolean(), ExpressionWireSchema])` on all three. + + Two names are new on the published surface: + + - `ExpressionWire` (type, main entry) — the TypeScript wire union, in `packages/types/src/expression.ts`. + - `ExpressionWireSchema` (`@object-ui/types/zod`) — its runtime twin, hoisted out of `zod/form.zod.ts` (where it was module-private) into `zod/expression.zod.ts` and imported by both `base.zod.ts` and `form.zod.ts`. One envelope type, reused by reference; no second spelling. + + This is a **widening**, not a replacement, and the renderer's behaviour is untouched: `SchemaRenderer`'s `shouldHide` / `shouldDisable` chains already routed all three keys through core's one definition of "declared" and evaluated the value, and the envelope was already pinned as working on `hidden` and `disabled` — through a `Record` cast, because no key declared it. Measured before this change, `BaseSchema.safeParse({ type, hidden: { dialect: 'cel', source: 'true' } })` returned `success: false` (`invalid_union` at path `hidden`) while the identical envelope on `FormField.visibleWhen` parsed one file over; that is the gap this closes, on all three keys at once. Every boolean and string value keeps parsing and keeps type-checking unchanged. `dialect` is optional and unconstrained on the wire because the runtime reads it that way (only `'cel'` keeps its envelope on the canonical engine; anything else is unwrapped onto the legacy path). + + Not changed: `hasDeclaredPredicate` (no per-key branch — option B was rejected), the `*On` / `visibleWhen` sibling keys, and `ActionSchema.condition`, which already declared the envelope inline. + + Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). +- 8fe8e5c: `ChartDataSeriesSchema` (and its TS twin `ChartDataSeries`) declares the six series keys the + renderer reads — `label`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis` — which the + non-strict Zod object had been **stripping in silence** while `safeParse` reported success + (objectui#7546, `domain:ui` PM ruling: measure per key, declare what is live, report the rest). + + ⚠️ Shipped as `minor`, not `patch`. The declared value domains are the read's own, so the accept + set widens toward what already renders — but one document class that validated before now + **refuses**: a series carrying one of these keys with a value the renderer drops in silence + (`variant: 'bogus'`, `yAxis: 'top'`, `opacity: '0.4'` / `Infinity`, a non-string `stack` / + `dashArray`, a non-string non-map `label`) — and, separately, `variant: 'current'`, which the + renderer does NOT drop: the normalizer keeps that renderer-internal spelling and draws it exactly as + `primary`, but it is not a member of the published pair, so it now refuses at `variant` (below). + Such a document draws a chart today — the normalizer ignores the bad value, or honours `current` + — so this is a narrowing away from something that renders, which is the + distinction objectui#6939's grading language turns on, and it takes the level objectui#6896 and + objectui#7113 set for the same transition in this same file. This repository's `major` is a + cross-repo pin to `@objectstack`'s major, not a severity dial; the change is announced here. + + ## The defect, measured + + `ChartDataSeriesSchema` is a non-strict `z.object` — not `.passthrough()` like `BaseSchema` — so an + undeclared key is removed, not kept. Reproduced red on `origin/main` `a472b071` before the change: + + ``` + input : { name, label, stack, yAxis, opacity, dashArray, variant } + parse : success = true + output: { name } + ``` + + Every one of the six is read by `normalizeSeries` (`@object-ui/plugin-charts`, + `normalizeChartSchema.ts:242-255`) and does real work in `AdvancedChartImpl.tsx` — `label` names + the legend entry, `variant === 'comparison'` selects the muted overlay, `opacity` / `dashArray` set + stroke and fill, `stack` becomes Recharts' `stackId`, `yAxis` binds the secondary axis. Any consumer + of the parse output — `objectui check` / `objectui validate` via `safeValidateSchema`, a JSON + schema derived from the mirror, or any pipeline that keeps `parse()`'s result — lost them outright. + + ## Per-key liveness, not a blanket declare + + "The renderer reads it" was ruled insufficient (a read leg can sit on a value nothing produces — + objectui#7642), so each key was measured on producers, real work in the reader, and consumer + surprise, with a lit control on every count. The six are `@objectstack/spec`'s own + `ChartSeriesSchema` members under the same names and value domains; this node's `series` + accepts the spec shape by design; in-repo producers write them onto `type: 'chart'` nodes + (`DashboardRenderer`, `ObjectChart`, `DatasetWidget`, `core/utils/chart-presentation`); and + the `variant` / `yAxis` narrowings are design intent the reader already enforces. + + **`chartType` — the seventh key the review found — is deliberately NOT declared.** It is the first + limb of `str(raw.chartType) ?? str(raw.type)`, but it is the renderer's *internal* spelling of the + declared `type`; the spec's `ChartSeriesSchema` lists it as an alias of `type` and refuses it by + name; and zero documents, fixtures, catalog entries or designer inputs write it on this face + (controls lit). Declaring it would mint a second writable name for one override. It is reported + for its own card; the mirror still strips it, and the pin test holds that gap visible. + + ## FROM → TO + + ```ts + // ChartDataSeries — all optional, all additive on the TS face + + label?: string | I18nLabel; // spec I18nLabel: string | inline locale map + + variant?: 'primary' | 'comparison'; // the spec's own pair + + opacity?: number; // finite; NaN / Infinity / strings refused (the spec's 0–1 bound is not enforced — the read's domain) + + dashArray?: string; + + stack?: string; + + yAxis?: 'left' | 'right'; + ``` + + `variant` is the spec's own pair. The normalizer also tolerates a third spelling, `current`, but that + is the renderer's internal default — written only by the compare-to producers (`ObjectChart`, + `DatasetWidget`) onto internal-shape arrays that never pass this mirror, and by nothing an author + writes (docs, fixtures, designer inputs: 0, controls lit) — so it is not a member here: declaring it + would have fossilised a renderer-side tolerance into a second contract. The normalizer's tolerance + is unchanged; objectui#7682 owns that decision. + + ## Unchanged, deliberately + + The object stays non-strict — a truly undeclared key is still stripped, exactly as + `chart-inline-data-retired.test.ts` pins; this change declares what is read, it does not close + the object. The `data` tombstone (objectui#6896) and the at-least-one-binding refinement + (objectui#6939 / #7113) are untouched. No reader changed. + + Pinned in `packages/types/src/__tests__/chart-series-keys-7546.test.ts` — the card's fixture + surviving byte-for-byte, each key on the mirror's own `.shape`, each value domain refusing at its + own path, the TS face in lockstep, and the `chartType` gap. +- e62c44e: Re-home the breakpoint layout vocabulary and delete the two dead responsive + implementations (objectui#7580, maintainer ruling 2026-09-04, option A). + + **Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole + `ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`, + `BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the + stated ground that the four types "had no other authorable carrier". That ground is + measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component + whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by + `resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in + `@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and + when a renderer implements it" — is already met here, so the two types a renderer reads + are re-homed rather than retired. + + What survives, under the same names and the same members: + + - `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead + of re-exported from the spec. **No consumer change**: same name, same six members, same + export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved. + - `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`), + verbatim from the retired `$strict` schema: six optional column counts, no index + signature. `responsive-grid`'s `columns` input and its resolver are unchanged. + + What is removed: + + - `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had + no read point in the package; it was published only because the retired + `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order` + needed the type. With the schema gone there is no order vocabulary for it to be the type + of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes. + - `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and + `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with + `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` / + `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired + `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773). + - `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once + the two implementations above went, dropped rather than re-declared locally, the same + disposition the retired i18n names in that file already carry. + + No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`, + `ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and + `responsive-grid` itself — are untouched. + + **Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:` + objectui#7580: the retirement is merged upstream and unreleased, so this must land first. +- 5d0876c: **Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the + dashboard-root `title` member (objectui#7623). + + Its doc comment said "Dashboard title displayed in the header", and that stopped being + true one release earlier: objectui#7509 retired all five dashboard-root `title` read + arms under ADR-0049, leaving the key declared, documented as rendering, and inert. The + header text is the spec-canonical `label` on `BaseSchema`, resolved through + `pickLocalized`. + + What an author loses is the **type-level suggestion** only, and there is **no runtime + and no validation behaviour change**. `BaseSchema`'s index signature means an existing + `title:` line still compiles; objectui's Zod twin extends `.passthrough()` `BaseSchema`, + so it parsed a root `title` before this release and still does; and a document validated + against `@objectstack/spec`'s strict `DashboardSchema` was already refused there + (`unrecognized_keys: ['title']`) long before it reached a renderer. Nothing that rendered + stops rendering — the read arms were already gone. + + **Unlike `aria` (objectui#5830), no tombstone comes with this.** The spec refuses a root + `title` as an unrecognized key, not with a named removal message, so there is nothing to + inherit by reference and none was invented; the pin + (`packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts`) asserts the + key-set half only — `title` is out of the interface's declared members while `columns`, + `widgets` and `header` stay in. + + **Not affected, despite the shared name:** widget-level `DashboardWidget.title` — the + spec's `I18nLabel` on a different receiver, live, declared and read by + `DashboardRenderer`, `DashboardGridLayout` and `DashboardWithConfig`. The pin carries it + as an explicit control. + + **Migration:** delete a dashboard-root `title` from any authored dashboard and use + `label` (it accepts a string or a per-locale map). Widget titles are unchanged. +- bc640ec: `SchemaRegistry['kanban']` stops describing a component it cannot name + + `SchemaRegistry` documents itself as "the Single Source of Truth for component + type lookups". Its `'kanban'` entry named `DeclarativeKanbanSchema` — the + authoring/validation face declared in this package — while the renderer + registered for that key is `ObjectKanbanRenderer` in `@object-ui/plugin-kanban` + (`ComponentRegistry.register('kanban', …)`), which consumes that package's own + `KanbanSchema`. The two are distinct dialects, measured member by member: the + plugin face declares 19 members in its own body, the declarative face 7, and + they share three names — `type`, `columns` and `onCardMove`. `onCardMove` has + the same signature on both sides; `columns` is required + `DeclarativeKanbanColumn[]` on one and optional `KanbanColumn[]` on the other, + the two element types sharing five of their six members; the remaining 16 + plugin members and the declarative `draggable` / `onCardClick` have no + counterpart across the gap. For this one key the map's value did not describe + the component the key names. + + Pointing the entry at the honest type is not reachable from this layer, and + that was measured rather than assumed. Importing `@object-ui/plugin-kanban` + here is a phantom dependency — `check:phantom-deps` rejects it by file and + pair, type-only imports included — and declaring the dependency would close the + cycle `@object-ui/types` → `@object-ui/plugin-kanban` → `@object-ui/types`, + because this package is the zero-workspace-dependency bottom layer. + objectui#6172's ruling (2026-08-31) kept the plugin's bare names rather than + relocating that dialect down here — that is why this entry cannot name the + plugin's type at this commit. That half of the ruling has since been reversed: + objectui#7664's ruling (a) (2026-09-05) rewrites this package's `'kanban'` + arm to the plugin's shape, has `@object-ui/plugin-kanban` conform to it, and + retires the `DeclarativeKanban*` trio; `registry.ts` is on its execution list, + so this entry is scheduled to be re-pointed at the declared type. The value + below is the transitional state under that ruling, not the permanent one. + + So the entry now asserts only what this layer can prove, and what BOTH dialects + satisfy: `BaseSchema & { type: 'kanban' }`. + + **What this changes for consumers.** `keyof SchemaRegistry` — and therefore the + published `ComponentType` union — is unchanged; `'kanban'` is still a member, + pinned at compile time so it cannot be narrowed away silently. What changes is + the value side of this one key, in two ways, one loud and one silent: + + - **Breaking, loudly, for consumers who indexed `SchemaRegistry['kanban']` and + flowed the value into the declarative face.** Before this change + `SchemaRegistry['kanban']` *was* `DeclarativeKanbanSchema`, so + `const s: DeclarativeKanbanSchema = registryValue` compiled. It no longer + does: the entry lacks the required `columns` member, and that assignment is + a compile error (TS2741). Nothing in this repository performed such an + indexed access, so no in-repo code moves — the break is for consumers + outside it. + - **Silent, for member reads.** `BaseSchema` carries an index signature + (`[key: string]: any`), so reading the declarative face's own members — + `columns`, `draggable`, `onCardMove`, `onCardClick` — off the new entry is + not an error: the reads resolve through the index signature and type as + `any`. `SchemaRegistry['kanban']['columns']` was `DeclarativeKanbanColumn[]` + and is now `any`; nothing turns red, the read just stops being checked. + (Undeclared keys already read as `any` before, through the same signature; + what changes is those declared members.) + + Migration: name the face you actually mean. `DeclarativeKanbanSchema` is still + exported from `@object-ui/types` unchanged for the authoring face, and + `KanbanSchema` from `@object-ui/plugin-kanban` for the rendered one. + + This is a breaking change shipped as `minor`: this repository's + version-alignment rule keeps objectui's major pinned to `@objectstack`'s and + ships objectui's own breaking changes as `minor` with the break spelled out in + the changeset body, which is what the two bullets above are. +- 3e377c9: Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an + ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's + "Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed + (objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44). + + ⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.** + Ships as `minor` per the launch-window convention: objectui's `major` is a cross-repo pin + to `@objectstack`'s so that "same major means compatible" holds across the two repos + (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes ship as + `minor` with the break named where it lands — this entry is the channel that carries it. + + ## What was retired, and why + + The node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one + selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling + of that same choice, and no renderer has ever read it: `chatbot-floating` renders the + trigger and panel unconditionally, and `chatbot` never looked at the key, so + `displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a + `chatbot-floating` node changed nothing. It was nevertheless declared on both faces, + painted as a **Display Mode** control in the designer's property panel, and written as + `'floating'` into every node the designer created — two surfaces teaching a switch that + did not exist. + + Re-measured on this branch's base rather than inherited from the card: a whole-repo + `git grep` census over tracked files, build output excluded, returned the declarations, + the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and + two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same + pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was + not blind. + + FROM → TO: + + - `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an + ADR-0049 retirement tombstone whose comment points at `type` as the replacement. + - `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, + the same tombstone. objectui#7655 declared the key on the floating face with + `ChatbotSchema`'s own lines precisely so this retirement would find it on both faces; + leaving the copy typed would have kept the published face teaching the switch. + - `chatbot-floating` `inputs`: the **Display Mode** control is removed. + - `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into + designer-created nodes. + + A control is restated, never deleted into a vacuum (objectui#7070): the restatement of + the removed control is the tombstone's guidance plus this note. + + **Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema` + or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` — + `'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for + inline. **No JSON document needs editing** — see the next section. + + ## Stored documents: runtime validation of this key is unchanged — zero before, zero after + + `displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for + both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is + `.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node + the designer ever created) parses green before this change and parses green after it, + and the value is dropped at render time exactly as it always was. + + That is deliberate, and it is why this tombstone has **no `retirementTombstone()` + half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis + (objectui#6152), a different defect, and a parse outcome the ruling did not ask for. + `packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins' + shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if + objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the + mirror adds the `retirementTombstone()` half at that time, flipping the control rather + than deleting it. + + ## Why a tombstone and not a deletion — measured on this carrier + + `ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index + signature, and on such a carrier deleting an optional member is **silent in every value + shape**: the index signature defeats both excess-property checking and the weak-type + check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature + control carrier (`FloatingChatbotConfig`) lit in the same run: + + | route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | + |---|---|---|---| + | declared (before) | clean | `TS2322` | clean | + | deleted | clean | **clean** | clean | + | tombstoned (after) | `TS2322` | `TS2322` | `TS2322` | + + Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned, + **presence with any value** is a compile error — a channel deletion cannot produce on + this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not + louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678). + Prong 2 of that discriminator licenses the tombstone: the key was advertised in the + 3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the + presentation switch. The deleted row is pinned in the test file as a live control — an + undeclared key that rides both shapes with no directive — so the contrast cannot rot. + + ## Accept-set change, one line per face + + - **TypeScript.** A write of `displayMode` against either chatbot face used to compile + and now does not. + - **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document + carrying the key parses green before and after, and keeps the value. + - **Designer.** The **Display Mode** control disappears from the `chatbot-floating` + property panel, and newly created nodes no longer carry the key. + - **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating` + registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to + 8, so the manifest projected from them no longer lists the prop. Measured on both sides + of this change: `validateTree` on a stored `chatbot-floating` node carrying + `displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity + **`warning`**, message `` `<chatbot-floating> has no prop "displayMode"` `` — which is + what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs + the props interface `generateDts` derives from those same `inputs` drops from 20 members + to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those + generated intrinsics no longer type-checks the attribute. + + **This is author-time only: no stored document stops parsing and nothing at render + moves.** The value survives compilation — `compile()` returns a tree still carrying + `displayMode: 'floating'`, byte-for-byte the same keys before and after — and a + `warning` never blocks a page, because the page renderer filters the diagnostics to + `severity === 'error'` before deciding whether to fail. Two neighbouring instruments are + untouched and worth naming so the scope is not read wider than it is: `os validate` runs + `safeValidateSchema`, the Zod path, and is silent on this key before and after; and the + build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does + not contain `chatbot-floating` on either side of this change. +- a3eb5d0: Retire `FloatingChatbotConfig.triggerIcon` (objectui#7654, ADR-0049 enforce-or-remove). + + `triggerIcon` was declared `?: string` with `@default 'MessageCircle'` and read by nothing. + `FloatingChatbot` destructures six of the interface's seven keys — `position`, + `defaultOpen`, `panelWidth`, `panelHeight`, `title`, `triggerSize` — and never this one, + and `FloatingChatbotTrigger` takes no icon prop at all, so the advertised default never + rendered either. Re-measured on this branch's base rather than inherited from the card: a + whole-repo `git grep` census over tracked files, build output excluded, returns the + declaration and one historical CHANGELOG line and nothing else, while the same pass over + `triggerSize` — a key that IS read — returns ten sites across four files, so the instrument + was not blind. + + It is also absent from the `chatbot-floating` registration's `inputs` AND from its + `defaultProps` (`packages/plugin-chatbot/src/renderer.tsx`), both re-confirmed here. No + designer control ever offered it and no designer-created node carries it, so TypeScript was + the only way to reach the key. That is what makes this half of objectui#7654 an ordinary + retirement; the card's other key, `displayMode`, is seeded into `defaultProps` and is NOT + touched here. + + FROM → TO: `triggerIcon?: string` → **tombstoned**, `?: never` on the interface. The FAB + trigger renders a fixed icon and takes no icon prop; there is no authored spelling that + changes it. + + ## This tombstone has NO Zod half, deliberately + + Every other tombstone in this package pairs `?: never` with a `retirementTombstone()` + refusal on the Zod twin. There is no twin here to carry one: `FloatingChatbotConfig` has no + Zod mirror at all, and `floatingConfig` sits in the `UnmirroredDeclared` ledger + (`zod-mirror-parity.test.ts`, `complex.zod.ts#ChatbotSchema`). `BaseSchema` is + `.passthrough()`, so the whole `floatingConfig` object rides through unvalidated — before + this change and after it. Minting a mirror to host a refusal would be the + declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and + inert, or unmirrored and live, and fixing one says nothing about the other. This change + does not widen into it. + + **Accept-set change, stated plainly for reviewers:** on the TypeScript face, a write of + `FloatingChatbotConfig.triggerIcon` used to compile and now does not. On the runtime face, + nothing changes at all — the key parsed green before and parses green after. The refusal is + TYPE-LEVEL ONLY, which is narrower than this package's other tombstones and is the reason + this carries a contract-review label rather than being filed as an internal tidy-up. + + ## Why a tombstone and not a deletion, when the usual argument does not apply + + The usual case for `?: never` argues from the mirror: an undeclared key is silently + STRIPPED by a non-strict `z.object`, so deleting trades one silent no-op for another. With + no mirror, that argument is unavailable, so the route was measured on the `tsc` channel + alone instead: + + | route | fresh object literal | widened (non-fresh) value | + |---|---|---| + | deleted | `TS2353` excess-property error | **compiles CLEAN** | + | tombstoned | `TS2322` | `TS2322` | + + Excess-property checking only reaches a fresh literal, so deletion would have left the + widened path — `const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig = + raw;` — silently accepting a key nothing reads. The declared `never` makes the assignment + itself ill-typed, so freshness stops mattering. Both rows are pinned in + `packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts`, the "deleted" + row as a live control on a genuinely undeclared key rather than as prose, so the contrast + cannot rot. + + That file also pins the runtime half as a **tripwire**: it asserts that a node carrying + `floatingConfig.triggerIcon` still parses green. If objectui#6152 ever mints a + `FloatingChatbotConfigSchema`, it goes red — the intended signal that whoever lands the + mirror must add the `retirementTombstone()` half at the same time and flip the control + rather than delete it into a vacuum. +- 4ce14f1: One named, importable authoring-face type per `plugin-chatbot` registration: + `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` join `ChatbotSchema` + (objectui#7655, under the objectui#6169 / #6172 family ruling — every component + node has exactly one named, importable authoring-face type). + + `packages/plugin-chatbot` registers three components — `chatbot`, + `chatbot-enhanced`, `chatbot-floating` — and `@object-ui/types` published ONE + face for the family with `type` pinned to `'chatbot'`. An author annotating a + `chatbot-enhanced` or `chatbot-floating` node either dropped to untyped JSON or + annotated with `ChatbotSchema` and lied about `type`; the docs' floating example + had to be a `json` fence because no `tsx` fence could compile. The two + registrations' real key sets lived in anonymous `ChatbotSchema & { ... }` + intersections local to the renderer, referenceable by nothing outside that file. + + ## The shape, and why not the smaller diff + + One interface per registration, not `ChatbotSchema['type']` widened to the union + of the three keys. The union would give three nodes ONE type and re-open what + #6169 closed — a single interface declaring keys only some of its own `type` + values read — and this card exists because the family's declarations had already + drifted from its reads. Each face declares what ITS registration reads, censused + per key on the PR's base (one `schema.KEY` read per registration body in + `renderer.tsx`, lit by keys that are NOT shared: `processVisibility` 0 / 1 / 0, + `floatingConfig` 0 / 0 / 1), and the twenty keys all three read are picked off + `ChatbotSchema` by name (`ChatbotSharedKey`) so they stay one declaration: + + - **`ChatbotEnhancedSchema`** (`type: 'chatbot-enhanced'`): the shared twenty, + plus `maxHeight` and `processVisibility` (read here, not by the floating + panel), plus `enableMarkdown`, `enableFileUpload`, `surface` (`'card' | + 'plain'`, objectui#6687) and the `onClear` runtime slot — four keys + `ChatbotSchema` never declared. + - **`ChatbotFloatingSchema`** (`type: 'chatbot-floating'`): the shared twenty, + plus `enableMarkdown`, `enableFileUpload`, `onClear`, and the two keys it + declares alongside `ChatbotSchema` — `floatingConfig` (`FloatingChatbotConfig`) + and `displayMode`. No `maxHeight`, `processVisibility` or `surface`: the + floating registration has no named read for any of them. (Its trailing raw + props spread does carry authored keys into the panel today — `processVisibility`, + `surface` and `showAvatars` are live there, measured through the real host; + that accidental channel is tracked as objectui#7708, and this face neither + declares nor promises it.) + - Neither face declares `ChatbotSchema`'s six legacy members (`loading`, + `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown`, `height`) — no + registration reads them by name — and neither redeclares `disabled`, which + stays `BaseSchema`'s `boolean | string` (objectui#7087). + + **`ChatbotSchema` is unchanged.** It keeps `displayMode` and `floatingConfig` + (declarations verbatim), and the floating face declares the same two, so + `ChatbotSchema['displayMode']` and `ChatbotSchema['floatingConfig']` stay the + typed members they were — the objectui#7669 `triggerIcon` tombstone keeps its + reach on `chatbot` nodes, now pinned on the node. `floatingConfig`'s doc comment + is rewritten on both faces: the old text said it was "only used when + `displayMode` is `'floating'`", which was false — it is read by `chatbot-floating` + alone and forwarded to the panel. `displayMode` is RULED RETIRED — objectui#7654, + maintainer ruling B (2026-09-05): `?: never` tombstone, designer control and + `defaultProps` seed removed, in that card's own change. This change carries the + key untouched on both faces (still unmirrored, still read by nothing) so that PR + finds the member exactly as ruled, and a tripwire test pins that any value still + parses green until that PR flips it. + + **New published symbol:** `ChatbotSharedKey`, the string-literal union of the + twenty keys all three registrations read. It is exported from `complex.ts` + because an exported interface may not extend a `Pick` over a private name + (TS4022), so it is emitted into `dist/complex.d.ts` and is reachable through the + published `@object-ui/types/complex` subpath (it is not re-exported from the + package entry). It is a census, not an authoring face. + + ## Zod twins, in lockstep + + `@object-ui/types/zod` gains `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` + (and `ComplexSchema` routes the two new discriminants). Every declared key is an + arm except: the three runtime slots (`onError`, `onSend`, `onClear`), refused by + name per objectui#6124; and, on the floating twin only, `floatingConfig` (no + `FloatingChatbotConfig` mirror exists — minting one is objectui#6152's axis) and + `displayMode` (unmirrored on `ChatbotSchema`'s twin too; retired by ruling on + objectui#7654 and executed there). The twins mirror the API body params under the + key the renderer reads, `requestBody`, and inherit `body` as the children slot — + they do not copy `ChatbotSchema`'s `body` naming collision. + + **Accept-set change, stated plainly:** a `chatbot-enhanced` or `chatbot-floating` + document parsed through the family's only twin used to fail on `type`; through + its own twin it now parses, and the keys the twin declares are VALIDATED where + they rode through `.passthrough()` unexamined before (`surface: 'frameless'`, + `enableMarkdown: 'yes'` and `requestBody: 'x'` are refused). A `chatbot` node's + parse outcome is unchanged: `ChatbotSchema`'s twin did not move. + + ## `@object-ui/plugin-chatbot` + + The `chatbot-enhanced` and `chatbot-floating` registrations type `schema` as the + published faces and drop the anonymous intersections. One consequence: + `chatbot-floating` used to write `disabled={schema.disabled}` and then spread + `{...props}` AFTER it — and `SchemaRenderer` always includes `disabled: verdict + || undefined` in those props, so the raw read was overridden on every render. + With `disabled` honestly typed as `boolean | string` the raw union cannot be + forwarded into the panel's `boolean` prop, so the registration now names the + host verdict (`disabled: hostDisabled`) the way its two siblings have since + objectui#4431. No render outcome moves; the pin renders through the real host + both ways. + + This ships as `minor` for `@object-ui/types` because it widens the published + surface with two new node types, two new Zod twins and one new type alias; + `ChatbotSchema`'s own accept set does not move: objectui's major is pinned to `@objectstack`'s + (`scripts/check-changeset-no-major.mjs`), and objectui's own contract changes + ship as `minor` with the semantics spelled out — as above. +- 2af1fa7: **BREAKING** — the `'kanban'` validator arm now accepts the shape the registered + renderer reads, and the six `DeclarativeKanban*` exports retire (objectui#7664, + maintainer ruling (a), 2026-09-05). + + For an authored `type: 'kanban'` document two different types were + authoritative depending on who asked. `safeValidateSchema` — what the CLI's + `validate` / `check` commands apply — honoured `DeclarativeKanbanSchema` + (`columns` with `color`, `draggable`, cards with `labels` / `assignees` / + `priority`), while the renderer registered for the key, `ObjectKanbanRenderer` + in `@object-ui/plugin-kanban`, consumed that package's own `KanbanSchema` + (`objectName` / `groupBy` / `cardTitle` / `cardFields`, cards with `badges`). + The two were unrelated dialects, so a board could pass `objectui validate` and + render **empty**. The ruling: the plugin dialect is authoritative. + + **What changes on this package's published surface:** + + - **The `'kanban'` arm's accept set is replaced.** `ComplexSchema` → + `AnyComponentSchema` → `safeValidateSchema` now validate the plugin dialect, + declared here as `KanbanSchema` / `KanbanColumn` / `KanbanCard` / + `CardTemplate` / `ColumnWidthConfig` (TypeScript) and `KanbanSchema` / + `KanbanColumnSchema` / `KanbanCardSchema` / `CardTemplateSchema` / + `ColumnWidthConfigSchema` (`@object-ui/types/zod`). An `objectName` / + `groupBy` board passes. A static `columns[].cards[]` board passes — that + spelling is the same document in both dialects and always rendered. A board + in the retired dialect is **refused by name** at the keys that betray it: a + board-level `draggable` and a column `color` are `?: never` tombstones on the + TypeScript face and named refusal arms on the mirror, each message naming the + retired `DeclarativeKanbanSchema` shape and the spelling to write instead. + Both were measured inert (zero read sites in the plugin). The retired card + keys are deliberately *not* refused: a card is an open record + (`[key: string]: any`), and `priority` or `dueDate` are legitimate record + fields. + - **Every handler key the retired arm refused is still refused, and one more + joins them.** The successor arm carries all five `#6124` refusal arms under + the same `'kanban'` key — `onCardMove`, `onCardClick` and `onQuickAdd` as + RUNTIME SLOTS (callable on the TypeScript face, refused by name on the + mirror: `KanbanRenderer` forwards all three off `schema.*` in one block), + `onColumnAdd` and `onCardAdd` as `?: never` tombstones. `onQuickAdd` is the + one that is newly refused — the plugin dialect declared it, the retired + declarative face did not. ⚠️ `onCardClick` is the key this arm must never + drop rather than refuse: the plugin dialect it is modelled on never declared + the member (the renderer read it undeclared), and because `BaseSchema` is + `.passthrough()`, leaving it out does not refuse it — it stops being judged + and the value is kept. Measured on the built dist, `{ type: 'kanban', + columns: [], onCardClick: { action: 'toast' } }` is REFUSED, beside the same + document at `onCardMove` / `onQuickAdd` / `onColumnAdd` / `draggable`. + - **Six exports retire — the second step of the objectui#6172 rename.** + objectui#6172 (PR #7643, same release line) renamed this package's trio from + the bare names to `DeclarativeKanbanSchema` / `DeclarativeKanbanColumn` / + `DeclarativeKanbanCard` and the three Zod mirrors to `DeclarativeKanban*Schema` + so the bare names could belong to the renderer's dialect. objectui#6172's own + stop condition was "if the renamed copy has no retained value, escalate", and + the retained value it cited was precisely the validator arm. This ruling moves + that arm to the plugin dialect, so the renamed copies have no consumer left + and retire under ADR-0049 (enforce-or-remove): `DeclarativeKanbanSchema`, + `DeclarativeKanbanColumn`, `DeclarativeKanbanCard` from `@object-ui/types` and + `DeclarativeKanbanSchema`, `DeclarativeKanbanColumnSchema`, + `DeclarativeKanbanCardSchema` from `@object-ui/types/zod` are gone. + Importing any of them is a compile error (TS2305). + - **`SchemaRegistry['kanban']` is `KanbanSchema`.** objectui#7645 (PR #7662) + weakened the entry to `BaseSchema & { type: 'kanban' }` because this layer + could not name the plugin's type; it now names the declaration the plugin + itself imports. `keyof SchemaRegistry` — the published `ComponentType` union — + is unchanged. + - **The bare names return to this package with a different shape than they had + before objectui#6172.** `KanbanSchema` here is now the plugin dialect, not the + declarative one the pre-rename `KanbanSchema` was. A consumer that never + migrated off the old bare name and expected `columns` to be required, or + `draggable` to exist, gets a type error rather than a silent change. + - `KanbanConditionalFormattingRuleSchema` is newly exported from + `@object-ui/types/zod`: the rule union the `'object-kanban'` arm already + applied, now shared with the `'kanban'` arm. + + **Migration.** Author boards in the plugin dialect — `objectName` + `groupBy` + for an object-bound board, or `columns[].cards[]` with `badges` for a static + one. Replace `DeclarativeKanbanSchema` imports with `KanbanSchema` (from + `@object-ui/types`, or the Zod `KanbanSchema` from `@object-ui/types/zod`; + `@object-ui/plugin-kanban` re-exports the same `KanbanSchema` type). Delete + `draggable` (drag-and-drop is always on) and column `color` (style a lane + through `className`). `content/docs/api/schema-reference.md`'s kanban section + now documents this dialect. + + This is a breaking change shipped as `minor`: this repository's + version-alignment rule keeps objectui's major pinned to `@objectstack`'s and + ships objectui's own breaking changes as `minor` with the break spelled out in + the changeset body, which is what the bullets above are. +- caf477f: `ChartDataSeriesSchema` (and its TS twin `ChartDataSeries`) now REFUSES `chartType` on a chart + series BY NAME and points at `type` — the renderer-internal spelling the non-strict Zod object had + been **stripping in silence** while `safeParse` reported success (objectui#7694, the `domain:ui` PM + ruling on objectui#7546: option A, a named alias refusal, the posture `@objectstack/spec` already + takes). + + ⚠️ Shipped as `minor`, not `patch`, because this is a NARROWING of a published accept surface, and + it is named here in the words a release reader can act on: + + - **Before:** `series: [{ name: 'revenue', chartType: 'line' }]` validated green through + `@object-ui/types/zod` (`safeValidateSchema`, `objectui check` / `objectui validate`, any + pipeline that keeps `parse()`'s output) — and the key was gone from the output, so a consumer of + the parse result drew that series in the chart's own family, precisely what the author was + overriding. On the TypeScript face the key was merely an excess property on a fresh literal; + a widened object carrying it assigned structurally. + - **After:** the same document REFUSES at `series[i].chartType` (issue code `invalid_type`) with + one message on both channels — the parse-time issue and the `.describe()` metadata: + `Unrecognized key(s) on this chart series: \`chartType\`. Did you mean \`chartType\` → \`type\`? …` + followed by the reason and the remedy. Write `type: 'bar' | 'line' | 'area'`. On the TypeScript + face `ChartDataSeries.chartType` is a `?: never` tombstone, so both the fresh literal and the + widened assignment are `tsc` errors. + - **Both written** (`{ type: 'bar', chartType: 'line' }`) is refused at `chartType` alone — the + key is not folded onto `type` and no precedence is minted between the two spellings. + - **Which documents to scan.** The narrowing does not stop at `ChartDataSeriesSchema`; it reaches + every document through the parents that embed it — `ChartSchema.series` + (`zod/data-display.zod.ts:622`, `z.array(ChartDataSeriesSchema)`) and, one level further out, + `ReportSectionSchema.chart` (`zod/reports.zod.ts:105`, `ChartSchema.optional()`). Authors meet it + through `safeValidateSchema()` (`zod/index.zod.ts:434`, which parses `AnyComponentSchema`) and + through the CLI's `objectui check` and `objectui validate` commands (`packages/cli/src/cli.ts:211` + and `:223`). In practice: every `chart` node's `series[]`, and every report section whose `chart` + carries one. + + This repository's `major` is a cross-repo pin to `@objectstack`'s major, not a severity dial; the + break is announced here, which is the channel that carries it. + + ## Why a refusal, and not the two alternatives + + `chartType` is the renderer's INTERNAL spelling of `type`: the first limb of `normalizeSeries`' + `str(raw.chartType) ?? str(raw.type)` (`@object-ui/plugin-charts`, `normalizeChartSchema.ts:244`), + written by the internal-shape producers that hand `dataKey`-shaped arrays straight to + `ChartRenderer` (`ObjectChart`, `DatasetWidget`; `core/utils/chart-presentation` translates authored + `type` *into* it) and by nothing an author writes. Re-measured at implementation time, series-level, + with lit controls (`dataKey` / `name` / `type` / `color`): docs 0, fixtures 0, designer inputs 0 + (the `chart` registration's `series` is one `code` input), src literals 0, tests 9 — every one an + internal-shape array that never meets this mirror. Limb ablation over 304 files / 5817 tests: + deleting `str(raw.chartType) ??` left all green; deleting the `?? str(raw.type)` sibling went 2 red. + + - **Not a fold onto `type`.** The renderer takes `chartType` FIRST, so a fold would let the alias + overwrite the canonical key when both are written — the inversion of the objectui#7113 precedence + rule (`xAxis` → `xAxisKey` folds *because* the reader already prefers the canonical key). + - **Not a second writable name.** `@objectstack/spec`'s `ChartSeriesSchema` lists `chartType` in its + alias map as a spelling of `type` and refuses it by name; declaring it here would mint a second + de-facto contract against the spec's own posture (AGENTS.md #0.1). + + ## The primitive, and the JSON-Schema surface + + The new `aliasKeyRefusal()` helper (`zod/tombstone.zod.ts`, internal — not re-exported) reuses + `retirementTombstone`'s primitive, `z.never({ error }).optional().describe()`, deliberately not + `handlerKeyRefusal`'s `z.custom`: measured, `z.toJSONSchema` throws on a `z.custom` arm ("Custom + types cannot be represented in JSON Schema") and represents a `z.never` arm as `{ not: {} }` with its + description. `z.toJSONSchema(ChartDataSeriesSchema)` succeeded before this change and still does — + it now lists `chartType` as a refused property carrying the guidance, 11 properties to 12. + + ⚠️ The two io modes are not affected alike, measured on this tree. In `io: 'input'` the emitted + object carries no `additionalProperties` at all, so the accept set genuinely narrows there: + `chartType` goes from unmentioned to a property that nothing satisfies. In `io: 'output'` — the + default, and what a bare `z.toJSONSchema(…)` emits — the base **already** emitted + `additionalProperties: false`, so the key was outside the accept set before this change; what that + mode gains is the NAMED refusal and its guidance, not a narrower accept set. + + ## Unchanged, deliberately + + The object stays non-strict — a truly undeclared key is still stripped, exactly as + `chart-inline-data-retired.test.ts` pins. The six keys objectui#7546 declared, the `data` tombstone + (objectui#6896) and the at-least-one-binding refinement (objectui#6939 / #7113) are untouched. + **No reader changed:** `normalizeSeries` still reads `chartType` first on the internal-shape arrays + its producers hand it; that limb is a reader decision, not this declaration's. + + ## FROM → TO + + ```ts + // ChartDataSeries + + chartType?: never; // alias of `type` — refused by name; write `type` + ``` + + Pinned in `packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts` — + the refusal envelope, both-written, the TS face, the spec's own posture measured live on the + installed `@objectstack/spec`, and the JSON-Schema surface; `chart-series-keys-7546.test.ts` block + (d) now pins the handoff from the gap that card reported. +- f6375da: Export `ComboboxOption` from the `@object-ui/types` root entry (objectui#7697). + + **Additive only.** `ComboboxOption` is added to the root barrel's existing named + re-export list from `./form.js`, next to the sibling option types that were already + there (`SelectOption`, `RadioOption`). Nothing is removed, retyped or narrowed: the + declaration stays in `src/form.ts`, its three members (`value`, `label`, `disabled?`) + are unchanged, and the `@object-ui/types/form` subpath spelling keeps working exactly + as before. Both spellings now resolve to the same declaration. + + **Why it was missing.** objectui#7691 made this package the single authority for the + name — `@object-ui/components` stopped declaring its own copy and now re-exports this + one — but reached it through the `/form` subpath, because the root barrel was held by + objectui#7683 at the time. That choice was deliberate and was judged sound on its own + merits (the subpath is a house pattern, alongside `@object-ui/types/zod` and + `@object-ui/types/internal/retired-field-keys`); this release is the follow-up + objectui#7691 could not take, not a correction of it. + + **What changes for you.** `import type { ComboboxOption } from '@object-ui/types'` now + compiles; on the previous release it read `TS2305` while its two siblings on the same + list resolved. If you already import from `@object-ui/types/form`, or through + `@object-ui/components`, nothing changes and no migration is needed — both remain + supported and are pinned as such. +- a4611b3: Retire the six `ChatbotSchema` members no `plugin-chatbot` registration reads — + `loading`, `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown` and `height` — as + ADR-0049 retirement tombstones on **both** published faces (objectui#7703). + + ⚠️ **BREAKING for anyone authoring one of these six against `ChatbotSchema`, on either + face.** Ships as `minor` per the launch-window convention: objectui's `major` is a + cross-repo pin to `@objectstack`'s so that "same major means compatible" holds across the + two repos (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes + ship as `minor` with the break named where it lands — this entry is the channel that + carries it. + + ## What was retired, and why + + The published type taught six knobs that did nothing. An author — or an AI author reading + the `.d.ts` — wrote `showAvatars: true` or `height: 400` on a `chatbot` node, got no error + on either face, and saw no change. Two of them advertised a `@default true` for a switch + that did not exist. + + Re-measured on this branch's base (`21d7989fb`, i.e. **after** objectui#7708's fence + landed as PR #8077) rather than inherited from the card: one `schema.KEY` count per + `ComponentRegistry.register(...)` body of `packages/plugin-chatbot/src/renderer.tsx`, the + file split at the three register calls. + + | key | declared type | `schema.KEY` reads (`chatbot` / `chatbot-enhanced` / `chatbot-floating`) | + | :-- | :-- | :-- | + | `loading` | `boolean` | 0 / 0 / 0 | + | `showAvatars` | `boolean` (`@default true`) | 0 / 0 / 0 | + | `userAvatar` | `string` | 0 / 0 / 0 | + | `assistantAvatar` | `string` | 0 / 0 / 0 | + | `markdown` | `boolean` (`@default true`) | 0 / 0 / 0 | + | `height` | `string \| number` | 0 / 0 / 0 | + + Lit controls on the same instrument in the same pass: `placeholder` 1 / 1 / 1, `messages` + 1 / 1 / 1, `userAvatarUrl` 1 / 1 / 1, `maxHeight` 1 / 1 / 0, `floatingConfig` 0 / 0 / 1, + `processVisibility` 0 / 1 / 0. The zeros are readings, not a blind grep. + + ⛔ `processVisibility` is **not** in this retirement — `chatbot-enhanced` reads it, and + objectui#7655 left the `ChatbotSchema` member as it was. It is pinned live as a control. + + ### `showAvatars` is the one key the FENCE turned dark, not a key nothing ever read + + The distinction is recorded rather than smoothed over, because the two provenances are + different facts. `ChatbotEnhanced` really does have a `showAvatars` prop, and until + objectui#7708 the `chatbot-floating` registration ended its panel element with a raw + `{...props}` spread that handed an authored value straight to it — measured live through + the real host. That card was ruled **fence**, not declare, and landed as PR #8077: the + spread is filtered through `toDomProps` and moved ahead of every named prop. So the key is + dark on all three registrations **by ruling**, and this retirement records that, not an + absence. The other five were live on no channel at any time — they are not + `ChatbotEnhancedProps` members either (`markdown` exists there only as `enableMarkdown`), + so the spread had nothing to land them on. + + ## Enforce-or-remove, decided per key + + The other arm was taken key by key and refused each time. `<Chatbot>` + (`plugin-chatbot/src/index.tsx`) — the component the `chatbot` registration renders — + declares `messages`, `placeholder`, `onSendMessage`, `disabled`, `showTimestamp`, + `userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, `assistantAvatarFallback` and + `maxHeight`, and **not one of the six**. + + | key | why not enforce | migration | + | :-- | :-- | :-- | + | `loading` | Chat progress is runtime state the chat runtime owns — the registration derives it from `useObjectChat` as `isLoading`. A static authored boolean would fight the runtime, not configure it | delete the key | + | `showAvatars` | No target on `<Chatbot>`. Declaring it on the two faces that DO reach `<ChatbotEnhanced>` would re-open by declaration the channel objectui#7708 closed by fence, one card earlier | delete the key — a `chatbot` node already renders an avatar beside every message | + | `userAvatar` | A second authorable spelling of an image `userAvatarUrl` already carries (AGENTS.md #0.1: one strict contract, not N dialects) | `userAvatarUrl` (+ `userAvatarFallback`) | + | `assistantAvatar` | Same | `assistantAvatarUrl` (+ `assistantAvatarFallback`) | + | `markdown` | `<Chatbot>` prints message content as text and has no markdown path; on the two nodes that render markdown, `enableMarkdown` is the live key | `type: 'chatbot-enhanced'` with `enableMarkdown` | + | `height` | `<Chatbot>` has no `height` prop, and the live `maxHeight` it forwards is a `string`, not this key's `string \| number` union | `maxHeight`, or `floatingConfig.panelHeight` on a floating node | + + ## FROM → TO + + Each member goes to `?: never` on `packages/types/src/complex.ts` and to + `retirementTombstone(...)` on `packages/types/src/zod/complex.zod.ts` — both halves, in + lockstep, the convention `MarkdownSchema.sanitize` (objectui#6972), + `TimelineSchema.timeScale` (objectui#6355) and `ObjectViewSchema.viewTabBar` + (objectui#7779) already carry. Each refusal names the key, says why it is retired, and + points at what to write instead; one string feeds both the parse-time message and the + `.describe()` metadata, so the two cannot drift. + + ## Accept-set change, one line per face + + - **TypeScript.** A write of any of the six against `ChatbotSchema` used to compile and + now does not — including through a widened (non-fresh) value, which is the half a + deletion would have missed on a `BaseSchema` carrier. + - **Runtime (Zod / `safeValidateSchema`).** A `chatbot` document authoring one of the six + used to parse **green** and now parses **red**, `invalid_type` at the key's own path + with the guidance as the message. This is the narrowing that carries + `needs:contract-review`. + - **`chatbot-enhanced` / `chatbot-floating`.** Unchanged in both directions. Those faces + never declared the six (objectui#7655 censused them out), their twins have no arm to + refuse one, and `BaseSchema` is `.passthrough()` — so a stored node of either type + carrying a retired key parses exactly as it did. Pinned. + + ## Why tombstones and not deletions + + All six **have** a Zod arm, and that is what decides the route here. `BaseSchema` is + `.passthrough()` on the Zod side and carries a `[key: string]: any` index signature on the + TS side, so an UNDECLARED key is not refused — it is KEPT. Deleting the members would hand + the authored spelling exactly the silent no-op this card exists to close, on both faces at + once. The two-prong discriminator (`mobile.ts`, objectui#5941 / #7526 / #7678) leaves that + structural hazard to the carrier: where there is no mirror there is "no silent-strip hazard + for prong 2 to guard". Here there is a mirror to host the refusal, and prong 1 holds by the + letter for four of the six. The "deleted" row is pinned live as a control in + `packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts`, so the contrast + cannot rot into prose. + + ## Docs + + `content/docs/plugins/plugin-chatbot.mdx` gains the restatement of the six removed keys + with their replacements (objectui#7070: a control is restated, never deleted into a + vacuum), and its three present-tense claims that the `chatbot-floating` props spread is + still unfiltered are corrected to what PR #8077 actually left behind. +- 20316ba: `SchemaRegistry` names all three `plugin-chatbot` registrations, so the published + `ComponentType` union does too (objectui#7704). + + `packages/plugin-chatbot/src/renderer.tsx` registers three components — `chatbot`, + `chatbot-enhanced` and `chatbot-floating` — and `SchemaRegistry` mapped one of them. + Since `ComponentType = keyof SchemaRegistry` is the published union, a consumer + discriminating on it was told two registered keys do not exist: an author narrowing a + node by `ComponentType`, or writing a `Record<ComponentType, …>` table, had no arm for + either. The asymmetry that showed which half was wrong is that + `packages/cli/src/utils/known-schema-types.ts` keeps its own parallel list containing + both keys, precisely because this map did not. + + **Additive.** Two keys join the map under its `// Complex` group; no existing entry + changes, `'chatbot'` still maps to `ChatbotSchema`, and nothing is removed. The union + widens, which cannot break a consumer that produces `ComponentType` values and can only + help one that consumes them — except an exhaustive `Record<ComponentType, …>` or + `switch`, which now needs the two new arms. Nothing in this repo has one: the only + consumer of `ComponentType` outside its own declaration is a pin test. + + **Why the entries can be honest now.** This map's value has to be the type the + registered renderer honours, and until objectui#7655 there was none to point at — + `ChatbotSchema` pins `type` to `'chatbot'`, and each registration's real key set lived + in an anonymous `ChatbotSchema & { … }` intersection local to the renderer file. + objectui#7655 published `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` from this + package, and both registrations already take them as their `schema` parameter, so the + map's value and the renderer's prop type are one declaration — the same property the + `'kanban'` arm gained in objectui#7664. Being declared here also makes them reachable: + `@object-ui/types` has zero workspace dependencies, which is exactly what blocked the + `kanban` case objectui#7645 measured, where the honoured type lived in a plugin. + + Pinned in `src/__tests__/schema-registry-chatbot-keys-7704.test.ts` in two channels — + compile-time (`tsc -p tsconfig.test.json`: the keys survive in `keyof`, each value is + the face its renderer honours, and each value's own `type` literal is its key) and + runtime (a TypeScript-AST census of the interface source, plus each key selecting its + own arm through `safeValidateSchema`). + + Scope: these two keys, whose authoring faces now exist — not a sweep of the map's other + entries, which objectui#7665 holds. +- d3499b3: `chatbot-floating` now fences its `<FloatingChatbot>` spread the same way its + two sibling registrations (`chatbot`, `chatbot-enhanced`) already do — + `{...toDomProps(props)}`, at the head of the element, instead of a raw + `{...props}` spread at the end (objectui#7708). This is a deliberate, + user-visible behavior change, not a refactor: + + - **A message sent through a floating chatbot now actually renders.** + Previously the authored `messages` seed (whatever array was on the node + when it was authored) silently overrode the live runtime messages on every + render, because the raw spread landed AFTER `messages={runtimeMessages}`. + Neither the user's own message nor an `autoResponse` reply ever appeared — + the identical send on `chatbot-enhanced` worked correctly. Fixed. + - **`displayMode`, `systemPrompt` and `model` stop leaking as DOM attributes** + on the panel's root element (`systemPrompt` / `model` are still read + normally, by name, for the request they configure — only the second, + unfiltered forward is gone). Closes objectui#4425's leak class on the one + `plugin-chatbot` registration that had not closed it yet. + - **Three undeclared keys go dark on `chatbot-floating` nodes:** + `processVisibility`, `surface` and `showAvatars` reached the panel's + `ChatbotEnhanced` through the raw spread even though `ChatbotFloatingSchema` + never declared them. `ChatbotFloatingSchema` documents this explicitly and + always has — the face never promised these keys — so this closes an + accidental channel rather than removing declared behavior. A document that + relied on any of the three to affect a floating node loses that effect; + author them on a `chatbot-enhanced` node instead, where they are part of + the declared, tested contract. + + `@object-ui/types`: `ChatbotFloatingSchema`'s doc comment is updated to match + — no type-shape change, so nothing that imports the type needs to change. +- 18897a4: Declare `wrapperClass` on `SwitchSchema`, `TextareaSchema`, `DatePickerSchema`, + `SelectSchema` and `ListSchema`, on both faces (objectui#7722 — the read-driven + residue outside objectui#6938's batch, one key over five more types). + + Each of `renderers/form/switch.tsx`, `textarea.tsx`, `date-picker.tsx`, + `select.tsx` and `renderers/data-display/list.tsx` reads `schema.wrapperClass` + onto its wrapper element, and neither the TypeScript interface (`form.ts`, + `data-display.ts`) nor the zod mirror (`zod/form.zod.ts`, + `zod/data-display.zod.ts`) declared the key. The reads compiled through + `BaseSchema`'s index signature (objectui#5155) and the values parsed through + `.passthrough()`, admitted unexamined. The same key, on the same class of read, + is declared on `CheckboxSchema` (objectui#6938), `FileUploadSchema` and + `FilterBuilderSchema` (objectui#6150); these five were left out only because + their doc pages never listed it. + + **minor, not patch — the published face gains five members.** objectui#6938 and + objectui#7295 graded a one- or two-key residue `patch` because "the accept set + only widens toward what already renders"; that reasoning still describes the + VALUE dimension here, but this change is the batch shape of objectui#6150 + (`minor`), and it is dispatched under the contract-review tier precisely because + it widens the PUBLISHED surface: five schemas each gain a member of the shipped + `.d.ts` and of the mirror's `.shape` that an editor completes, an annotation + checks and a validator enforces. Two verdicts move, in opposite directions: + + - **Nothing well-typed stops validating or compiling.** Key membership was never + narrow: `[key: string]: any` admitted the key on the TS face and + `.passthrough()` admitted it on the zod face, so every document that carried a + string `wrapperClass` parsed green before and parses green now, with the value + surviving the parse exactly as before. + - **A non-string `wrapperClass` is now REFUSED at the key** on these five mirrors + (`{ type: 'switch', wrapperClass: 42 }` parsed green before; it is refused now, + at `wrapperClass`). That is enforcement of the declared type, not a new + capability, but it is a behaviour change for a document that carried a + wrong-typed value under one of these five names — a value the renderer would + have interpolated into the class string as text. + + Keys outside the five are untouched: an undeclared key of any type is still + admitted unexamined on all five mirrors, pinned per mirror with a control key + the renderer does not read. `InputSchema.wrapperClass`, declared on the TS face + only, is a recorded row of the parity ledger (`UnmirroredDeclared`) and stays + there; the new sweep pin carries it as a self-expiring exemption. +- 8b7ea39: The zod mirrors stop authoring defaults; the renderer's fallback is the one + authoritative default (objectui#7735, director ruling, decision batch #69, + 2026-09-07 — maintainer reply 「其他同意」). + + **A validator validates; it does not write values into an author's document.** + `.default(v)` on a mirror member is not documentation — `parse` SUBSTITUTES `v` + into the output when the key is absent. One authored document therefore had two + shapes depending on whether it had been through `safeValidateSchema`, and where + the mirror and the renderer disagreed the mirror silently won: + + | key | mirror wrote | the renderer applies | + |---|---|---| + | `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | + | `FlexSchema.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | + + A `container` omitting `maxWidth` rendered `max-w-xl` as authored and `max-w-lg` + after a round-trip through the mirror; `'center'` is a value neither `flex` nor + `stack` applies at all. `StackSchema` already declared no defaults, so the file + was not even self-consistent. + + **What changed.** All 41 `.default()` call sites under `packages/types/src/zod/` + are removed — `layout.zod.ts` 22, `crud.zod.ts` 11, `form.zod.ts` 5, + `views.zod.ts` 2, `app.zod.ts` 1. `@object-ui/components` reconciles the third + face objectui#8229 found: `flex`'s registration `defaultProps.align` seeded + `'center'`, the value its own renderer never applies, so a designer-made node + laid out differently from a hand-authored one; it now seeds `'start'`. + + **Accept set unchanged.** Every one of the 41 was spelled + `<type>.optional().default(v)`, so none was carrying optionality: nothing that + parsed before is refused now, and nothing refused before is accepted. This is a + narrowing of `parse` OUTPUT only. + + **⚠️ Breaking for consumers that read `result.data`.** `validateSchema` / + `safeValidateSchema` no longer substitute: for a document that OMITS one of these + keys the key is now ABSENT from the parsed output instead of carrying a value the + document never authored. Every one of the 41 keys changes output this way. If you + read a default off the parsed document rather than applying your own fallback, + apply the fallback yourself. + + **And rendering changes too, wherever the mirror's value differed from the + renderer's.** An earlier draft of this note said rendering does not change on the + ground that "the two keys that disagreed were the bug"; that was the triage + sample of 7 keys, not a measurement of the population. Auditing all 22 layout + keys against their renderers finds **six** disagreements, not two: + + | key | the mirror wrote | what the renderer applies when the key is absent | + |---|---|---| + | `container.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` | + | `flex.align` | `'center'` | `flex.tsx`: `\|\| 'start'` | + | `grid.columns` | `3` | `grid.tsx`: `let baseCols = 2` | + | `text.variant` | `'body'` | `text.tsx`: none — a bare node, no typography class (objectui#6942) | + | `resizable.withHandle` | `true` | forwarded bare; `undefined` draws NO grip | + | `page.template` | `'default'` | `page.tsx`: a null template falls through to the `pageType` dispatch | + + So a consumer that renders `result.data` sees a different result on all six. What + does NOT change is rendering inside this repository: the render path never ran + this validator at all — `SchemaRenderer` calls the STRUCTURAL `validateSchema` + from `@object-ui/core`, which returns `{valid, errors}` and hands the renderer + the author's own object. The four modules that do import `@object-ui/types/zod` + were censused: two read only `result.success`, one discards `result.data`, and + `os validate` reads only `type` / `id` / `label` / `title` / `children`, none of + which ever carried a default. The four JSDoc `@default` tags that described the + mirror rather than the renderer (`grid.columns`, `text.variant`, + `resizable.withHandle`, `page.template`) are corrected in the same change. + + Note that `safeValidateSchema` still substitutes through subschemas imported by + reference from `@objectstack/spec` (`app.active`, `object-view.navigation.*`, + `list-view.sharing.type`, and others). Those values are written in that package, + not this one, and are unaffected by this change. +- 64a252d: **`CollapsibleSchema.trigger` now declares the node array its Zod mirror, its runtime and its own shipped default already accept** (objectui#7767). + + `trigger` on `CollapsibleSchema` widens from `string | SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face, and stays required. The dropped `string |` half was redundant rather than an extra: `SchemaNode` is `BaseSchema | string | number | boolean | null | undefined`, so `string | SchemaNode` already denoted exactly `SchemaNode` — a bare string trigger type-checks before and after. `SchemaNode` itself is unchanged. + + This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/disclosure.zod.ts` already spelled this key `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: `renderers/disclosure/collapsible.tsx` hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and that same registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying the renderer's own default into a typed document is no longer a type error against the type that shipped it. The docs page's `trigger` row follows the declaration. + + This is the eighth member of the change objectui#7081 made to the overlay family, carried out under the same ruling (2026-09-03 on that card): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). +- c842594: `DashboardComponentSchema.widgets` gains the component-node arm on the TypeScript face (objectui#7952) + + `widgets` was `DashboardWidgetSchema[]` in TypeScript while the zod schema it mirrors has been a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the widget slot (`type: 'metric-card'`, body validated as passthrough `BaseSchema`) or a spec-family widget. So the shape `@object-ui/plugin-dashboard`'s README teaches in every `metric-card` example — and the shape the shipped `DashboardRenderer` renders — parsed green under `safeParse` and was refused by `tsc --strict` (`TS2561: 'value' does not exist in type 'DashboardWidgetSchema'`, six occurrences across the README's dashboard blocks at `fc32921`). There was no annotation an author could write for a document the platform accepts. + + **Accept-set change (Clause ②, TypeScript face only).** `widgets` is now `Array<DashboardWidgetSlotComponentSchema | DashboardWidgetSchema>`, and `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` — is a new export of `@object-ui/types`. The zod schema is unchanged; `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue` (those are `MetricCard`'s registry inputs, not widget keys — the compiler's `Did you mean to write 'values'?` points at the repair both declarations forbid). + + **What still refuses.** A widget that names a spec-family `type` and carries an undeclared key (`{ type: 'bar', bogus: 1 }`) is still a `tsc` error: the literal is discriminated by `type`, so the passthrough arm never applies to it. A `type` outside both vocabularies is refused as before. The one corner the TypeScript union cannot discriminate — a legacy `component` envelope with NO `type` plus an undeclared key — compiles on the TypeScript face and is refused by name at validation, as every `BaseSchema` slot already behaves. + + **Consumers.** The new arm is assignable to `DashboardWidgetSchema`, so code that annotates a widget callback `(w: DashboardWidgetSchema)` keeps compiling unchanged. Code that reads a property off an unannotated element of `schema.widgets` now sees the union, and through `BaseSchema`'s index signature that read is `any` rather than the widget's declared type — annotate the parameter to keep the narrower type. +- cf1d29e: `ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one + (objectui#8067). + + A registration's `type: 'array'` said a value was a list and stopped there, so a member + that drifted from `@objectstack/spec` was invisible to every layer that reads a + declaration. `page:header.actions` is the measured cost: the contract declares + `z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef` + objects, and the repo-wide parity gate in + `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the + whole life of the drift because both sides carried the key and neither could say what + was inside it. What settled it was a maintainer ruling, not a test — and even after the + fix, "these are ids" survived only as English in the registration's `description`. + + **What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind + vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an + `object` used as a map. One kind, or an array of them for a member contract that is a + union, with `type`'s semantics — a member passes when any declared arm accepts it. The + manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per + input instead of six. + + **Three readers ship with it**, which was the bar this slot had to clear (objectui#5905 + is the precedent: five `ComponentInput` keys declared and read by nothing). The + repo-wide parity gate compares every declared `of` against the member kind + `ComponentPropsMap[type]` actually accepts and fails on one the contract refuses; + `sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new + `member-type-mismatch` diagnostic naming the offending positions; and the generated + `sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is + `string[]` where it used to be `unknown[]`. + + **Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen: + every container key's member position was probed with one value of each coarse kind and + a declaration written only where exactly one kind was accepted. A member contract that + admits several kinds — `record:highlights.fields` takes a field name or an inline field + object — is deliberately left undeclared and pinned with its reason, because picking one + arm there is a narrowing this repo leaves un-gated and picking all of them would + advertise shapes only a per-block pin can vouch for. + + **The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer + ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description` + is the publication face's expression ceiling, and spec is the sole judge of values — + stands exactly as written. `of: 'object'` says the members are objects; which keys they + carry is still `description`'s job and `os validate`'s. + + **Nothing published before this changes.** An input that declares no `of` validates, + serializes and types byte-identically: `validateTree` checks no member, the serializer + emits no key, and the codegen emits the same `unknown[]`. +- 6bca0e4: **Breaking for authored metadata:** the legacy `ActionSchema`'s Phase-2 callback + pair — `onSuccess` / `onFailure`, each carrying an `ActionCallback` object — is + RETIRED, and the `ActionCallback` type and its Zod mirror `ActionCallbackSchema` + (with the inferred `ActionCallbackSchemaType`) are DELETED from `@object-ui/types` + and `@object-ui/types/zod` (objectui#7068; maintainer ruling option 1 of + 2026-09-05, immediate, no deprecation window; ADR-0049 enforce-or-remove). + + **What an author who wrote the shape sees now.** A `{ type: 'action', … }` + document authoring `onSuccess: { type: 'toast', message: '…' }` (or any + `onFailure` callback) no longer validates: the parse fails loudly on the + `onSuccess` / `onFailure` path (`invalid_type`, expected `never`) with the + explanation and the migration in the message, and the TypeScript members are + `?: never` tombstones so the same document is a `tsc` error at the authoring + site. `import type { ActionCallback } from '@object-ui/types'` and + `import { ActionCallbackSchema } from '@object-ui/types/zod'` fail to resolve. + + **What was measured, on this branch's base (`900f8d99`).** `ActionCallback` + (`{ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | + 'dialog', message?, url?, api?, method?, dialog?, handler? }`) was declared in + `crud.ts`, mirrored in `zod/crud.zod.ts`, re-exported by both barrels, and + carried on the legacy `ActionSchema` as `onSuccess?` / `onFailure?`. Producers: + the package's own `phase2-schemas.test.ts` fixture and three `ts` fences in + `content/docs/core/enhanced-actions.mdx` — nothing else (`git grep -l + ActionCallback` over `packages content skills` hit the five `packages/types` + files; positive control `SchemaNodeSchema` hit 22). Runtime readers: none — + `ActionRunner` imports `UIActionSchema`, never this interface, and its own + `ActionDef.onFailure` is a different (runner-native) meaning. It was the THIRD + meaning of one key: objectui#5934 had already retired the runner's callback + meaning of `onSuccess` and converged it on the spec's block. + + **Why authored JSON that passed publish is unaffected.** `@objectstack/spec`'s + `ActionSchema` (installed pin 17.2.0) already refused the callback shape at + publish — `invalid_type` at `onSuccess.navigate` plus `unrecognized_keys` on the + `onSuccess` block, and `onFailure` refused as an unrecognized key on the action — + so no published or saved metadata could carry it. Only TypeScript code that + typed a callback against the legacy interface, or JSON validated solely through + `@object-ui/types/zod`, meets the new refusal. + + **Where the live meaning lives.** Post-success navigation is the spec's + `onSuccess` block, `{ navigate, openIn }`, declared on `UIActionSchema` + (`ui-action.ts`) and forwarded to the runner (objectui#5934). A success or + failure notice is `successMessage` / `errorMessage` — adjacent keys on the same + legacy `ActionSchema`, NOT retired, and still accepted on both faces. + + **Two published faces, one retirement — tombstone on the keys, deletion of the + type.** `BaseSchema` is `.passthrough()` on the mirror and carries an index + signature on the interface, so DELETING the two keys would have ADMITTED an + authored callback unchecked on both faces; they stay declared as `?: never` / + `retirementTombstone()` (the PR #7761 / #7769 shape) and the base-vs-extended + contrast is pinned. The standalone `ActionCallback` / `ActionCallbackSchema` have + no such escape hatch and are deleted outright, the route objectui#7664 / PR #7743 + took for the `DeclarativeKanban*` trio; the parity ledger drops the pair + (`EXPECTED_MIRROR_PAIRS` 159 → 158) and the absence is pinned in + `action-callback-retired-7068.test.ts`. + + **Docs, same change.** `content/docs/core/enhanced-actions.mdx` — the three + `onSuccess` / `onFailure` fences author `successMessage` / `errorMessage` + instead, and the "Callbacks" section is a "Post-success behaviour" note pointing + at the spec block (no fence: the legacy type carries no spec-derived block). + `content/docs/guide/schema-overview.md` — the fragment line, the feature bullet + and the checklist row are rewritten to the truth (the ✅ claim is now a + retirement note). + + **Migration:** delete `onSuccess` / `onFailure` from any legacy `ActionSchema` + document or fixture; write `successMessage` / `errorMessage` for notices, put + follow-up work in `chain`, and author post-success navigation as the spec's + `onSuccess: { navigate, openIn }` block on `UIActionSchema`. + + Graded `minor`, not `patch`: this narrows the accepted input set on both faces + and removes two exports, which is breaking for any consumer who wrote the shape. + It is not `major` per this repo's fixed-group convention (objectui's own breaking + changes ship as `minor`; the group's major tracks `@objectstack` — AGENTS.md + 版本号策略, mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 2fcefb9: **`AppComponentSchema.hidden` is now declared `boolean` — the spec's app-catalogue flag — instead of the `boolean | ExpressionWire` hide predicate it inherited from `BaseSchema`.** On the `app` node two keys collide in name and differ in meaning (objectui#7542): the zod mirror takes `@objectstack/spec/ui` `AppSchema.hidden` ("Hide from the App Switcher") by reference through `SpecAppFields`, where the spec's `z.boolean().optional()` lands after the base's key and overrides it, so the validator has always refused a predicate string or a CEL envelope object at path `hidden` on an `app` document — through `AppComponentSchema.safeParse` and through `safeValidateSchema` alike — while the published TypeScript interface, restating nothing, invited exactly that spelling. This is direction 1 of the card: the declaration is pulled back to what the validator enforces. Direction 2 — giving the catalogue flag its own name upstream in `@objectstack/spec` so the app node can inherit the renderer's predicate again — stays open as the alternative and is a protocol change, not taken here. + + Breaking on the TypeScript face only: a predicate string or envelope on an `app` node no longer type-checks, and an `app` node cannot use the predicate spelling every other node accepts. Authored JSON metadata is unaffected — the validator never admitted it — and the in-repo reader (`filterActiveApps` in `@object-ui/app-shell`) already treats the key as the boolean `hidden !== true`, never evaluating it. The renderer's `hidden` predicate on every other node is unchanged, `BaseSchema.hidden` is untouched, and the `KnownDrift` row objectui#7455 seeded for this pair is removed because the drift it recorded is gone. +- b55a346: `'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema` + (objectui#5740 — the value-level residue of objectui#5667's key-level + convergence of `CalendarViewSchema` on the registered `calendar-view` + renderer's measured read set; ADR-0049 enforce-or-remove). + + The union declared a value nothing enforced: the registered renderer's `view` + input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves + any off-enum value — `'agenda'` included — to `undefined` (the component's + `'month'` default), and `CalendarView` renders no agenda view. An author + writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with + no error or warning. No in-repo, example, or catalog app authors + `view: 'agenda'` (measured during objectui#5667's sweep and re-measured for + this change, including the objectstack tree). + + **This narrows the accept set — unlike #5667's key retirements, which created + no new rejections.** `view` is a declared key, and declared keys are validated + even under `.passthrough()`, so `view: 'agenda'` is now a **validation error + that previously parsed green** (an `invalid_value` issue on the `view` path, + offering `month`/`week`/`day`). Undeclared keys still pass through unchanged. + Breaking on the published zod surface; ships as `minor` per this repo's + version-alignment policy (majors track `@objectstack`). + + The runtime boundary is unchanged: an off-union `view` in raw metadata still + falls back to the component's `'month'` default at the renderer, and the + registry input already declared the three-value enum. Docblocks, the schema + reference table, and the zod `describe` no longer teach an `'agenda'` + fallback. +- 065bba7: `CalendarViewSchema` (TS interface and zod mirror) converges on the registered + `calendar-view` renderer's measured read set (objectui#5667, maintainer ruling + option A — the renderer is authoritative). + + **Breaking for consumers of the published type** (deliberate; per-repo policy + breaking changes ship as `minor` — the fixed group's `major` tracks + `@objectstack`): + + - Nine inert keys are retired: `events` (the interface's only required key, + which the renderer deliberately drops — objectui#4433), `defaultView`, + `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + `onEventUpdate`, `onDateChange`. None had a read site on the authored-node + path and no measured app authors them (ADR-0049 enforce-or-remove). + - The type now declares what the renderer actually reads: `data`, `titleField`, + `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, + `currentDate`, `allowCreate`, `className`, plus the two host-only function + hatches it forwards (`onEventClick`, `onViewChange`). + - Practical radius, measured: `BaseSchema` carries an index signature and the + zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys + neither fail to compile nor get rejected at validation — they are simply no + longer declared, documented, or type-checked. The material accept change is + that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` + node without it now validates (previously the one key validation demanded + was the one key guaranteed to do nothing). + + Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README + and `content/docs/api/schema-reference.md` are repaired to the converged + surface in the same change, so no copy of the old contradiction survives. +- 6d1c155: `ComponentMeta` is now declared once and re-exported, and `PluginComponentMeta` is + deprecated in favour of `ComponentMeta` (objectui#5893). + + ## The convergence + + `@object-ui/types` published `ComponentMeta` twice, from two different declarations: + `base.ts` and `plugin-scope.ts` (the latter published as `PluginComponentMeta`). They + were structural copies, not an alias pair. `plugin-scope.ts`' `ComponentMeta` is now + `export type { ComponentMeta } from './base.js'` — the disposition objectui#4580 ruled + for the identical shape, *a structural copy would reproduce the defect the moment either + side moved*, and the same move objectui#5671 made for the sibling type `ComponentInput` + in the same file. + + Either side had already moved. `base.ts` declared eleven keys; the plugin-scoped copy + declared nine — the same nine, **minus `tags` and `description`**. So a plugin author + typing against the plugin-facing declaration could not write two keys the main surface + advertises, and which the runtime validator already accepted: `ComponentMetaSchema` in + `zod/base.zod.ts` declares all eleven, so two of the three authorities agreed and the + plugin-facing one did not. `resizeConstraints`' six members were identical in both, so + the delta was exactly those two keys. + + What changes for a consumer: `tags` and `description` become writable on the + plugin-facing type. Nothing narrows — no key is removed and no key's type changes, so no + existing registration stops compiling. The convergence buys **acceptance** of two keys; + it buys no rejection of anything. `ComponentMetaSchema` is a plain `z.object` with no + `.strict()`, so it strips unknown keys rather than refusing them, and that is unchanged + here. + + ## The alias deprecation, sequenced after it + + `PluginComponentMeta` — the published alias for the plugin-scoped declaration — is now + `@deprecated` in favour of `ComponentMeta`. **`PluginComponentMeta` is the name to search + for** if you import it; replace it with `ComponentMeta` from the same entry point. + + This is stage 1 of objectui#5674's two-stage retirement (maintainer ruling, 2026-08-22: + deprecate for a release, then remove). Nothing is removed here — the export still exists + and still names the same type. + + The ordering is deliberate and is why the two halves ship together. Until the convergence + above, the alias named a genuinely different nine-key interface; deprecating it then + would have warned consumers about a name that was still about to change meaning. It is + deprecated now, at its final meaning. + + **Why a deprecation window rather than a deletion.** The measurement that licenses + deleting an export from a published package is *"no importer"*, and what can be measured + from inside this repository is only *"no importer here"*. In-repo, `PluginComponentMeta` + has exactly one occurrence — its own export line — searched across every root + (`packages/`, `apps/`, `content/`, `docs/`, `skills/`, `examples/`, `e2e/`, `scripts/`, + `eslint-rules/`, `public/`, `.changeset/` and the root docs) plus the sibling + `objectstack` framework checkout, with controls searched identically so a broken search + could not read as a clean one. What no search here can see is a consumer on npm. **That + external caveat is unchanged from objectui#5674 and is not being dropped:** the window + converts a silent break into a warned one before stage 2 lands. Stage 2 removes the alias + and the now-dead re-export in `plugin-scope.ts` that exists only to feed it, and ships as + a `minor` under this repo's policy that its own breaking changes never declare `major`. + + ## Pinned by identity, not by member set + + A new test asserts that `plugin-scope.ts` re-exports the declaration and declares no + `ComponentMeta` of its own. A member-set assertion cannot do this job: TypeScript is + structurally typed, so a local re-declaration carrying the same eleven keys is mutually + assignable with the imported one and passes every type-level check. A member-identical + structural copy is exactly what objectui#4580 predicted would drift and exactly the state + this card recorded — this copy started identical and acquired its two-key delta later. + The member-set checks are kept alongside the identity pin, labelled as the control that + shows what it cannot see. +- d7573b3: `ComponentInput` is now declared once and re-exported, instead of restated in three + places (objectui#4972). + + `@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' + plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) + were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now + re-exports of that one declaration, which is the disposition objectui#4580 ruled for the + identical shape — *a structural copy would reproduce the defect the moment either side + moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. + + Either side had already moved. `base.ts` declared thirteen keys; both copies declared + nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every + component registration actually imports**. Those four keys were unwritable at any real + registration — a plain TypeScript error at the call site — while `ComponentInputSchema` + (the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face + advertised four keys the authoring face rejected. Measured over the repository, no + registration had tried to write one yet, so nothing a user hits was broken today; what + changes is that the four keys become writable, and there is no longer a second + declaration for the next widening to miss. + + `ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single + declaration imported by all three sites (objectui#3832); this converges the rest of the + interface. + + Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is + byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported + here only with its control — a probe that added a *required* key to `ComponentInput` left + the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of + `export *` lines that names `ComponentInput` zero times. The gauge that can actually fail + is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does + `@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted + declarations that change in either package. + + `WidgetInput`'s union-arm capability is deliberately untouched — a different gate path + and a separate judgment. +- 18a8e7d: **Published TS surface narrowed:** `DashboardComponentSchema` no longer declares + the `aria` member (`{ ariaLabel?, ariaDescribedBy?, role? }`). Its doc comment + claimed alignment with `@objectstack/spec AriaPropsSchema`, but the spec removed + `dashboard.aria` at the #3896 audit close-out — `DashboardSchema.shape.aria` is + a tombstone that refuses any value and tells authors to delete the key — and no + dashboard renderer ever read `schema.aria` (objectui#5830). + + What an author loses is the **type-level suggestion** only: the key was already + refused at parse (the Zod twin inherits the spec tombstone by reference), and + `BaseSchema`'s index signature means an existing `aria:` line still compiles. + There is **no runtime behaviour change** — the key never rendered, and stored + documents carrying it already failed validation before this release. +- e7957ab: **Retired the designer-surface dashboard `aria` pair — `DashboardConfig.aria` and `DashboardConfigSchema.aria`** (objectui#5852). + + Both spellings are named verbatim above so a host can grep its own sources: the + retired member is `aria`, on the TypeScript interface `DashboardConfig` + (`@object-ui/types`, `designer.ts`) and on its Zod mirror `DashboardConfigSchema` + (`@object-ui/types/zod`). It declared `{ label?: string; description?: string }`. + + **Why.** The spellings `label`/`description` match neither `@objectstack/spec`'s + `AriaProps` vocabulary (`ariaLabel` / `ariaDescribedBy` / `role`) nor anything a + renderer maps, so no read point could have consumed them even in principle. + Re-measured on `main` at the retirement: zero `.aria` reads in + `packages/plugin-designer/src`, `packages/plugin-dashboard/src` and + `apps/console/src`; zero occurrences of either name anywhere in the `objectstack` + repo; and `DashboardConfigPanel.tsx` — the panel the interface's own doc comment + says it serves — imports neither name. + + **The two directions differ, and neither is a no-op:** + + - **TypeScript (a narrowed suggestion, not a compile break).** `DashboardConfig` + carries a `[key: string]: any` catch-all, so an existing `aria:` line still + compiles; what is gone is the editor suggestion and the false implication that + the key was part of the contract. + - **Zod (a behaviour change — read this one).** `aria` is now an ADR-0049 + retirement tombstone (`z.never().optional()`), following this package's + existing convention. Previously an authored `aria` was **accepted and + preserved** in `safeParse` output; it is now **refused by name**, with `aria` + in the issue path and a message telling the author to delete the key. A plain + deletion was deliberately not taken: `DashboardConfigSchema` is a bare + `z.object` with no `.strict()`, so deleting the key would have made an + authored `aria` **silently disappear** from the parsed output instead — a + quiet data loss in place of a loud refusal. + + **External caveat.** In-repo consumer count is zero, but that is not the npm + count: `@object-ui/types` is published, and stored dashboard configuration is + not reachable from this repo. A host that authored `aria` on a `DashboardConfig` + document will now see a validation error naming the key where it previously saw + a silently carried value. The remedy is to delete the key — it never reached a + renderer. +- f7e34ca: Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectui's own component extension. + + `DashboardWidgetSchema.type` was `string` on the TypeScript interface and `z.string()` in the Zod twin — an unbounded hatch. A typo'd family, a chart type the spec retired, and a component type nothing registers all type-checked and validated, surfacing only as the renderer's red `OBJUI-001` panel at runtime. + + It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum). + + Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- ed8df3e: `'agenda'` leaves `defaultView` on all three of its declaration faces + (objectui#5784 — the `defaultView` sibling of objectui#5740's retirement on + `CalendarViewSchema.view`; ADR-0049 enforce-or-remove): the + `ObjectCalendarSchema` TS interface (an inline union `#5740`'s + `CalendarViewMode` narrowing could not reach), the zod `ObjectCalendarSchema`, + and the list-view `calendar` config's objectui-only `defaultView` extension. + All three are now `['month', 'week', 'day']`. + + The declarations admitted a value nothing enforced: `ObjectCalendar`'s props + declare `defaultView?: 'month' | 'week' | 'day'`, its schema read casts to the + same three values, and `CalendarView` renders no agenda view. An author + writing the type-legal, zod-valid `defaultView: 'agenda'` on an + `object-calendar` node or in a list view's `calendar` config got a month + calendar with no error or warning. The spec side already agrees: + `@objectstack/spec`'s `ObjectCalendarProps.defaultView` is + `['month', 'week', 'day']`. No in-repo, example, or catalog app authors + `defaultView: 'agenda'` (measured for objectui#5784 with positive controls, + including the objectstack tree — its only `agenda` token is the Agenda + job-scheduler library). + + **This narrows the accept set: an author who writes `defaultView: 'agenda'` + will now be refused at validation.** `defaultView` is a declared key, and + declared keys are validated even under `.passthrough()`, so + `defaultView: 'agenda'` is a **validation error that previously parsed + green** (an `invalid_value` issue on the `defaultView` / + `calendar.defaultView` path, offering `month`/`week`/`day`). Undeclared keys + still pass through unchanged. Breaking on the published zod surface; ships as + `minor` per this repo's version-alignment policy (majors track + `@objectstack`). + + The runtime boundary is unchanged: `ObjectCalendar` still resolves an + off-union raw `defaultView` away to its `'month'` default. Docblocks and both + zod `describe` strings now teach the three-value set. +- 199d31b: **`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both + gantt renderer branches honour it** (objectui#5074, maintainer ruling + 2026-08-19: declare-and-wire; the spec half landed first upstream). + + - `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, + DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` + enum by reference, so the member list cannot drift. Deliberately no + default: an omitted `viewMode` keeps letting a persisted layout + (`persistLayoutKey`) seed the timeline granularity before the renderer's + `'day'` fallback. + - The timeline branch (`GanttView`) now receives an authored `viewMode`. + Previously only the resource-workload branch (`resourceView` + + `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt + view was silently ignored. + - The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both + branches read the declared `ganttConfig.viewMode`, which also honours the + key when authored inside the spec's `gantt` config block. + - Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value + (e.g. `viewMode: 'hour'`) becomes a zod validation error where it + previously passed through unvalidated. Values on the published spec enum + are unaffected. +- 3e01cb5: **`BaseSchema.hidden` now declares the predicate string the renderer already evaluates** (objectui#7455, maintainer ruling 2026-09-03). + + `hidden?: boolean` becomes `hidden?: boolean | string`, and the Zod mirror's `z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible` (#4581) and `disabled` (#4580 ruling Q3-A) on both faces. `hidden` was the third key on the same evaluated path and the only one still declared boolean-only. + + This is a **widening**, not a replacement: every boolean `hidden` keeps parsing and keeps type-checking unchanged, and the renderer's behaviour is untouched by this change — `SchemaRenderer`'s `shouldHide` chain already routed this key through `hasDeclaredPredicate` and evaluated it, which is the evidence the widening rests on. What changes is that authors and their tooling can now write `hidden: "${data.status === 'draft'}"` without casting past the declaration, and the Zod mirror stops refusing it (before this, that value failed `safeParse` with `invalid_type` at path `hidden` while the identical string on `visible` parsed). + + `hiddenOn` is unchanged and remains the sibling expression spelling. The CEL envelope object form is still declared on none of `visible` / `hidden` / `disabled`; objectui#7530 rules on all three together. + + Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略"). +- 105f3c5: Retire `CRUDSchema` and the `type: 'crud'` node spelling (objectui#5373, + maintainer ruling 2026-08-20, route 2) under ADR-0049 enforce-or-remove. + + `crud` had four declaration faces and no registered renderer, for the whole + life of the key: the TS interface (`packages/types/src/crud.ts`), the zod + mirror (`packages/types/src/zod/crud.zod.ts`), a dedicated branch in + `validateSchema` that affirmatively PASSED it, and `CRUDBuilder` in + `@object-ui/core`. A node spelling it painted the OBJUI-001 "Unknown component + type" panel, and `content/docs/api/schema-reference.md` published it as + reference material — so a reader (or an AI author) who copied the page got a + red panel. + + Removed from `@object-ui/types`: the `CRUDSchema` interface and its zod + mirror, the four shapes that existed only to type its keys — `CRUDOperation`, + `CRUDFilter`, `CRUDToolbar`, `CRUDPagination` and their zod mirrors and + `…SchemaType` aliases — and `CRUDSchema` as a member of `CRUDComponentSchema`, + which is what took it off the node union `AnySchema`. `ActionSchema`, + `DetailSchema` and `CRUDDialogSchema` are unchanged and remain the union's + members. + + Removed from `@object-ui/core`: `CRUDBuilder` and the `crud()` factory. + + Authoring `crud` is now REFUSED BY NAME rather than passed or silently + ignored. `validateSchema` returns an `error` with `code: 'RETIRED_TYPE'` on + `schema.type` — at any depth, since it is what `validateChildren` recurses + with — so `assertValidSchema` throws and `isValidSchema` answers `false`. The + message names the migration: `object-grid` for the record table with its + toolbar, filters, pagination and row/batch actions, `object-form` for the + create/edit form, and `detail` for the record view. `api/schema-reference.md` + is rewritten around those shapes. + + Note on blast radius: the repository itself contains zero authored `crud` + nodes and zero registrations of the key (measured on the merge base against + the doc gate's own 659-key registry derivation, which reads `register` and + `registerLazy` alike). That is an IN-REPO zero, not an npm zero — a published + consumer that imported the `CRUDSchema` type, called `crud()` / `CRUDBuilder`, + or authored `type: 'crud'` will see a compile error or a validation error + respectively. Both are the intended, loud replacement for a shape that has + never rendered. +- 3ccd9e8: Split the static `table` column type off the rich shared `TableColumn` + (objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = + enforced holds per renderer. + + `TableColumn` is unchanged and remains the rich shape `data-table`, + `CRUDSchema` and detail-view relations honour. The static `table` renderer's + `TableSchema.columns` now declares the new narrow `StaticTableColumn` + (`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the + keys that renderer reads). The eleven keys the static renderer never read are + retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on + `TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / + `filterable` / `resizable` / `editable` / `cell` on its columns. + + Breaking for authored metadata that wrote those keys on a `type: 'table'` + node: they were silently inert before and are now refused loudly — a tsc + error on the interface (`?: never`) and a parse rejection naming the key in + `@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: + nodes that wanted the interactive behaviour move to `type: 'data-table'` + (whose columns keep the rich `TableColumn`); right-aligned columns on the + static table use `cellClassName: 'text-right'`; alternate-row styling uses + Tailwind on `className`. +- b2ea297: `ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject` + instead of being a hand-written copy (objectui#5362; maintainer ruling + 2026-08-20: the object document type belongs to the spec). + + What changes on the published type surface: + + - **Gained:** the full spec object-document surface, including the three keys + the runtime already reads but the old interface rejected as excess + properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`, + `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of + the spec document). + - **Removed:** nine members the old interface declared that no objectui + runtime code reads and the spec document does not know: `extends`, + `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling + is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`. + `ObjectTrigger` and `ObjectRelationship` remain exported unchanged. + - **Kept:** `editMode` — the one measured client-side member the runtime reads + (`recordFormNavigation` / `AppContent`) — now declared on the new + `ObjectSchemaClientExtensions` interface, which the derivation intersects. + Note the spec's strict parse rejects `editMode` on published documents + (`unrecognized_keys`); it is a client-type member only. + + Spelling settlement: `listViews` (camelCase) is canonical — `list_views` + appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in + `@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling + READ fallback for stored pre-settlement documents (that stock has never been + censused — objectstack#7917); the CRUD guide and its pinned transcription now + author the canonical spelling. +- 5b5a5c3: **Breaking for authored metadata:** `ObjectViewSchema.viewTabBar` is RETIRED + (objectui#7779, maintainer ruling B of 2026-09-06; ADR-0049 enforce-or-remove). + An `object-view` node that authors `viewTabBar` no longer validates: the parse + fails loudly on the `viewTabBar` path with the explanation in the message, and + the TS member is a `?: never` tombstone so the same document is refused at + compile time. Nothing ever read the key off the node — the tab-bar UX config + (`ViewTabBarConfig`, still exported) is the `config` prop of the `ViewTabBar` + component, composed by the host, not authored metadata. Remove the key. + + In the same change, eight `ObjectViewSchema` keys the TypeScript interface + declared and the Zod mirror never did are now ENFORCED. Until now a document + authoring any of them passed the validator unexamined through `BaseSchema`'s + passthrough while the published type invited the author to write it — declared + but not enforced (objectui#7279's `UnmirroredDeclared` reading). Each key now + admits exactly what the declaration promises and refuses a wrong-typed value at + its own path; a correctly typed document is untouched. + + | key | disposition | how | + | --- | --- | --- | + | `navigation` | mirrored | the spec's `ListViewSchema.navigation` slot by reference (`NavigationConfigSchema`, optional) | + | `searchableFields` | mirrored | the spec's `ListViewSchema.searchableFields` slot by reference (`string[]`, optional) | + | `filterableFields` | mirrored | the spec's `ListViewSchema.filterableFields` slot by reference (`string[]`, optional; the spec marks it a legacy shorthand for `userFilters.fields`) | + | `allowCreateView` | mirrored | the sibling `ViewSwitcherSchema.allowCreateView` slot by reference — the renderer forwards the value verbatim into the `view-switcher` node it composes | + | `viewActions` | mirrored | the sibling `ViewSwitcherSchema.viewActions` slot by reference, for the same reason | + | `defaultViewType` | mirrored | local literal: the declaration's seven-value union (`grid`, `kanban`, `gallery`, `calendar`, `timeline`, `gantt`, `map`) — read as `schema.defaultViewType \|\| 'grid'` | + | `defaultListView` | mirrored | local literal: `string` — read as `namedListViews?.[schema.defaultListView]` | + | `showViewSwitcher` | mirrored | local literal: `boolean` — read as `schema.showViewSwitcher === true` | + | `viewTabBar` | RETIRED | `?: never` + `retirementTombstone()` — zero reads on the node | + | `listViews` | unchanged (still unmirrored) | see below | + + **What was measured.** Every reading was taken on the `object-view` node + renderer (`packages/plugin-view/src/ObjectView.tsx`, registered by + `plugin-view/src/index.tsx`) with `schema.objectName` / `schema.layout` as the + positive controls of the same `schema.KEY` query, so each zero is a reading; + the repo-wide census of `viewTabBar` finds the key in no source file outside + `@object-ui/types` (two doc tables listed it as authorable and are corrected + here). The spec side was read through the installed pin + (`@objectstack/spec@17.2.0`, `ui` entry, 117 exported object schemas walked; + control keys `objectName` / `columns` / `navigation` / `listViews` hit): the + three spec-modelled keys are optional slots on `ListViewSchema` and + `ObjectListViewSchema`; the six local keys have no spec slot anywhere. + + **`listViews` stays unmirrored, on the ruling's own fallback clause.** The + declaration's value is the local `NamedListView` — **47 declared top-level + members**, of which the renderer reads six: `label`, `type`, `columns`, + `filter`, `sort`, `options`. It reads a seventh key off a named view, `data`, + but `data` is **not a declared member of `NamedListView` at all**: it reaches + the renderer through an `as any` cast on the named-view config in + `packages/plugin-view/src/ObjectView.tsx`, so it never was one of the declared + members a mirror would have to carry. The spec's `ViewSchema.listViews` is a + record of the STRICT `ObjectListViewSchema`, which requires `columns` and + refuses `options`, ObjectQL tuple filters and `default` — that is, it refuses + the named views this package's own README and + `content/docs/api/schema-reference.md` teach (`{ label: 'All Users' }` fails at + `columns`; `filter: [["owner", "=", "..."]]` fails at `filter.0`). Mirroring the + spec value would lose documented behaviour; mirroring the local value would + enforce **41 unread members** (47 declared, minus the 6 that are both declared + and read) into the contract — the + very thing ruling B refused for the six local keys. The key therefore stays in + the parity ledger with that measurement, pinned, until the maintainer decides its + value type. It is not papered over with `z.any()`. + + **Who is affected:** an author who wrote `viewTabBar` on an `object-view` node + (remove it), or who wrote a wrong-typed value for one of the eight keys — e.g. + `defaultViewType: 'tree'` (host-composition-only, objectui#5321), + `navigation: 'page'` (write `navigation: { mode: 'page' }`), + `searchableFields: 'name'` (write an array), `viewActions: 'share'` (write + `[{ type: 'share' }]`). Such documents used to pass validation and render with + the key ignored; they now fail at the key with the reason. + + **Who is NOT affected:** every correctly typed document, and every document + that never wrote these keys — `absent` stays valid on all nine. No renderer + changed. The parity ledger (`zod-mirror-parity.test.ts`) records the move: + `UnmirroredDeclared` 14 entries / 96 keys to 14 / 87, the `ObjectViewSchema` + entry re-derived into the SPEC-DERIVED half because the mirror now references + the spec in code. + + Graded `minor`, not `patch`: this narrows the accepted input set, which is + breaking for any author who wrote the tolerated spellings. It is not `major` + per this repo's fixed-group convention (objectui's own breaking changes ship as + `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略, + mechanically enforced by `scripts/check-changeset-no-major.mjs`). +- 258d264: `QueryParams` (`@object-ui/types`) no longer carries the `[key: string]: any` + index signature; its key set is now exactly the nine declared `$`-prefixed + members — `$select`, `$filter`, `$orderby`, `$skip`, `$top`, `$expand`, + `$search`, `$searchFields`, `$count` (objectui#7497). + + **What stops type-checking that type-checked before.** Any object literal + assigned or passed as a `QueryParams` that carries a key outside those nine — + the unprefixed spellings the readers silently dropped (`{ filter }` for + `$filter`, `{ limit }` for `$top`, `{ options: { $top } }`), and any + `$`-prefixed name that is not declared (`{ $limit }`). Reading an undeclared + key off a `QueryParams` value (`params.filter`) is refused too. A published + guide had taught `adapter.find('contacts', { filter: { active: true } })` and + asserted the client saw that `filter`: it compiled, `convertQueryParams` + dropped the key, and the assertion could never pass. That literal is now a + compile error at the call site. + + **What does not change.** Every reader in this repository — + `convertQueryParams` and `rawFindWithPopulate` in `@object-ui/data-objectstack`, + `queryParamsToRecord` in `@object-ui/core`, `ValueDataSource.find` — reads only + declared members, so nothing the runtime honoured is refused. A + `Record<string, unknown>` or `Record<string, any>` VALUE (the shape + `@objectstack/spec`'s `ViewData.params` parses to) still passes, as does a + spread of one beside `$top` / `$skip`; a type assertion (`{ limit: 1 } as + QueryParams`) still compiles because assertions skip excess-property checks, + which is why `object-ui/no-unprefixed-query-params` keeps its typed cases. + + **Why `minor` and not `patch` or `major`.** Narrowing the accepted set of a + published contract is a breaking change for any downstream caller that relied + on the extra keys. The census behind the grade: across this repository's + packages, apps, examples, e2e and scripts trees (4,362 files), zero `find` / + `findOne` call sites or `QueryParams` literals carry a non-`$` key, and no + adapter reads one — the signature carried nothing but typos. This repository + records its own breaking changes as `minor` with the break spelled out; `major` + is reserved for following `@objectstack` across a major (see AGENTS.md, version + alignment). A `patch` would be wrong: this is a deliberate narrowing, not a + fix inside the accepted set. + + Migration for a downstream caller that did pass an extra key: if an adapter of + yours reads it, declare it on your own params type and widen at your adapter's + boundary; if nothing reads it, it was already being dropped — delete it. +- 78cbdb5: Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer + implemented (objectui#5489). + + `packages/types/src/theme.ts` declared a theme-manager **component** carrying + `themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and + `storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching + Zod object as a member of `ThemeUnionSchema` and therefore of + `AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no + `ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`, + and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS` + (`packages/components/src/renderers/placeholders.tsx`), so it did not even + resolve to a placeholder — a page declaring one got the registry's "Unknown + component type" panel (OBJUI-001) instead of a theme manager. Declared-but- + unenforced, removed under the maintainer ruling of 2026-08-21 on + objectstack#10485 (option B). + + Removed from the published surface: the `ThemeComponentSchema` type + (`@object-ui/types`), the `ThemeComponentSchema` Zod object + (`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the + `'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling + `type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than + accepted and then rendered as an error panel, which is pinned by a test. + + **The theme system is unchanged.** `Theme` (the spec's authoring theme + document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine` + (`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and + untouched — the same ruling retains them explicitly. Author a theme as a + document handed to `ThemeProvider`; that path never went through the removed + component kind. +- b7543a9: Retire `ThemeSwitcherSchema` (`type: 'theme-switcher'`) and + `ThemePreviewSchema` (`type: 'theme-preview'`) — the two remaining theme + component kinds, which no renderer implemented — together with + `ThemeUnionSchema`, the union that after objectui#5489 held only these two + members (objectui#5647). + + `packages/types/src/theme.ts` declared a theme-switcher control (`variant`, + `showMode`, `showThemes`, `lightIcon`, `darkIcon`) and a theme-preview panel + (`showColors`, `showTypography`, `showComponents`), and + `packages/types/src/zod/theme.zod.ts` published the matching Zod objects as + the two members of `ThemeUnionSchema` and therefore of `AnyComponentSchema`. + Nothing rendered either: neither literal appears at any + `ComponentRegistry.register(...)` / `registerLazy(...)` site in + `packages/*/src` (202 registered keys enumerated; positive control on the same + pipeline: `tooltip` → 1), nor in `PROTOCOL_COMPONENTS` / + `PALETTE_PLACEHOLDER_BLOCKS` + (`packages/components/src/renderers/placeholders.tsx`), and no fixture + declares either kind (control: `"type": "form"` → 81) — so a page declaring + one got the registry's "Unknown component type" panel (OBJUI-001), never a + switcher or a preview. Declared-but-unenforced, removed under the 2026-08-21 + maintainer ruling (option B) on objectstack#10485, extended to these siblings + by inheritance on identical evidence (objectui#5647). + + Removed from the published surface: the `ThemeSwitcherSchema` / + `ThemePreviewSchema` types (`@object-ui/types`), the matching Zod objects and + `ThemeUnionSchema` (`@object-ui/types/zod`), and the `ThemeSwitcherSchemaType` + / `ThemePreviewSchemaType` inference aliases. `zod/theme.zod.ts` now exports + nothing and stands as the tombstone module. A schema spelling + `type: 'theme-switcher'` or `type: 'theme-preview'` is now REFUSED by + `AnyComponentSchema.safeParse` rather than accepted and then rendered as an + error panel, which is pinned by a test. + + **The theme system is unchanged.** `Theme` (the theme document vocabulary, + owned by `@object-ui/types` since objectui#5716), `ThemeEngine` + (`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are retained and + untouched — the rulings retain them explicitly. Author a theme as a document + handed to `ThemeProvider`; that path never went through the removed component + kinds. +- c9327c9: Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. + + Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): + + - `Typography` — the shape lives on as the inline `Theme['typography']` member. + - `BorderRadius` — lives on as inline `Theme['borderRadius']`. + - `Shadow` — lives on as inline `Theme['shadows']`. + - `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. + + Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). + + The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. + + `@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). +- 920165d: Remove the six retired `@objectstack/spec/ui` theme-schema re-exports from `@object-ui/types/zod` — `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`, `ThemeDefinitionSchema` (the spec's `ThemeSchema`) — plus their `…SchemaType` inference helpers, and the `theme` / `mode` props of `ThemePreviewSchema` (zod and interface) that consumed them. + + objectstack#10485 (ADR-0049 enforce-or-remove, PR objectstack#10695) retired the spec's whole `ui/theme.zod.ts` module, and the maintainer's ruling on objectstack#10856 (Options A + C) has objectui remove the dangling imports first so the Console Pin Gate can build objectui against the framework tree; restoring the spec exports (Option B) was explicitly not taken. Breaking in effect for anyone importing those six names from `@object-ui/types/zod`: there is no replacement — the validators retired upstream with no successor. The theme TYPE surface (`Theme`, `ThemeMode`, `ColorPalette`, … re-exported from `@object-ui/types`) and the ThemeEngine/ThemeProvider runtime are unchanged. +- 4d73b07: The zod `BaseSchema` mirror now accepts everything its TypeScript declaration + declares — five keys had drifted narrower (objectui#4605). + + `@object-ui/types/zod` is a published runtime validator hand-written to mirror the + `BaseSchema` interface. As the interface widened, the mirror did not, so five keys + refused at parse time a spelling the published types invite and the renderer + implements — "declared = enforced" inverted. `.passthrough()` rescued none of them: + passthrough admits UNDECLARED keys, and all five are explicitly declared, so the + narrow declaration won. + + Measured against the unmodified mirror before the change, these were the refusals: + + | key | authored input | old mirror said | + |---|---|---| + | `visible` | `'${data.status === "open"}'` | `expected boolean, received string` | + | `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` | + | `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` | + | `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` | + | `description` | `{ en: 'The record owner' }` | `expected string, received object` | + + `visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts, + no wider. `ariaLabel` takes the KEYED reference through a new exported + `KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema` + BY REFERENCE, so a change to the spec's label contract is picked up rather than + re-typed. Every spelling that parsed before still parses. + + The two i18n vocabularies are kept apart rather than merged into "some object". + `label`/`description` are the spec's INLINE locale map (resolved by + `resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by + `resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render + an EMPTY aria-label). Widening both slots to accept either shape would have + reproduced objectui#4167's confusability hazard inside the validator that exists to + catch it, so each slot admits only its own vocabulary and both cross pairings are + pinned as rejections. + + The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own + `.shape` and compares each key against the declaration, so the next widening of + `base.ts` that forgets this file turns it red with no list to maintain. It reads + `.shape` and not `keyof z.input<…>` because that spelling was measured vacuous — + `.passthrough()` collapses the inferred key union to bare `string`, and a pin written + over it resolved `never` while five keys were demonstrably narrow. Two guards pin the + derivation against both degenerations (`never` and `string`). + +### Patch Changes + +- 06a8af5: `QueryParams.$filter` now declares both shapes the data sources actually accept — the + MongoDB-style field-keyed record, or a `FilterArray`, the ObjectQL AST sugar bound from + `@objectstack/spec/data` (objectui#3909). + + **Nothing is narrowed and no accepted value changes.** `Record<string, any>` already + accepted arrays structurally — they satisfy its string index — so the union documents + shapes that were always legal rather than admitting new ones. Measured both ways under + `tsc --strict`: all five inputs `translateFilterToAST` enumerates assign to the old and + new declarations alike, and both reject a bare number and a bare string identically. A + downstream `turbo run build` over all 43 dependent packages is green, which is the + evidence a published type change breaks no consumer. + + The harm was entirely on the type face, and it was two-sided. The declaration blocked + nothing while describing one legal shape as though it were the only one — objectui#3831 + is what that cost, a rule array accepted by a `Record<string, any>` slot, object-spread + flattened to `{"0": {...}}`, types green, and the query filtering on a column literally + named `0`. And someone writing a new consumer would read the type and its record-only + `@example`, conclude the array path was illegal, and add a tolerant conversion for it — + the "widen the consumer to tolerate the producer" shape AGENTS.md #0.1 forbids. Two + producers have fed arrays through this slot all along: `plugin-list`'s + `buildEffectiveFilter` (grid and export) and `plugin-view`'s `ObjectView` (calendar / + kanban / gallery / timeline). The runtime was right; the declaration was narrow. + + The array half is **bound** to the spec's `FilterArray` rather than restated locally, so + it cannot fork from the vocabulary the servers parse — the same failure two hand-written + operator lists had in objectui#3948. The doc comment names `translateFilterToAST` as the + authoritative accepted set instead of carrying a second list to drift from. + + `@object-ui/fields` drops the local cast this defect forced. PR objectui#3908 wrote + `filter as Record<string, any>` at one assignment in `useRecordQuery`, deliberately, as + debt rather than widening the shared type. `hasFilter` is now a type predicate narrowing + to the `$filter` slot's own type, so the assignment needs no cast and the guard cannot + drift from the declaration it guards. Type-only throughout; no runtime behaviour changes. +- 460575f: `exportOptions`: the alignment claim is now checked against the installed spec, + not asserted in prose + + `ListViewExportOptions` mirrors `@objectstack/spec`'s + `ListViewSchema.exportOptions` object branch. The comment above it explained + that the five keys were restated rather than derived "because objectui still + pins `@objectstack/spec@17.0.0-rc.6`, whose `ListView.exportOptions` is the + LEGACY bare format array". + + That is no longer true. objectui installs `@objectstack/spec@17.2.0`, which + carries the object form: the bare format array lifts to `{ formats: [...] }` at + parse and `'pdf'` has left the enum. Nobody edited the comment; the dependency + moved underneath it. This is the second time this particular comment has gone + false — objectui#4535 was filed for the first — and both times the mechanism was + the same: a prose claim about another package's shape, with nothing that fails + when that shape changes. + + So the reason is corrected and, more to the point, it stops being load-bearing. + A new `export-options-spec-parity.test.ts` reads the object branch out of the + spec package that is actually installed, at test time, and asserts against it: + the key set, the format enum (`'pdf'` absent from both sides), upstream + strictness, the migration prescription the `'pdf'` refusal carries, and the + parse-time array lift. The key set is projected from `keyof + ListViewExportOptions` through an exhaustive `Record`, so a local key added or + dropped fails to compile rather than passing a comparison against a hand-copied + list. + + The mirror itself is unchanged, and stays a mirror for a measured reason the + test now pins: `ListViewExportOptionsSchema` is internal to the spec bundle and + is not one of the package's public exports, so there is nothing to import. Only + the enclosing `ListViewSchema` is exported, and its `exportOptions` is a + two-branch union whose inferred type is not this interface. When upstream + exports the symbol, the test says so by failing, and the mirror can go. + + Types are unchanged — no export added or removed, no key or union altered — so + this is a patch. The shape change was the earlier minor that introduced + `ListViewExportOptions`. +- 64d624d: `RuntimeWidgetManifest` / `RuntimeWidgetSource` document the spec's retired + `WidgetManifest` / `WidgetSource` as HISTORY, instead of describing them as a + live schema (objectui#5213). + + Both JSDoc blocks were written while `@objectstack/spec/ui` still exported a + field-widget-plugin `WidgetManifest` and a `WidgetSource` union — they said the + local types were "renamed off the spec's name", in the present tense, and the + manifest block enumerated the spec shape key by key (`fieldTypes`, `category`, + `lifecycle`, `events`, `properties`, `implementation`, `screenshots`, `license`, + `aria`, `performance`). Protocol 17 retired that entire widget-registration + vocabulary under ADR-0049 enforce-or-remove (objectstack#5055): the installed + `@objectstack/spec` 17.2.0 exports none of those names, and its own tombstone + records why there is nothing to migrate — no schema ever declared a carrier key + of a widget shape, so the record is the D3 `SemanticMigration` + `ui-widget-i18n-family-retired` plus `ui/WidgetManifest` in + `RETIRED_DEFS_BY_MAJOR` for major 17. + + A per-key description of a schema that no longer exists is the ADR-0033 failure: + an AI author reads a published docblock as present-tense fact and builds on it. + The enumeration is dropped rather than re-dated, and the blocks now say what is + true today — the bare names are owned by NOBODY, `RuntimeWidgetManifest` is + objectui's only widget-registration contract, and the `Runtime` prefix is kept + BY CHOICE (objectstack#4988's precedent: a freed word is not a reason to spend a + second breaking rename taking it back; the unlock is recorded, not taken, in + objectui#4164). The `inline` collision that made the `WidgetSource` rename + urgent is kept, in the past tense, because it is the reason the prefix exists. + Both blocks now point at the live assertion instead of restating it: the "the + spec no longer owns" rows in + `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`, which are what + goes red if the spec ever re-publishes either name. + + Comments only. No type, signature, member or test changed, and the parity test + that owns this fact was already correct — it moved both rows to its "spec no + longer owns" table on the 17.0.0-rc.6 bump. + + Declared a `patch` for `@object-ui/types` alone because the emit was measured, + not assumed: both blocks sit on EXPORTED declarations, so they publish. Rebuilt + from a cleared `dist` and `tsconfig.tsbuildinfo` on both sides and compared by + SHA-256 — `dist/widget.d.ts` `bb4f2fd702cac02a…` -> `db1d5fbd53d305f5…` + (a consumer reads this text on hover and in the API docs), while + `dist/widget.js` is byte-identical across the rebuild + (`a3de34c54213a269…` both sides), so nothing runtime moved. +- 40c479a: Static-table retirement tombstones now refuse with their remediation text + (objectui#6105). + + The nine ADR-0049 tombstones on `StaticTableColumnSchema` (`minWidth`, `align`, + `fixed`, `type`, `sortable`, `filterable`, `resizable`, `editable`, `cell`) + already refused an authored value at the right path — but the carefully written + `.describe()` string never reached the author, because `.describe()` is schema + METADATA. What an author saw was zod's own `Invalid input: expected never, + received string`: which key is wrong, nothing about why it was retired or what + to write instead. Loud refusal is the ruled outcome; half its payload was being + dropped. + + One shared mechanism carries the text into both channels. `retirementTombstone()` + (`zod/tombstone.zod.ts`) takes the guidance string ONCE and writes it to both + `z.never({ error })` — the parse-time issue message — and `.describe()` — the + generated JSON-Schema and docs surface, unchanged. One string, so the two cannot + drift. + + Authoring `align: 'right'` on a static table column now reports `RETIRED + (objectui#5474) — never read by the static table; use data-table, or a + cellClassName like text-right`. + + The accept set is untouched: same `success`, same issue `path`, same issue `code` + (`invalid_type`) for all nine, measured member-by-member before and after. Only + the message differs. +- 905b21f: The three view handler keys are declared as EVENT NAMES, not callbacks. + + `ViewSwitcherSchema.onViewChange`, `FilterUISchema.onChange` and + `SortUISchema.onChange` were described as "change callback" on both the zod + mirror and the TS interface. They are not callbacks: the string an author + writes is the NAME of a `CustomEvent` the renderer dispatches on `window` — + `new CustomEvent(schema.onViewChange, { detail: { view } })` and its two + siblings. + + **What an author feels.** Nothing they write breaks — the type is still + `string`, so no accept set moves and no existing document changes verdict. + What changes is the two places this contract is published — the zod mirror's + `describe()` text and the TS JSDoc — which now tell them what the string is + FOR, and what to listen for: + + ```json + { "type": "sort-ui", "fields": [{ "field": "name" }], "onChange": "myapp:sort-changed" } + ``` + + ```js + window.addEventListener('myapp:sort-changed', (e) => e.detail.sort); + ``` + + Previously "Sort change callback" invited the two readings the runtime does not + support — a function (unwritable in JSON) or a handler expression (dropped at + runtime) — with no hint that the working form is an event name. + + The correction also protects the capability. A handler-key census that buckets + by declared TYPE cannot tell an event name from the unsupported + handler-expression dialect, and on that reading these three had been swept in + for retirement, which would have deleted working behaviour. A new pin + (`plugin-view/src/__tests__/handlerEventNameLiveness.6124.test.tsx`) now holds + both halves — that each key is DECLARED on the authorable surface, and that the + authored string reaches `new CustomEvent(...)`. +- 6f81384: `ObjectViewSchema`'s `table` and `form` slots now ship the members they promise + (objectui#6269). Both were declared by deriving from the schema they document — `table?: + Partial<Omit<ObjectGridSchema, 'type' | 'objectName'>>`, `form?: Partial<Omit<ObjectFormSchema, + 'type' | 'objectName' | 'mode'>>` — and both derived types declared **zero** properties. + + `Omit<T, K>` is `Pick<T, Exclude<keyof T, K>>`, and `keyof T` on a type carrying a string index + signature is `string | number` — the literal member names are absorbed. `ObjectGridSchema` and + `ObjectFormSchema` both inherit `BaseSchema`'s `[key: string]: any` (objectui#5155), so each + `Pick` rebuilt a type holding the index signature and none of the named members. Measured + through the TypeScript checker: `ObjectGridSchema` 61 members, the `Omit` of it 0; + `ObjectFormSchema` 67, the `Omit` of it 0. This is objectui#6151's collapse in *property* + position — #6151's guard walks the `LayoutSchema` union and cannot see properties on + `ObjectViewSchema`. + + Nothing errored, because the index signature answered every key as `any`. The visible costs + were the ones only a reader of the declaration meets: `table: { colunms: 3 }` type-checked, + `table: { pageSize: 'ten' }` type-checked, and editor completion inside `table: { … }` offered + nothing at all for a slot documented as "inherits from ObjectGridSchema". + + Each `Omit` is now a `Partial<Pick<…>>` over an explicit key list — 59 keys for `table`, 64 for + `form`, i.e. every declared member minus the identity keys the view itself fixes. `Pick` with + literal keys never computes `keyof T`, so it cannot collapse the same way. The key lists are + pinned against silent drift by `packages/types/src/__tests__/object-view-slot-key-lists.test.ts`, + which recomputes each source schema's declared members through the TypeScript checker and + requires set equality; a member added to `ObjectGridSchema` and not to the list turns it red. + + **Tightening, deliberately.** Restoring named members re-enables excess-property checks on + object literals assigned into these two slots, so a misspelled key there is now an error + instead of silently doing nothing. That is the intent of the fix. The slots' member *types* are + unchanged — every key that resolved to a real declared type before still does. + + The `Pick` lists exist only because `BaseSchema` carries a root string index signature. When an + objectui#5155 phase removes it, `Omit` stops collapsing and the lists (plus their pin) become + removable; the pin's own comment records the condition, and one of its assertions is the + tripwire that will notice. +- dddb942: Delete the dead `metadata-admin/previews/object-fields-bridge.ts` module, and the three + prose references that still described it as wired. + + The module exported `bridgeFromDraft`, `commitToDraft` and `FieldsBridgeResult` and had + **zero importers** — re-measured on the merged base, not inherited from the filing. Nothing + in the repository could reach it either: `@object-ui/app-shell`'s `exports` map declares + only `.` and `./styles.css`, so the file was not addressable as a deep import even from + outside the workspace. + + Removing it is not the whole change. Three comments — in `types/src/designer.ts`, `types`' + `designer-field-types.test.ts` (twice) and `fields`' `richtext-cell-renderer-5452.test.tsx` + — cited the bridge as a live corroborating source. Left behind, they would have swapped + dead code for false documentation: three in-repo pointers telling a future reader that this + bridge mediates between the framework field record and `FieldDesigner`, and nothing telling + them it is unreachable. The two that named it as the consumer deriving an editable-subset + check from `DESIGNER_FIELD_TYPES` now name `MetadataFieldsPage`, which does exactly that + with the same idiom and the same `objectui#3017` anchor. The third cited the bridge's + `richtext` → `html` mapping as one of three corroborations that `richtext` stores HTML; the + other two (the showcase seed and the field-type decision tree) are live and carry the point + on their own, so that clause is dropped rather than repointed. + + No behaviour changes: nothing imported the module, so there is nothing to migrate. +- c8ea8af: `PartialSchema<T>` is pinned as collapsed, and its doc comment now says so + (objectui#6397). Nothing about the type changes — the declaration is deliberately + left exactly as written. + + The alias promises "all properties optional except the type" and does not deliver + it. Every instantiation declares exactly ONE property, `type`, and carries a live + `[key: string]: any`, so it accepts any key at `any`. Measured through the + TypeScript checker against the emitted `index.d.ts` — the same instrument that + produced objectui#6269's 61 -> 0 reading: + + PartialSchema<ObjectGridSchema> -> 1 declared property: type (source: 61) + PartialSchema<ObjectFormSchema> -> 1 declared property: type (source: 67) + PartialSchema<ObjectViewSchema> -> 1 declared property: type (source: 42) + PartialSchema<ButtonSchema> -> 1 declared property: type (source: 27) + + `Omit<T, K>` is `Pick<T, Exclude<keyof T, K>>`, and `keyof T` on a type carrying a + string index signature is `string | number` — the literal member names are + absorbed. Every `T extends BaseSchema` inherits `BaseSchema`'s `[key: string]: any` + (objectui#5155), so `Partial<Omit<T, 'type'>>` rebuilds a type holding the index + signature and none of the named members. This is objectui#6151's collapse + (heritage clause) and objectui#6269's (property position) in a third position: a + generic mapped-type alias, which is why neither of their guards sees it — #6151's + walks the `LayoutSchema` union, #6269's reads `ObjectViewSchema`'s two slots. + + **Why a pin and not a repair or a retirement.** Triage ruled on 2026-08-25 that + retiring the alias is a removal of a published export of `@object-ui/types` — a + breaking removal of published capability, which sits on the human floor — and that + escalating it today would spend the maintainer's attention on a question + objectui#5155 is expected to moot. Repair in place is unavailable: `T` is generic, + so there is no literal key list to `Pick` the way #6269 could for its two concrete + schemas, and every generic re-spelling collapses for the same `keyof T` reason. + Once #5155 removes the root index signature, the alias starts working as written + with no edit at all. What this ships is the removal of the one impermissible + state — *declared, published, collapsed, and unpinned*. + + No runtime code, no type declaration and no accepted value changes; a consumer's + `PartialSchema<X>` means exactly what it meant before. Declared `patch` rather + than as a no-release so the doc-comment warning actually reaches the published + `.d.ts` a consumer reads — that warning is the deliverable half of the card. +- 45a9aeb: `ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published + declaration faces, naming `dependenciesField` as the canonical spelling + (objectui#6470). Nothing is removed and nothing is rejected that was accepted + before. + + **What the two spellings were.** `ObjectGanttSchema` declares both, and + `getGanttConfig`'s flat branch reads them with a `||`: + `dependenciesField: schema.dependenciesField || schema.dependencyField`. + `dependenciesField` is the spec's key (`@objectstack/spec` + `GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO + spec counterpart — zero occurrences across `packages/spec/src`, measured against + a live positive control on the plural. Until objectui#6051 declared the plural, + the singular was the ONLY dependencies spelling this interface carried, so for + the whole time the alias existed the published type taught the non-spec key and + hid the canonical one. + + **What was missing was the ranking, not the behaviour.** The two were declared as + equals: nothing on either face said which one to author, so a reader — including + an AI writing metadata, which is the reader this project optimises for — had a + coin flip between a spec key and pre-spec vocabulary. The marker turns that coin + flip into a fact the type itself carries, and the zod mirror's description makes + it readable at runtime as well as in an editor. + + This adopts the idiom already ruled for this exact shape rather than inventing a + second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry + `/** @deprecated legacy alias for the spec's X */` plus + `.describe('Deprecated alias for X')`, and `dependencyField` now reads the same + way. + + **⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the + renderer's `||` — would break every author who wrote the singular and narrow the + accept set of a published surface. That is a maintainer decision on a future + enforce-or-remove card once the deprecation has sat a release, and it is + explicitly excluded here. Two pins hold the line in both directions: + `packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts` + fails if the marker goes missing AND if the alias stops being declared or + accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx` + fails if the `||` limb is dropped — the two spellings must keep resolving to the + same config, with the canonical one winning when both carry a value. + + `packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the + singular; it was the only in-repo site that did, and it now authors the plural + with the alias named as legacy. No runtime code, fixture, example app or catalog + schema authored it. +- f20dcf0: `ObjectGanttSchema`'s flattened gantt face documents `colorField`, `parentField`, + `tooltipFields` and `quickFilters` in its own words, instead of pointing at a type that + says nothing about them (objectui#6547). + + Those four members carried a bare `See {@link GanttConfig}.` pointer. `GanttConfig` is + `SpecGanttConfig & { … }`, and all four arrive from the **spec** half: the local half of + that intersection declares exactly ten top-level members (`timeSegments`, `lockField`, + `objectField`, `summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, + `dependencyTypes`, `timeZone`, `exportFileName`, `interactions`) and none of these four is + among them. `SpecGanttConfig` is `z.input<typeof GanttConfigSchema>`, and the spec's + emitted `.d.ts` carries no per-member JSDoc — its 19 members are bare `z.ZodOptional` + entries under one type-level "Gantt Settings" docblock. So the pointer was not merely + unhelpful, it was misdirecting: a reader who followed it landed on a type documenting + nothing about the key and concluded the key was undocumented. + + Verified on the built artifact rather than the source: all four bare pointers were present + in `dist/objectql.d.ts` before this change and none is after, so the defect was on the + published surface and the repair reaches it. + + **Prose only — the published shape is unchanged, and that is the point.** The natural + repair, writing the prose onto `GanttConfig`, means re-declaring these members inside the + intersection; PR objectui#6546 measured that as a widened published surface on the built + `dist/index.d.ts` and it was rejected there. So the shape was measured, not asserted: + `ObjectGanttSchema`'s 45 members and their checker-resolved types are byte-identical + before and after, and the built face's 51 declaration lines diff clean with comments + stripped. Only comment bytes moved. + + The seven **member-qualified** pointers (`See {@link GanttConfig.borderColorField}`, + `.lockField`, `.summaryExtent`, `.defaultCollapsedDepth`, `.timeSegments`, + `.interactions`, `.exportFileName`) name members of that ten-member local half, resolve to + real prose, and are deliberately left alone — the defect is the bare form, and a substring + search for `{@link GanttConfig` hits both. + + The prose is taken from the renderer's live read sites in `plugin-gantt`, not invented: + `colorField`'s status/state/priority/severity fallback chain and the platform default + blue, `parentField`'s unknown-id-renders-as-root rule, `tooltipFields`' drop-empty-rows + behaviour (which is what lets a mixed-object tree list the union of every level's fields) + and its replacement of the default date · duration · progress tooltip line, and + `quickFilters`' schema-resolved option domains. No behaviour change, no runtime code. +- d6ceb8d: Implement `ListColumn.wrap` — a column that says it wraps now actually wraps + (objectui#6650, maintainer ruling 2026-09-02, Option B). + + `@objectstack/spec` declares `ListColumn.wrap` and describes it to authors as + "Allow text wrapping", and `packages/plugin-grid/README.md` shows it in its + authored-column example. No renderer anywhere implemented it. Long cell text + stayed clipped to one line, with no error, no warning and no feedback of any + kind — a promise made at authoring time and silently broken at render time. + + **What changes.** A `data-table` column with `wrap: true` renders its cell body + `whitespace-normal break-words` instead of the default `truncate`, so long text + flows onto further lines and the row grows to fit. `ObjectGrid.generateColumns()` + forwards the authored key into the column slot, and `TableColumn` declares it, so + the key is honoured whether it is authored on a spec list view or directly on a + `data-table` node. `ObjectGrid`'s own `LinkCell` — the record link that column one + of almost every grid renders through — honours it too, because its own `truncate` + would otherwise clamp the text back to one line inside a cell body that was + willing to wrap. `@object-ui/types`' zod mirror carries the key as well; without + that the non-strict mirror would silently strip an authored `wrap` on the parse + road, which is the same "renderer honours what the declaration refuses" gap + objectui#6424 and objectui#6425 closed for their keys. + + **Nothing changes for anyone not authoring the key.** `wrap` absent or `false` + renders exactly what shipped before, pinned as a control rather than assumed, and + the link cell's default markup is byte-identical to what it was. + + **Precedence, where the two keys conflict.** `fitContent` WINS over `wrap`. A fit + column is `width:1%` with no `minWidth`/`maxWidth` clamp, so the auto table layout + sizes it from its content alone, and `whitespace-nowrap` is what holds that + content's min-content width at its max-content width — one line. Drop nowrap and + min-content falls back to the longest word, so honouring `wrap` there does not + wrap the column, it collapses it: measured in Chromium with the cell shape + reproduced exactly, 463.9px wide on one line with nowrap against 70.9px wide over + ten lines without it — 6.5x narrower and 5.9x taller. The keys do not compose, and + the one that yields is the one whose outcome nobody asked for. + + The static `table` renderer does not gain the key: `StaticTableColumn` tombstones + it, so an author who writes `wrap` there is refused loudly at parse time with the + remedy named, rather than having it silently stripped. +- adb2a86: The standalone runner renders `AppAction.items` from its declared type only, which + makes `AppActionSchema.onClick`'s retirement message true again (objectui#6854, + maintainer ruling of 2026-09-05, option B2). + + `AppAction.items` is `AppMenuItem[]`, and the zod mirror parses it with the legacy + eight-member `MenuItemSchema` — neither declares `onClick` or `shortcut`. + `LayoutRenderer` reached both through `as any`, past the type it was handed, and + that left three mutually exclusive signals about the same key: the TypeScript face + said `?: never`, the validator's refusal said "no renderer reads this key, so + nothing could ever run it", and a renderer read it. An agent or a reader could + believe any one of the three and be contradicted by the other two. + + **No published accept set moves and no exported symbol changes.** `AppAction.items` + is NOT re-typed (the alternative was measured and refused: it would have carried a + breaking migration for `path` / `href` / `badge` / `type` and the divider spelling, + for a capability with no measured consumer). The refusal message itself is unchanged + — it is shared by 22 other retired handler keys, and deleting the cast is what makes + its sentence true rather than restating it. + + - `@object-ui/runner`: `LayoutRenderer` no longer reads `onClick` or `shortcut` on a + `type: 'user'` action's `items`. The `onClick` branch was an empty body and could + never run a JSON value; the `shortcut` read rendered a `DropdownMenuShortcut` from + a key the mirror strips in silence, so no validated document could reach it. A + census of every JSON and TypeScript app document in this repository found zero + authors of either key (positive controls recorded on the issue). + - `@object-ui/types`: the rationale comments on `AppAction.onClick` and + `AppActionSchema.onClick` said "nothing reads `AppComponentSchema.actions[]`". + That was false — the runner renders both the `'button'` and the `'user'` arm. + Corrected to what was measured: `actions[]` is read, `onClick` is not. + + Whether `shortcut` should become authorable on `AppAction.items` is a separate + contract question and is filed on its own. +- 8063bcb: The remaining eleven ADR-0049 tombstones now refuse with their remediation text + (objectui#6931). + + objectui#6105 converted nine tombstones on `StaticTableColumnSchema` to + `retirementTombstone()`, which writes a guidance string ONCE into both + author-facing channels — `z.never({ error })` (the parse-time issue message) + and `.describe()` (generated JSON-Schema and docs). Eleven declarations were + left on the bare `z.never().optional().describe(...)` spelling and kept + emitting zod's own `Invalid input: expected never, received string`: which key + is wrong, nothing about why it was retired or what to write instead. + + Five of those eleven sat on `StaticTableColumnSchema` itself, so an author of a + static-table column read guidance on nine keys and zod's generic on five — a + shape that teaches the message means something and then withholds it. This + converts all eleven: + + - `StaticTableColumnSchema`: `headerIcon`, `fitContent` (objectui#6424), + `format`, `options`, `currency` (objectui#6425) + - `TableSchema`: `hoverable`, `striped` (objectui#5474) + - `TimelineSchema`: `timeScale` (objectui#6355) + - `MenuItemSchema`, both union arms: `type` (objectui#6523) + - `ActionSchema`: `confirm` (objectui#4314) — the key that ESTABLISHED this + convention, and the last one still answering with zod's generic message + + Authoring `timeScale: 'day'` on a timeline now reports `RETIRED + (objectui#6355) — author scale instead`; authoring the structured `confirm` + object on an action now reports `RETIRED (objectui#4314) — author confirmText + instead`. + + The accept set is untouched. For every converted member, plus the nine already + converted and six live-value controls, `safeParse` reports the same `success`, + the same issue `path`, the same issue `code` (`invalid_type`) and the same + `expected` (`never`) before and after — only the message differs. Every + `.description` on the five affected schemas (132 members) is byte-identical: + `retirementTombstone()` passes the same string `.describe()` already carried. + + One nuance for `MenuItemSchema`, which is a union: its top-level issue is + still zod's own `invalid_union` / `Invalid input` at path `[]`, and the + converted guidance rides the per-arm issues underneath it. That is a property + of the union rather than of the tombstone, and it is pinned in the tests so the + unchanged top-level message is not misread later as a failed conversion. +- b74a859: Declare `wrapperClass` on `CheckboxSchema`, on both faces (objectui#6938 — the + residue of that card; its `context-menu` half landed with objectui#6939 group 1). + + `packages/components/src/renderers/form/checkbox.tsx:36` reads + `cn("flex items-center space-x-2", schema.wrapperClass)` — classes on the wrapper + `div` around the box and its label — and neither the TypeScript interface in + `packages/types/src/form.ts` nor the zod mirror in `zod/form.zod.ts` declared the + key. It compiled through `BaseSchema`'s index signature and parsed through + `.passthrough()`, admitted unexamined. The same key, on the same class of read, is + declared on `FileUploadSchema` and `FilterBuilderSchema` (objectui#6150); the + checkbox was left out only because its doc page's schema block is a six-line + summary. + + **patch, not minor: the accept set only widens toward what already renders.** The + key is optional; no document that validated before stops validating. In the value + dimension the mirror now REFUSES a non-string `wrapperClass` it used to admit + unexamined — enforcement of the declared type, not a new capability. +- 77cb489: Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional, + and a refinement requires that at least one of `data`, `staticData`, + `objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 — + this is one of the eight groups on that card, dispatched as its own PR per the + ruling). + + Both renderers resolve their records from one of three keys, in this order — + `getDataConfig` in `plugin-map/src/ObjectMap.tsx` and + `plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then + `objectName`. Both mirrors required `objectName` alone, so a document authored + on `staticData` drew correctly and was refused by `safeValidateSchema` — six + catalog entries, three per component. + + - **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the + mirror and on the TypeScript twin in the same stroke, and each member ends in + `requireRecordSource`, whose issue sits at the root path, carries + `params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author + can supply. + - **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the + spelling `object-map` already used): it is the FIRST key that resolver reads + and was undeclared on both faces, which would have left the refinement naming + a key the validator had never heard of. + + **patch, not minor: the accept set only widens toward what already renders.** + Every document that validated before still validates — `objectName` alone, + including an empty one, still parses, because presence is `!== undefined` and + not the renderer's truthiness. The one shape the refinement refuses (none of + the three) was refused before too, when `objectName` was required. Documents + the renderers already draw start validating. +- bfaa158: Repair the `tooltip` and `context-menu` mirrors: declare the keys their renderers + actually read, and stop requiring the `children` neither of them reads + (objectui#6939, maintainer ruling recorded 2026-09-02 — this is one of the eight + groups on that card, dispatched as its own PR per the ruling). + + Both members demanded `children` and omitted keys the renderer reads first, so + `safeValidateSchema` refused two catalog entries that draw correctly: + + - **`tooltip`** now declares `trigger`, and `content` / `body` as the two halves of + one read (`renderers/overlay/tooltip.tsx:28,31` — `renderChildren(schema.trigger)` + and `schema.content || renderChildren(schema.body)`). The registration's own + `inputs` list `trigger` / `content` / `body` and never `children`. `trigger` + follows `HoverCardSchema` two entries below, which is the settled in-repo shape + for this slot. + - **`context-menu`** now declares `triggerClassName`, `contentClassName` and + `modal` (read at `renderers/overlay/context-menu.tsx:87,88,91`), which survived + only on `BaseSchema.passthrough()`. + + **patch, not minor: the accept set only widens toward what already renders.** + Every key involved is optional, and `children` stays legal — it is `BaseSchema`'s + own optional key, merely no longer demanded here. No document that validated + before this change stops validating; documents the renderers already draw start + validating. The TypeScript twins in `packages/types/src/overlay.ts` move in the + same stroke, so the published declaration and the published validator keep saying + the same thing. + + ⛔ A tooltip's trigger is authored under `trigger`, never `children`: the catalog + entry was already moved to `trigger` once on render evidence (objectui#4626 — it + was a measured blank tile) and moving it back is a known regression. +- 777e5c6: Repair the `tree-view` mirror: `data` is optional, so the `nodes` spelling the + renderer reads FIRST is a legal document on its own (objectui#6939, maintainer + ruling recorded 2026-09-02 — this is one of the eight groups on that card, + dispatched as its own PR per the ruling). + + `TreeViewSchema` REQUIRED `data`, the limb the renderer reads THIRD: + + const rawNodes = boundData || schema.nodes || schema.data || []; + // packages/components/src/renderers/data-display/tree-view.tsx:105 + + The registration's own `inputs` and `defaultProps` spell it `nodes`, and the + four `components-data-display-tree-view/*` catalog entries ARE those + `defaultProps` — so `safeValidateSchema` refused every one of them + (`: Invalid input`) while the renderer drew them correctly. Re-measured on + `origin/main` at `fe4e7a9e8`: four refusals, and four renders that are + byte-identical under either spelling (28 / 28 / 12 / 34 elements, same tag + census, same `textContent` SHA-256). Identical output under the "correction" is + objectui#6318's own triage test for *the schema was the wrong side*. + + **For AUTHORS this widens on both faces.** `data` goes from required to optional + on the mirror and on the TypeScript twin in the same stroke; nothing that + validated before validates less, and no document that type-checked as a literal + stops doing so. A document authored on `data` — such as the tree-view entry in + `packages/types/examples/data-display-examples.json` — is untouched, and both + spellings together stay legal. + + **For a READER of the TypeScript twin this is a narrowing, and that is the half + worth stating.** `TreeViewSchema['data']` is now `TreeNode[] | undefined`, so + code that read `schema.data` and relied on its presence needs a guard and will + otherwise stop compiling (measured on a consumer probe: exit 0 before, `TS2322` + plus `TS18048` after). The only in-repo reader already has that guard — + `renderers/data-display/tree-view.tsx:105` reads + `boundData || schema.nodes || schema.data || []` — and it type-checks clean, so + nothing in this repository changes. An out-of-repo consumer that reads the key + unguarded is the population this paragraph exists for. + + Still `patch`: the required-ness was never a guarantee the renderer honoured (it + reads the key third, behind a default), the accept set only grows, and this is + the same shape as the two sibling groups of this card that have already landed. + + **`data` stays DECLARED rather than being deleted**, and the difference is + measured rather than assumed: `BaseSchema` already declares `data` + (`z.any().optional()`; `data?: any` on the TS face), so removing the member + would not reject the key — it would admit it *unvalidated* while the renderer + went on reading it. Optional-and-typed is the only shape in which `declared` and + `enforced` agree for a key that is still read. + + **No refinement was added**, deliberately, unlike this card's + `object-map` / `object-gantt` group. A tree-view carrying no data source at all + becomes legal here, and that admits no new rendering outcome: `{ data: [] }` was + already legal and already drew the same empty tree, so an "at least one of + `nodes` / `data` / `bind`" rule would forbid a spelling of an empty state the + contract already permits rather than buy a guarantee. + + `nodes` and `title` are objectui#6150's declarations and are unchanged; that + card declared the reads and said in as many words that relaxing `data` was a + separate accept-set change. This is that change. +- 0c386dd: `DataTableSchema.rowActions` validates as the boolean it has always been declared to be + (objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A). + + The hand-written zod mirror in `zod/data-display.zod.ts` declared + `rowActions: z.array(z.any()).optional()`. Every other face of the same key says + **boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's + destructuring default (`rowActions = false`), its two truthiness gates and two + `colSpan` arithmetic sites, the registered authoring input + (`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`, + and the renderer's own docblock example, which authors `"rowActions": true`. The mirror + was the single outlier — and the published one, so `safeValidateSchema` refused the + exact spelling the component's documentation, defaults and authoring UI all teach. Two + shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`) + failed validation for this and no other reason; both now validate **unchanged**. + + **Patch, not minor or major, and the reasoning is the ruling's own:** no author can have + relied on an array value. The renderer never reads the array — it only truthiness-tests + the key — so the smallest zod-valid array, `[]`, rendered the actions column identically + to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present, + against 39 with the key absent). An array authored here could therefore never have + carried meaning to any consumer: it either behaved exactly like `true` or, if empty, + still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted + but inert, not one anything could have depended on. + + A `boolean | array` union was considered and **not** taken: it would permanently accept + a shape the renderer cannot act on, which is the same second de-facto contract that the + array spelling already was. + + The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`, + the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is + correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is + not touched. +- ecd9cb2: Wizard view v1, the objectui half (Card R, objectui#6985) — alignment + pins for the + ruled `type: 'wizard'` tightening (objectstack#13622 D1–D8, maintainer ruling + 2026-08-31; spec half objectstack PR #13733). + + The renderer was already aligned: `WizardStepConfig` carries no predicate/collapse + keys (objectui#6237's ruled split), the wizard route drops-and-reports an authored + step `visibleWhen`, and `allowSkip` has been navigation-freedom-not-validation- + exemption since #2959. This card lands the residue: + + - **metadata-admin view create seeds one starter step for a wizard** (app-shell + `anchors.ts`): the create body used to emit `sections: []` for every form type, + which for `type: 'wizard'` is exactly the shape the tightened spec refuses at + parse (D7 — a stepless wizard silently rendered as a plain simple form). Same + seed-the-required-shape move the flow anchor makes for its `type` enum + (objectui#2326). Other form types keep the bare `[]` — only the wizard variant + refuses emptiness. + - **`@object-ui/types` TSDoc states the ruled wizard boundary** where the shared + section/form types restate the form-view family: `ObjectFormSection.visibleWhen` + / `collapsible` / `collapsed` name the wizard drop + spec-door refusal; + `ObjectFormSchema.sections` states sections-ARE-steps and array-order-is-step- + order; `allowSkip` states the D4 semantics. Type SHAPES are unchanged — the + spec's own ruled mechanism is a parse-time refinement over the single shared + section schema (D2 option A), which these types mirror at the type level. + - **Consumer-side behaviour pins** (`wizardRuledSemantics-6985.test.tsx`): the + wizard-inert step keys are dropped, never honoured (a denying `visibleWhen` + does not remove a step; `collapsible`/`collapsed: true` produce no collapse + affordance, with a positive control on the affordance probe); the empty-steps + wizard's measured degradation to a simple form is pinned as the shape the spec + door now refuses (one-step wizards stay legal — no arity floor); array order + is step order (with a reversed-array control). + - **Installed-spec door pins** (`wizardSpecDoor-6985.test.ts`), gated on a + capability probe of the installed `FormViewSchema` rather than a version + string: the post-Card-S half (refusal messages, prescriptions, the authored- + `false` collapse boundary, the wizard-scoped control) activates by itself on + the lockfile bump that brings the tightening in; until then the pre-tightening + half records the 17.2.x accept-set it measured. `steps:` is pinned refused on + every spec line. + + No teaching material — the #13337/#13086 fence lifts only after both halves land; + docs changes here are TSDoc/comments only. +- 0e3b3be: Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the + real boundary (objectui#7014). + + **No contract change.** No type, schema, export or runtime path moves. What changes is + published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was + asserting the opposite of what the spec does. + + Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in + fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a + control that accepts the same payload minus the key: + + - `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec` + `SelectOptionSchema.description`". That schema is `.strict()` over exactly + `{label, value, color, default, visibleWhen}`; `description` fails with + `unrecognized_keys`. + - `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec` + `FieldSchema.rows` declares the key "authorable on exactly the multiline editor + types". `FieldSchema` refuses `rows` by name on all four of + textarea/markdown/html/richtext. + + The keys themselves stay declared and stay consumed — `LookupField` searches an option's + `description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the + attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select + field's `options` through the strict option schema, so authoring `description` on an + option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422 + `INVALID_METADATA`. The comments were inviting exactly that write. They now say these are + objectui-side read-model extensions that must never reach authored object metadata. + + A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set + and each by-name refusal, so if the spec ever adopts one of these names the claim is + re-opened loudly instead of silently becoming true. +- c1fe272: Correct `BaseSchema.hidden`'s JSDoc: it hides by NOT RENDERING, exactly as + `visible: false` does (objectui#7088, maintainer ruling 2026-09-01). + + The declaration promised "Controls whether the component is hidden (but still + rendered) … component is rendered but not visible (visibility: hidden)". The + renderer has never done that. `visible`, `visibleWhen`, `visibleOn`, + `visibility`, `hidden` and `hiddenOn` are legs of one `shouldHide` chain in + `SchemaRenderer`; every leg feeds the same `_hidden` flag, and `_hidden` has + exactly one consumer — `if (evaluatedSchema._hidden) return null`. No node + survives for either key, and nothing in the repo emits a `visibility` style. The + sibling `visible` comment claimed `display: none` on the same false premise and + is corrected with it. + + **Comment-only — no behaviour moves.** The other reading, keeping the node in the + tree and hiding it visually, was weighed and **declined**: it is a behaviour + change on a published prop with zero named consumers, so an accessibility or + animation use-case that wants it reopens the question as its own feature card. + The JSDoc now records the synonymity as a decision, so the next reader does not + read "two keys" as "two behaviours", and notes that synonymous in OUTCOME is not + synonymous in PRECEDENCE — a declared `visible` short-circuits `hidden`, which + is unchanged and pinned elsewhere. + + Why a comment was worth a changeset: the JSDoc is the authority a later docs + correction is measured against, and this one nearly propagated. While splitting + the schema-reference `hidden` row, a reader checked it against `base.ts` and + almost "corrected" the table's "Inverse of `visible`" — the half that describes + shipped behaviour — toward the declaration. That row is unchanged and stays. + `SchemaRenderer.hiddenVisibleSynonymy.test.tsx` now pins the claim the comment + makes: the two keys produce the same rendered output, and `hidden: true` leaves + no node that could carry a `visibility` style. +- 858cd72: Declare `avatar` and `avatarFallback` on `ChatMessage`, on both faces + (objectui#7295 — the residue of objectui#4424, whose `RuntimeOnlyMessageKeys` + named only the three keys API mode lifts out of the stream, never the two a + human author writes by hand). + + `packages/plugin-chatbot/src/index.tsx:173–178` reads + `message.avatar || userAvatarUrl` and + `message.avatarFallback || userAvatarFallback` (and the assistant twins), the + authoring-to-runtime seam spreads every unlisted key through + (`chatMessageAdapter.ts`, `...passthrough`), and the SDUI renderer feeds the + authored `messages[]` straight in — a per-message avatar override renders, is + documented, and no authoring-facing type declared it. `ChatMessage` in + `packages/types/src/complex.ts` has no index signature (objectui#5155, + deliberately — none is added here), so an author annotating + `ChatbotSchema.messages` was told a value that renders is an error (TS2353); the + zod mirror `ChatMessageSchema` is a plain strip-mode `z.object`, so the value + parsed green and was silently DROPPED from the parsed output. + + **patch, not minor: the accept set only widens toward what already renders.** + Both keys are optional; no document that validated before stops validating, and + no TypeScript value that compiled before stops compiling. Two verdicts move, + both measured on `446d93d` before the change and both toward the renderer's + behaviour: + + - The mirror now KEEPS an authored `avatar` / `avatarFallback` through + `ChatMessageSchema`, `ChatbotSchema` and `safeValidateSchema` — it stripped + them before. A consumer that renders the PARSED document (none in this + repository does; the renderer receives the authored one) sees the override for + the first time. + - The mirror now REFUSES a non-string value at the key (`avatar: 42` was + admitted-and-stripped before) — enforcement of the declared type, not a new + capability. + + Same precedent as `CheckboxSchema.wrapperClass` (objectui#6938) and the + objectui#6150 batch. `RuntimeOnlyMessageKeys` in `plugin-chatbot` is untouched; + `SeamChatMessage` inherits the two keys through its `ChatMessage` half. The + three example blocks on `content/docs/plugins/plugin-chatbot.mdx` that PR #7294 + left unannotated (`supportChat`, `salesBot`, `multiAgentChat`) are annotated + `ChatbotSchema` again. +- 9587fc9: `ClassNameStylePropsSchema` describes itself by its two keys (objectui#7578). + + The schema's `.describe()` text changes from `Style properties` to + `className and inline style`. **This is published runtime metadata, not a + comment**: on this package `.describe()` is what lands in the generated + JSON-Schema `description` field and in the derived docs, so a consumer that + renders or diffs those will see the new string. The object itself is unchanged — + same two optional keys (`className`, `style`), same accept set, same types, same + export name; nothing validates differently. + + Why it moved. objectui#5928 renamed the const away from `StyleProps`, because + the like-named TypeScript `StyleProps` is the Tailwind-scale vocabulary + (`padding`, `margin`, `gap`, `backgroundColor`, ...) and shares zero keys with + these two. That rename only reached readers who can see the const name; the + description still said what the retired name said, so a reader who meets this + schema through generated JSON-Schema or docs was left hunting `padding` or `gap` + under a label that promised them. Naming the two keys ends that at the one place + that reader actually sees. + + The new text uses the verbatim key spellings, so the label answers "what is in + here" with names the reader can act on. Pinned in + `packages/types/src/__tests__/classname-style-describe-7578.test.ts`, read off + the live schema exported by the published `@object-ui/types/zod` barrel. +- 967e5d8: Chart `series[].opacity` and `series[].dashArray` are honoured on every series, + not only on a `variant: 'comparison'` one (objectui#7698). + + `@objectstack/spec` declares `ChartSeries.opacity` ("Override series opacity") + and `ChartSeries.dashArray` ("Override stroke dash pattern") as unconditional + per-series overrides, and `normalizeSeries` read both off every series. The + renderer then honoured them on a comparison overlay only, so an author who + wrote `{ name: 'cost', opacity: 0.6 }` or `{ name: 'cost', dashArray: '4 4' }` + on a primary series got a mark drawn exactly as if the key were absent. Fixed + in the renderer rather than by narrowing the published declaration to match: + the spec is the contract of record, and a renderer's partial implementation + does not get to dictate it (AGENTS.md #0.1). + + **Two gaps, not one.** The `variant` guard was the visible half — `comparisonStyle` + returned `null` for any other variant. The second half only showed on + `dashArray`: that helper already returned an AUTHORED dash for every family + (the `??` takes the left side whatever the kind), and the **Bar and Scatter + marks** then passed `fillOpacity` only, dropping `strokeDasharray` and + `strokeOpacity` on the floor — so an authored dash was lost on those two + families even on a comparison series. A fix aimed at the guard alone would have + left that untouched. `comparisonStyle` is now `seriesStyle`, and the Bar and + Scatter marks pass all three channels. + + **Comparison series are unaffected.** The authored branch already won over the + muted defaults, and those defaults stay gated on `variant: 'comparison'`: a + comparison series carrying neither key keeps its lower opacity and its `'4 4'` + line/area dash exactly as before. The two stroke defaults no mark ever consumed + (bar and scatter — neither is stroked by this renderer) are now spelled + `undefined`, so opening `strokeOpacity` on those marks does not hand them a + default they never had. + + Only a stroked mark can show a dash, so on a `bar` or `scatter` mark an + authored `dashArray` reaches the mark and paints nothing — the mark's geometry, + not a condition on the key. The `ChartDataSeries` mirror docs and the + plugin-charts reference, which stated the comparison-only condition as an + interim measure, are corrected in the same change. +- 4f9f1ee: Two of the ten `z.lazy` exports in the zod node face now memoise their getter, so their + public `.unwrap()` compares by identity (objectui#7918). The other eight are **deliberately + unchanged** — measured, memoising them is a module-load `ReferenceError`. + + The card that found this did not claim the ten were wrong. It asked whether the spelling was + buying a temporal-dead-zone dodge, and that check is what shipped. Each of the ten was + rewritten in place to `const inner = <body>; z.lazy(() => inner)`, the package rebuilt, and + the built barrel imported in a fresh process. **Eight refuse to load.** Seven name the very + const being declared (`children: z.array(TreeNodeSchema)` sits inside `TreeNodeSchema`'s own + initialiser); `SchemaNodeSchema` names `BaseSchemaCore`, which `base.zod.ts` declares below + it. For those eight the `z.lazy` is load-bearing, so `ActionSchema`, `AppMenuItemSchema`, + `FilterGroupSchema`, `MenuItemSchema`, `NavLinkSchema`, `NavigationMenuItemSchema`, + `SchemaNodeSchema` and `TreeNodeSchema` keep the spelling they have. The two that loaded + clean are memoised: `FilterBuilderConditionSchema` is not recursive at all, and + `NavigationItemSchema` already defers its self-reference through an inner + `z.lazy(() => NavigationItemSchema)` on `children`. + + ⚠️ Two corrections to the finding, both measured, both worth more than the edit: + + **The recursion point was already identity-comparable, through the right handle.** + `zod@4.4.3` caches a lazy's resolved inner type on `def._cachedInner` — its own comment says + this preserves "identity for cycle detection on recursive schemas" — and `S._zod.innerType` + reads that cache. It is stable for all ten, including the eight, and survives `.describe()` + clones. What is *not* stable is `S.unwrap()`, because `ZodLazy` defines it as + `() => inst._zod.def.getter()`, going around the cache (`ZodPromise` spells its own as a + stored field). So a schema walker can recognise the recursion point today by reading + `_zod.innerType`; the objectui#7581 false negative — `ActionSchema` reported "not exported by + name" when it plainly is — was the wrong handle, not an unrecognisable schema. Memoising is + still worth doing where it is free, because it makes the public `.unwrap()` honest. + + **The "rebuilt on every parse" cost does not exist.** The finding recorded, explicitly + unmeasured, that a document with N nodes reconstructs the recursive sub-schema N times. It + does not: the getter runs **once per lazy for the life of the process**, via the same + `_cachedInner` — measured at one call during the first parse of a 13-node document and zero + during the second. Wall clock agrees. `NavigationItemSchema` over a 73-node document, + memoised versus not, medians of nine trials of 200 parses: 149,973 ns versus 140,293 ns per + parse — ratio 0.94x, with the ranges overlapping. Those are shared-box seconds, so the + absolutes are not idle-machine figures; the ratio is the reading, and the reading is "no + difference". There is no parse-time win here, and anyone pricing the strict face + (objectui#7935 / objectstack#5250) should strike this from the input list. + + No accept/reject behaviour moves — a memoised getter changes schema *identity*, not what is + declared or admitted. The measurement, the eight `ReferenceError` messages, the identity + matrix and an executable reproduction of both the TDZ mechanism and the once-per-process + getter are pinned in `packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts`. + + ⚠️ Also settled while locating the ten: `AppMenuItemSchema` has no declaration of its own — + it is the barrel alias of `app.zod.ts`'s `MenuItemSchema`, while the barrel's own + `MenuItemSchema` is `overlay.zod.ts`'s. Two different schemas, so the list really is ten + entries and not nine. +- 100547e: `objectui validate` now refuses a form field whose widget id names a namespace + other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` + has given since objectui#5375 (objectui#5449). + + The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema + declared `type` and `widget` as bare optional strings — so a field typed + `ui:password` validated clean while the runtime validator rejected the same + document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an + author actually runs before shipping, so it was the one handing out the false + green: an author did exactly the diligence objectui#5375 asks for and still + shipped metadata that renders a secret into a plain text box. + + A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's + precedence (`widget` before `type`), the key it blames, its error code and its + message verbatim, so the two entry points cannot describe one defect two ways. + + **This rejects documents that previously validated.** Only colon-qualified + field widget ids outside the `field:` namespace are affected — `field:`-prefixed + ids and bare names such as `password` still pass, registered or not. A field + carrying, say, `type: 'ui:password'` must be rewritten as `password` or + `field:password`; it never rendered as a password box in any case. + + Which of the repo's authoring-time validators is canonical remains open + (objectui#4631) — this states the rule on the zod side rather than unifying + them. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- f9e4f91: `DashboardComponentSchema.dateRange.defaultRange` is now bound to + `DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a + hand-written 14-member union (objectui#4984). + + The union was byte-faithful to the spec — all 14 members, same order — so nothing + a user hits changes today. What was missing is the tie that keeps it faithful: + `resolveDashboardFilterDefs` takes `Pick<DashboardComponentSchema, 'globalFilters' | + 'dateRange'>`, so this union is what typechecks every TS-constructed dashboard, and + a preset the spec ADDS would have been a legal document that objectui's own types + said could not exist — the "narrower than the contract it implements" shape whose + consequence in objectui#4163 was that the bad reads were invisible to `tsc`. + + No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union + on an interface member has no symbol to collide with, while rule 2's claim heuristic + was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes + the file's existing "Aligned with @objectstack/spec" comment structural rather than + prose. + + The emitted `.d.ts` collapses the inline union to the imported alias; the published + type surface is unchanged — measured with the TypeScript checker over the emitted + declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, + and `defaultRange` resolving to the same 14 string-literal members before and after). +- 4e8622b: `ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a + plain string (objectui#4611). + + The paragraph explaining why `label` / `options[].label` are inherited rather than + locally overridden justified itself with a claim about `@objectstack/spec` that was + never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects + were dropped in favour of translation files". Measured against the installed GA pin + `@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of + a string and a string-to-string record, and the schema's own doc block states two + authorized forms with "Both are real; neither is deprecated by this schema". Executed + against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted, + `{ key, defaultValue }` rejected. A reader who believed the comment would have taken a + widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one + seat having nearly done exactly that. + + The replacement describes what `I18nLabel` admits and cites the spec's own doc block + rather than restating a zod expression; where today's spelling is named it is scoped as + a measurement against 17.0.0 with its file and line, so it ages as a reading rather than + as a standing fact. The decision itself is unchanged and never depended on the false + premise — `label` flows in by reference through the spec's schema, and a local + `string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds. + + Documentation only, and the release-visible surface is the declaration file: measured + with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`), + 108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every + other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha), + because the comment documents an `interface`, which is erased at emit along with its + leading comment. No behaviour changes; hover text and the shipped `.d.ts` do. +- dffd752: The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and + `ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613). + + Both are residue of the same removed local `label` / `options[].label` override. + + The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` + that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion, + `const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed + `I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain + string is assignable under the narrow shape *and* under the wide one, so that assignment + could only ever fail if the plain-string form were removed — the opposite of the event it + was written to catch. The widening had already landed: `@objectstack/spec@17.0.0` + declares `I18nLabelSchema` as a union of a string and a string-to-string record + (`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection + it did not provide, and asserted a false premise in its own name. + + It is retargeted at what actually holds the decision up — not which single form the spec + has, but that **both** authorized forms stay assignable, on the spec type and on the + inherited `ActionParam['label']` and `options[].label`. It now fails when either form is + withdrawn, and deliberately does not fail on a further widening, since inheriting by + reference is exactly what stays correct as the authorized set moves. The comment is + rewritten against the schema's own doc block (two authorized forms, "Both are real; + neither is deprecated by this schema") instead of the false premise. Verified by + construction: against a locally built narrow `type I18nLabel = string` the new assertions + fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both + shapes. + + `ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since + the override was removed, and nothing re-exported it — and the doc paragraph that + recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard. + + No behaviour changes; the release-visible surface is the declaration file. Measured with + the package's real `tsc` build, both legs building from a cleared `dist/` and cleared + composite build info: 108 emitted files on both sides, exactly one differing — + `dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed. + Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged + sha), because the comment documents an `interface`, which is erased at emit along with + its leading comment. The deleted type import contributes no emitted delta at all, and the + rewritten test file is not part of the build. +- 689b979: Add `editable` to the rich `TableColumnSchema` zod mirror. The `TableColumn` interface declares `editable?: boolean` and `data-table` honours it, but the mirror omitted the key, so a non-strict parse silently stripped it — and since the renderer treats absence as `true`, a column an author locked with `editable: false` came out of validation editable again. Columns locked with `editable: false` now stay locked through any pipeline that parses metadata via `@object-ui/types/zod` (including the CLI `validate` route). +- e546222: The published `@default` documentation on two `layout.ts` members now matches the value the renderer actually applies. `ContainerSchema.maxWidth` documented `'lg'` while `container.tsx` applies `schema.maxWidth ?? 'xl'`, and the shared `FlexLayoutProps.align` documented `'center'` while `flex.tsx` applies `schema.align || 'start'` and `stack.tsx` applies `schema.align || 'stretch'`. The renderers are unchanged — they are the authority for what runs — so only the docblocks moved; `align` now states both consumers in prose instead of carrying a single `@default`, because one member shared by two deliberately divergent component types cannot have one correct default. +- fd13f52: The published `@default` documentation on `FlexLayoutProps.direction` no longer states a value that only one of its two consumers applies. The member is declared once (objectui#6151) but `flex.tsx` reads `schema.direction || 'row'` while `stack.tsx` reads `schema.direction || 'col'` ("Default to column for Stack"), so the single `@default 'row'` was correct for `flex` and wrong for `stack` — whose own `defaultProps.direction` is `'col'`. The renderers are unchanged — they are the authority for what runs — so only the docblock moved: `direction` now names both consumers in prose, the same remedy objectui#7361 applied to the sibling `align`. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays: the criterion is a DIVERGENT shared member, not a shared one. +- 0fce2ef: `maxToolRoundtrips` on `ChatbotSchema` is deprecated: it is inert, and an author + who sets it is now told so instead of being left believing the documented cap + applies (objectui#5605). + + The key was declared authorable in `@object-ui/types` (interface and zod, with a + description), threaded from the authored document through the chatbot renderer at + three call sites, accepted by `useObjectChat`, given a default — and then dropped. + Measuring the installed chat runtime says it cannot be honoured from here rather + than that someone forgot to wire it: `@ai-sdk/react`'s `useChat` takes `ChatInit` + plus throttle/resume, and `ChatInit` declares exactly one loop control — the + boolean predicate `sendAutomaticallyWhen` — and no numeric cap under any + spelling. The numeric knob was removed from `useChat` in a major, and its + successor was renamed through `continueUntil` to `stopWhen` / `stepCountIs`, + which the installed `ai` package declares only on `generateText`, `streamText` + and the tool-loop agent settings — all server-side. ObjectUI is backend-agnostic, + so it owns no server loop to cap either, and putting the number in the request + body would only move the same dead key one hop onto a wire contract no backend + reads. + + This is stage one of a two-stage retirement, so nothing an author already wrote + breaks: the key still parses, still carries its declared shape, and the renderer + still threads it. What changes is that it is now marked `@deprecated` in the + interface, the zod description and the docs, and that authoring it logs a + one-time notice naming the knob that does work — `planning.maxIterations` on the + agent. A follow-up removes the declaration once this deprecation has shipped in a + release. +- a691c0b: `PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted. + + This is a published type surface: the TSDoc ships in `@object-ui/types`'s built + `.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'` + page is "constrained JSX/HTML + Tailwind" — and it links + `content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to + say the opposite. Shipped type documentation was contradicting the guide it points + readers to. + + ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that + framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind + is compiled at build time by scanning the console's own `src`, and there is no + safelist — so an authored utility class produces CSS only by coincidence, when + objectui already ships that exact class, and otherwise produces nothing with no + error anywhere. That is the ADR-0065 failure mode verbatim ("works only by + coincidence"), and it is how a modal's `bg-black/50` backdrop reached production + fully transparent. + + The tiers themselves are unchanged, and every load-bearing claim in the TSDoc + survives verbatim — parse-never-execute and untrusted-author safety for `html`, + the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the + `react-pages` host capability for `react`, the ADR-0080 citation and the guide + link. Only the styling conclusion changes, to the primitive each tier actually has: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows + light/dark and whatever theme the deployment installs. The TSDoc now also names the + rule that reports a violation — `page-source-className-tailwind`, shipped in + `@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by + `os validate` as a warning on both tiers. + + No behaviour change, and the accepted `kind` set is untouched. + + `packages/components/src/renderers/layout/react-page.tsx` carries the same + correction on its two source comments (the injected-scope note and + `buildComponentScope`), and gains the styling note the file was missing. Those are + internal comments — they do not project into any `.d.ts` and change no export — so + they get no entry of their own; there is nothing an `@object-ui/components` + consumer could read in a CHANGELOG and act on. +- 515f171: `PluginComponentInput` is deprecated in favour of `ComponentInput` (objectui#5674). + + This is stage 1 of a two-stage retirement the maintainer ruled on 2026-08-22: deprecate + for a release, then remove. Nothing is removed here — the export still exists and still + names the same type. + + `@object-ui/types`' entry point publishes `ComponentInput as PluginComponentInput`. Until + objectui#4972 that alias pointed at a genuinely different declaration: `plugin-scope.ts` + restated its own nine-key `ComponentInput`. objectui#5671 converged that declaration onto + `base.ts`, so the alias became a second published name for the *same* type, carrying no + information the first does not. Two published names for one type is a shape that costs + readers a step and gives AI-authored code a coin-flip between spellings. + + **Why a deprecation window rather than a deletion.** The measurement that licenses + deleting an export from a published package is *"no importer"*, and what can be measured + from inside this repository is only *"no importer here"*. In-repo the name has zero + importers — searched across every root (`packages/`, `apps/`, `content/`, `docs/`, + `skills/`, `examples/`, `e2e/`, `scripts/`, and the root docs), plus the sibling framework + repository, with a control name searched identically so a broken search could not read as + a clean one; the only occurrences are the alias itself and prose about it. What no search + here can see is a consumer on npm. The deprecation window is the answer to that + unmeasurable half: it converts a silent break into a warned one before the removal lands. + + **Reversibility, deliberately.** Deprecating is undoable; deleting a published export is + not. Where the evidence is one-sided, the retirement takes the reversible step first. + + The tag reaches consumers rather than only the source: a JSDoc block on an export + specifier survives declaration emit and lands attached to that specifier in the emitted + `index.d.ts`, which was measured for this change rather than assumed. With comments + stripped, the emitted entry point is byte-identical before and after — the published + *type* surface does not move, it only gains the notice. + + Stage 2 (removing the alias, and the now-dead re-export in `plugin-scope.ts` that exists + only to feed it) is filed as a follow-up and ships as a `minor`, per this repo's policy + that its own breaking changes never declare `major`. +- 3c73d99: Corrects the `@deprecated` prescription on `UniquenessValidation` in + `packages/types/src/data-protocol.ts`, which pointed authors at spellings the platform + no longer accepts (objectui#4765). + + Comment-only — no runtime behaviour changes. `patch` rather than an empty frontmatter + because the JSDoc sits on an **exported** declaration and therefore ships to consumers: + measured with the package's own build (`tsc`, and `tsconfig.base.json` deliberately sets + `removeComments: false`), `dist/data-protocol.d.ts` goes 40218 → 41781 bytes and the new + prose is present in the emitted `.d.ts`. What a consumer reads on hover changes, so it + is declared. The emitted `dist/data-protocol.js` is byte-identical (sha256 + `a3de34c5…`, 207 bytes both ways) — that file is a types-only module whose entire JS + output is the license banner plus `export {}`, so a comment on an erased `interface` + reaches the declaration file and nothing else. + + Two of the three spellings it prescribed were wrong, measured against the installed + `@objectstack/spec@17.0.0` (the report was written against `17.0.0-rc.6`): + + - **`indexes[].partial`** was retired in spec 17.0.0 under ADR-0049. It is a tombstone + (`z.never()`) that the parse rejects at any value, so "`partial` for a scoped + constraint" named a key that cannot be declared. A predicated unique constraint is + built at the database layer by a runtime migration issuing + `CREATE UNIQUE INDEX … WHERE`; the prescription now says so. + - **`{ fields, unique: true }`** on `ObjectSchema.indexes` is the deprecated positional + spelling of `unique: 'global'` under ADR-0120 — lint `unique/unscoped-declared-index` + warns in 17.x and protocol 18 rejects it. The prescription now states the scope: + `unique: 'global' | 'organization'`. + + The measurement also refined the report, and the refinement is the reason the rewrite is + not a uniform find-and-replace. The third spelling — **field-level** `unique: true` — is + NOT deprecated. `unique` is scope vocabulary shared by two surfaces on which the same + bare `true` means different things: at index level it stays verbatim (`isGlobalUnique` + and `isOrganizationUnique` both return `false`), which is why it is the positional + spelling of `'global'` and is being retired; at field level it is the positional spelling + of `'organization'` and, in the spec's own words, "stays valid indefinitely … no trap". + Rewriting both occurrences the same way would have replaced one piece of false guidance + with another, so the comment now names the per-surface difference explicitly. + + The interface's own deprecation is untouched and remains correct: `ValidationRuleSchema` + rejects `type: 'unique'` at the discriminator (accepted discriminants are `script`, + `state_machine`, `format`, `cross_field`, `json_schema`, `conditional`), so a rule in + this shape cannot reach the server. + + The replacement closes with what would falsify it — `UniqueScopeSchema` and + `IndexSchema` in `@objectstack/spec` — so the next reader checks the schemas rather than + trusting the paragraph. This is the fourth piece of false guidance found in this + campaign (strictness ledger finding 18), and prose that cannot be checked is how the + first three survived. +- 1170ed1: `WidgetInput.type` now uses the shared arm vocabulary `ComponentInputControlType` + instead of restating its eleven literals inline (objectui#5675). + + That vocabulary has had a name since objectui#3832, and objectui#4972 converged the + last structural copy of the surrounding `ComponentInput` interface onto one + declaration. `WidgetInput` was the remaining site spelling the arms out — a third + copy of one list, with nothing tying it to the other two. + + **No value a widget author may write changes.** The inline restatement was measured + member-equal to the shared declaration in both directions before it was replaced + (eleven arms each, same set, and in the same order), so this is a convergence rather + than a widening or a narrowing. `WidgetInput.type` also stays the SINGLE-kind form: + `ComponentInput.type` additionally accepts an array of arms for a union-typed key + (objectui#3832), and importing that capability here would have been a widening, so + it was deliberately left out — the same disposition objectui#4972 recorded when it + left this face alone. + + What the convergence buys is that one of the two drift directions was **silent**. + `WidgetRegistry.load()` in `@object-ui/core` translates each `WidgetInput` into a + `ComponentInput` and passes `type` straight through, so an arm REMOVED from the + shared vocabulary would have broken that assignment loudly at compile time — but an + arm ADDED to it produced no error anywhere. Widget authoring would just have stayed + narrower than component registration, with nothing in the tree saying so. After this + change neither direction is expressible. + + The pin is source-text, deliberately: a TS type alias erases at runtime, so a + member-identical restatement is indistinguishable from the derived type by any + assignability or runtime check. Both kinds of assertion are kept in + `__tests__/widget-input-control-vocabulary.test.ts`, and the ablation showing the + value assertions stay green on the defect while the identity pin turns red is quoted + in the PR body. + + Two divergences between `WidgetInput` and `ComponentInput` are deliberately **not** + repaired here, and are now recorded in `WidgetInput`'s doc block instead of living + only in a closed card's body: the enum slot is spelled `options` on one face and + `enum` on the other (adapted at the `WidgetRegistry` seam, so nothing fails to + arrive), and `ComponentInput` carries five keys — `inputType`, `min`, `max`, `step`, + `placeholder` — that a widget manifest cannot express. Both are surface questions + about published keys and are raised on objectui#5675 rather than answered by this + change. + ## 17.6.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 1ff1b945b6..51aff7f2d9 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/types", - "version": "17.6.0", + "version": "17.7.0", "description": "Pure TypeScript type definitions for Object UI - The Protocol Layer", "type": "module", "sideEffects": false, diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index c9ae926d1f..02fd29c428 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,5 +1,270 @@ # Changelog +## 17.7.0 + +### Patch Changes + +- 744c25e: Fix the phantom import the VS Code extension's **Export to React** command wrote into + every file it generated (objectui#7837). + + `generateReactComponent()` emitted a preamble that imported `registerDefaultRenderers` + from `@object-ui/components` and then called it. That symbol is on **no export** of + that package: its built `dist/index.d.ts` carries exactly one `register*` name, + `registerPlaceholders`, and `registerDefaultRenderers` appears **0 times** in either + `dist/index.d.ts` or `dist/index.js`. So every file the command produced failed to + compile with `TS2305` naming a symbol the user never typed. + + `@object-ui/components` registers its renderers as an **import side effect** — + `sideEffects: true` in its manifest, `import './renderers'` in its barrel under the + comment `Register all ObjectUI renderers (side-effects)`, and **114 `register(` call + sites** at module scope in the built `dist/index.js`. There is no registration function + to call, so the generated preamble now imports the package for the side effect and says + why. Same spelling the root README landed for objectui#7417. + + `packages/vscode-extension/DESIGN.md`, which documented the identical two lines, is + corrected in the same commit so the design record does not freeze the defect. + + No public surface moved: no export added, no signature changed. +- f98e73b: Drop the unused `import React from 'react'` the VS Code extension's **Export to React** + command wrote into every file it generated (objectui#7862). + + The generated file's only JSX is a single SchemaRenderer element, so under the automatic + JSX runtime — `"jsx": "react-jsx"`, what a new Vite or Next project is configured with — + the `React` identifier was never read. Measured on this branch against the built + `dist/index.d.ts` of `@object-ui/react` and `@object-ui/components`, TypeScript 6.0.3: + the emitted file compiled clean under `react-jsx` + `strict` (exit 0), and under the same + config plus `noUnusedLocals: true` it failed with + `TS6133: 'React' is declared but its value is never read` — so a consumer with that + option on could not compile the file the command had just handed them. + + The preamble now says in a comment that it assumes the automatic runtime and that the + import goes back only on the classic `"jsx": "react"` transform, which is the one + configuration this costs: measured, that file reports one diagnostic about `React` being + out of scope. Nothing in the extension emits or promises a `jsx` setting — the string + does not occur anywhere in the package — and the published docs page for the command + already showed the output without the import. + + A new pin, `src/__tests__/export-to-react-compiles.test.ts`, now extracts the template's + PRODUCT and compiles it under `noUnusedLocals`, rather than matching substrings in the + generator. The sibling objectui#7837 pin was green for the whole life of this line + because it never named it; a compile closes the class instead of one member of it. Its + positive control runs on every invocation: re-adding the import must report TS6133, so + the harness cannot go quietly, permanently green. + + `packages/vscode-extension/DESIGN.md`, whose section 4 hand-mirrors this preamble, is + corrected in the same commit so the design record does not freeze the defect — the + spelling objectui#7837 landed for the same file. What binds the two copies together is + still nothing, and objectui#7976 holds that question. + + No public surface moved: no export added, no signature changed. +- Updated dependencies [64dae8e] +- Updated dependencies [06a8af5] +- Updated dependencies [6a91586] +- Updated dependencies [a04d7c6] +- Updated dependencies [9801765] +- Updated dependencies [460575f] +- Updated dependencies [d88e20f] +- Updated dependencies [2d7304d] +- Updated dependencies [636b236] +- Updated dependencies [64d624d] +- Updated dependencies [053fdc8] +- Updated dependencies [d2fb6ef] +- Updated dependencies [490d9a9] +- Updated dependencies [fc62bb4] +- Updated dependencies [41df893] +- Updated dependencies [00f3eb5] +- Updated dependencies [1ec291c] +- Updated dependencies [453dbaa] +- Updated dependencies [69a2163] +- Updated dependencies [24e027e] +- Updated dependencies [2c3cd1b] +- Updated dependencies [90665e0] +- Updated dependencies [7e19d03] +- Updated dependencies [546ddf7] +- Updated dependencies [864154e] +- Updated dependencies [b023625] +- Updated dependencies [75bd83d] +- Updated dependencies [44d075b] +- Updated dependencies [40c479a] +- Updated dependencies [971d387] +- Updated dependencies [ee851c3] +- Updated dependencies [6414dfd] +- Updated dependencies [a8d5c71] +- Updated dependencies [905b21f] +- Updated dependencies [88e9109] +- Updated dependencies [2c45966] +- Updated dependencies [db3a600] +- Updated dependencies [52a43de] +- Updated dependencies [e4559d1] +- Updated dependencies [2c71482] +- Updated dependencies [a26b9e4] +- Updated dependencies [5ef9c4f] +- Updated dependencies [46f0bb4] +- Updated dependencies [6f81384] +- Updated dependencies [8f1d995] +- Updated dependencies [dddb942] +- Updated dependencies [29754cf] +- Updated dependencies [3c2b6f7] +- Updated dependencies [b84dc18] +- Updated dependencies [ac8abb0] +- Updated dependencies [9d86e1d] +- Updated dependencies [99a3c2d] +- Updated dependencies [5961030] +- Updated dependencies [c8ea8af] +- Updated dependencies [3190414] +- Updated dependencies [4e480f5] +- Updated dependencies [38a123c] +- Updated dependencies [299102e] +- Updated dependencies [d7acad6] +- Updated dependencies [45a9aeb] +- Updated dependencies [713db46] +- Updated dependencies [bf3a03c] +- Updated dependencies [831be72] +- Updated dependencies [29cb85b] +- Updated dependencies [3e028c8] +- Updated dependencies [d0889e2] +- Updated dependencies [ce503e5] +- Updated dependencies [f20dcf0] +- Updated dependencies [4ca30d0] +- Updated dependencies [7a5da14] +- Updated dependencies [2c1c967] +- Updated dependencies [4d5f9b4] +- Updated dependencies [d6ceb8d] +- Updated dependencies [7977ff9] +- Updated dependencies [3beef6d] +- Updated dependencies [045d20b] +- Updated dependencies [adb2a86] +- Updated dependencies [3561bd2] +- Updated dependencies [bf97b98] +- Updated dependencies [b0d308d] +- Updated dependencies [8063bcb] +- Updated dependencies [b74a859] +- Updated dependencies [d4493fd] +- Updated dependencies [240b80f] +- Updated dependencies [77cb489] +- Updated dependencies [bfaa158] +- Updated dependencies [777e5c6] +- Updated dependencies [0c386dd] +- Updated dependencies [9e37d9b] +- Updated dependencies [5ad86dd] +- Updated dependencies [16a725f] +- Updated dependencies [4dfdcc3] +- Updated dependencies [446d93d] +- Updated dependencies [ecd9cb2] +- Updated dependencies [98d4108] +- Updated dependencies [0e3b3be] +- Updated dependencies [4388f71] +- Updated dependencies [c93b4d5] +- Updated dependencies [c1fe272] +- Updated dependencies [8ad218d] +- Updated dependencies [5f78953] +- Updated dependencies [639114c] +- Updated dependencies [1f31d3a] +- Updated dependencies [351eb31] +- Updated dependencies [20c04b2] +- Updated dependencies [48c19bd] +- Updated dependencies [a6d8b8d] +- Updated dependencies [b652514] +- Updated dependencies [adbda1b] +- Updated dependencies [2e32ed4] +- Updated dependencies [e75f4c9] +- Updated dependencies [19f1639] +- Updated dependencies [47547d0] +- Updated dependencies [858cd72] +- Updated dependencies [554f2b6] +- Updated dependencies [669d71b] +- Updated dependencies [ed27d7c] +- Updated dependencies [52c8cf7] +- Updated dependencies [52c8cf7] +- Updated dependencies [81a2eb1] +- Updated dependencies [00d2fa6] +- Updated dependencies [c6198c2] +- Updated dependencies [51eb515] +- Updated dependencies [c354ce5] +- Updated dependencies [8fe8e5c] +- Updated dependencies [9587fc9] +- Updated dependencies [e62c44e] +- Updated dependencies [5d0876c] +- Updated dependencies [b041b9c] +- Updated dependencies [ce2aaef] +- Updated dependencies [bc640ec] +- Updated dependencies [3e377c9] +- Updated dependencies [a3eb5d0] +- Updated dependencies [4ce14f1] +- Updated dependencies [2af1fa7] +- Updated dependencies [caf477f] +- Updated dependencies [f6375da] +- Updated dependencies [967e5d8] +- Updated dependencies [a4611b3] +- Updated dependencies [20316ba] +- Updated dependencies [d3499b3] +- Updated dependencies [18897a4] +- Updated dependencies [8b7ea39] +- Updated dependencies [52cac38] +- Updated dependencies [64a252d] +- Updated dependencies [4f9f1ee] +- Updated dependencies [c842594] +- Updated dependencies [cf1d29e] +- Updated dependencies [0ea7054] +- Updated dependencies [fc32921] +- Updated dependencies [8f9d87a] +- Updated dependencies [6bca0e4] +- Updated dependencies [81c0bc4] +- Updated dependencies [2fcefb9] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [6d1c155] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [5aed9e4] +- Updated dependencies [83c77dc] +- Updated dependencies [18a8e7d] +- Updated dependencies [e7957ab] +- Updated dependencies [f7e34ca] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [199d31b] +- Updated dependencies [3e01cb5] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [105f3c5] +- Updated dependencies [3ccd9e8] +- Updated dependencies [689b979] +- Updated dependencies [e546222] +- Updated dependencies [fd13f52] +- Updated dependencies [0fce2ef] +- Updated dependencies [b2ea297] +- Updated dependencies [5b5a5c3] +- Updated dependencies [a691c0b] +- Updated dependencies [af3861f] +- Updated dependencies [515f171] +- Updated dependencies [258d264] +- Updated dependencies [c00bf28] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [83fe6e7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [ed71d9e] +- Updated dependencies [7776fc2] +- Updated dependencies [1170ed1] +- Updated dependencies [4d73b07] + - @object-ui/core@17.7.0 + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 728f2947ba..5daed3c1ef 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "object-ui", "displayName": "Object UI", "description": "VSCode extension for Object UI - Schema-driven UI development with IntelliSense, validation, and live preview", - "version": "17.6.0", + "version": "17.7.0", "publisher": "objectui", "private": true, "icon": "icon.svg",