diff --git a/.changeset/action-param-dialog-option-visiblewhen-4758.md b/.changeset/action-param-dialog-option-visiblewhen-4758.md
deleted file mode 100644
index c0335d9f7..000000000
--- a/.changeset/action-param-dialog-option-visiblewhen-4758.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`ActionParamDialog` (the `custom` barrel's published dialog) now resolves each
-`select` param's options through `@object-ui/core`'s shared option evaluator, so a
-per-option `visibleWhen` narrows the offered list here exactly as it does on the
-app-shell action dialog and in the object form (objectui#4758).
-
-This surface is the repo's second action-param dialog, and its `select` branch
-rendered `param.options?.map(...)` straight into Radix items. A per-option
-`visibleWhen` was not evaluated wrongly — it was not evaluated at all, so an option
-gated on `record.*` (a sibling param) or on `current_user.*` was offered
-unconditionally, while the app-shell dialog filtered the identical field metadata.
-Triage ruled the governed side authoritative; the dialog rebinds to
-`resolveVisibleOptions`, resolving predicates against the dialog's own in-progress
-values (the objectui#3765 Option B ruling) plus the ambient predicate scope.
-
-Rebind, not removal: the component stays a published export, its props and every
-other branch are untouched, and retiring it remains a separate decision.
-
-A selection the predicate stops offering is now cleared rather than kept as a hidden
-value — the same `isValueStillOffered` clear `SelectField` already performs. Without
-it, filtering alone would let a picked-then-gated-out option vanish from the trigger
-while still riding in the submitted payload. Params whose options declare no
-predicate are untouched.
diff --git a/.changeset/adapter-meta-type-singular-4940.md b/.changeset/adapter-meta-type-singular-4940.md
deleted file mode 100644
index a96ab6451..000000000
--- 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 e30834a07..000000000
--- 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 3ad06d025..000000000
--- 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-studio-accessor-5577.md b/.changeset/ai-studio-accessor-5577.md
deleted file mode 100644
index dca947932..000000000
--- 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/alias-closure-relative-miss-5386.md b/.changeset/alias-closure-relative-miss-5386.md
deleted file mode 100644
index f67ab4d32..000000000
--- 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/app-shell-page-header-subtitle-4761.md b/.changeset/app-shell-page-header-subtitle-4761.md
deleted file mode 100644
index 01333ab79..000000000
--- 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/approvals-inbox-cell-remount-5348.md b/.changeset/approvals-inbox-cell-remount-5348.md
deleted file mode 100644
index 4a11753a5..000000000
--- 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-raw-payload-gate-5553.md b/.changeset/approvals-raw-payload-gate-5553.md
deleted file mode 100644
index 3cd1ed264..000000000
--- 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 8bccea37e..000000000
--- 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 f44198385..000000000
--- 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 2137b2b8c..000000000
--- 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/calendar-explain-network-escape-5438.md b/.changeset/calendar-explain-network-escape-5438.md
deleted file mode 100644
index 18359f993..000000000
--- 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 eaf0ed083..000000000
--- 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/catalog-layout-props-sweep-4891-4890.md b/.changeset/catalog-layout-props-sweep-4891-4890.md
deleted file mode 100644
index 92f13f187..000000000
--- 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/chat-transport-memo-4187.md b/.changeset/chat-transport-memo-4187.md
deleted file mode 100644
index 93978f70c..000000000
--- 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 84f3ac9f6..000000000
--- 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/cli-validate-field-widget-namespace-5449.md b/.changeset/cli-validate-field-widget-namespace-5449.md
deleted file mode 100644
index 2984a2ef4..000000000
--- 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/cloud-connection-bind-failure-i18n-5054.md b/.changeset/cloud-connection-bind-failure-i18n-5054.md
deleted file mode 100644
index 68a88aac2..000000000
--- 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 00f5731ef..000000000
--- 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/componentinput-reexport-4972.md b/.changeset/componentinput-reexport-4972.md
deleted file mode 100644
index 910e0a734..000000000
--- 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 52cad5745..000000000
--- 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/console-action-dispatch-envelope-5611.md b/.changeset/console-action-dispatch-envelope-5611.md
deleted file mode 100644
index a7e8555f2..000000000
--- 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 44270d6e8..000000000
--- 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-form-container-specs-one-declaration-5596.md b/.changeset/console-form-container-specs-one-declaration-5596.md
deleted file mode 100644
index 6f199b8c8..000000000
--- 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 59b1a9fdc..000000000
--- 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-formpage-visible-predicates-5594.md b/.changeset/console-formpage-visible-predicates-5594.md
deleted file mode 100644
index 60c95e1a1..000000000
--- 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 9b44325e8..000000000
--- 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-studio-builder-decorative-lazy-5486.md b/.changeset/console-studio-builder-decorative-lazy-5486.md
deleted file mode 100644
index e557f79f0..000000000
--- 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 a629873f7..000000000
--- 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/current-user-default-prefill-5683.md b/.changeset/current-user-default-prefill-5683.md
deleted file mode 100644
index b519b0fb6..000000000
--- 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/dashboard-self-contained-unreachable-arm-4620.md b/.changeset/dashboard-self-contained-unreachable-arm-4620.md
deleted file mode 100644
index 7d8e70c29..000000000
--- 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 9fc980654..000000000
--- 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 8a0de721b..000000000
--- 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-title-locale-writeback-5428.md b/.changeset/dashboard-title-locale-writeback-5428.md
deleted file mode 100644
index 849b85b70..000000000
--- 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 ba87f15ec..000000000
--- 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,
-…>` (`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/data-table-header-declared-key-5351.md b/.changeset/data-table-header-declared-key-5351.md
deleted file mode 100644
index c0b154de2..000000000
--- 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 60e729853..000000000
--- 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`, 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-refresh-callback-objectview.md b/.changeset/dead-refresh-callback-objectview.md
deleted file mode 100644
index 6f077fa36..000000000
--- 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 0605f03d1..000000000
--- 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/delete-dead-release-workflow-5405.md b/.changeset/delete-dead-release-workflow-5405.md
deleted file mode 100644
index b1c00f27a..000000000
--- 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/@`) 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/@`. 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` 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/dom-leak-sweep-components-5574.md b/.changeset/dom-leak-sweep-components-5574.md
deleted file mode 100644
index ce389acf8..000000000
--- 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/dts-build-exit-code-5370.md b/.changeset/dts-build-exit-code-5370.md
deleted file mode 100644
index b6fe0bc6d..000000000
--- 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 73de8c9d3..000000000
--- 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 230d59670..000000000
--- 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/esm-load-comment-mask-5382.md b/.changeset/esm-load-comment-mask-5382.md
deleted file mode 100644
index 70abee6d0..000000000
--- 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 d8167b195..000000000
--- 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 c6a890b5e..000000000
--- 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/flow-resume-result-5417.md b/.changeset/flow-resume-result-5417.md
deleted file mode 100644
index 5de280435..000000000
--- 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: })`, 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-designer-namespaced-field-type-4838.md b/.changeset/form-designer-namespaced-field-type-4838.md
deleted file mode 100644
index 88b03f1e6..000000000
--- 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-reset-notification-channels-5235.md b/.changeset/form-reset-notification-channels-5235.md
deleted file mode 100644
index 6a2d2c3f1..000000000
--- 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 6e68a460d..000000000
--- 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 183a8fc65..000000000
--- 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-maxlength-override-5595.md b/.changeset/formpage-maxlength-override-5595.md
deleted file mode 100644
index 337dd53ad..000000000
--- 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 d20c6fa82..000000000
--- 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/governed-surface-human-merge-5149.md b/.changeset/governed-surface-human-merge-5149.md
deleted file mode 100644
index 7d63234c4..000000000
--- 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 d80cdfb61..000000000
--- 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 1fa03310a..000000000
--- 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` (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..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 1dc2d3506..000000000
--- 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 `
` 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-unresolved-column-diagnostic-5349.md b/.changeset/grid-unresolved-column-diagnostic-5349.md
deleted file mode 100644
index 43d1c5a7d..000000000
--- 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 8f7c79a35..000000000
--- 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 e644f0adf..000000000
--- 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/home-ai-studio-flag-5521.md b/.changeset/home-ai-studio-flag-5521.md
deleted file mode 100644
index 411aa0a99..000000000
--- 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 1f43f6e85..000000000
--- 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 1c922a8be..000000000
--- 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`, 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 1e4cfdd0e..000000000
--- 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-guide-label-rule-5081.md b/.changeset/i18n-guide-label-rule-5081.md
deleted file mode 100644
index 16ec799ad..000000000
--- 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 3a243e944..000000000
--- 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 ffa052d0b..000000000
--- 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 ebb7ec8a9..000000000
--- 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/issue-5389-workspace-admin-positions.md b/.changeset/issue-5389-workspace-admin-positions.md
deleted file mode 100644
index 2c12bc2f6..000000000
--- 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/list-user-actions-collision-5398.md b/.changeset/list-user-actions-collision-5398.md
deleted file mode 100644
index 62a0d3c77..000000000
--- 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 f330b77a6..000000000
--- 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-comment-pair-4559.md b/.changeset/listview-comment-pair-4559.md
deleted file mode 100644
index 2f012823c..000000000
--- 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/lookup-dropdown-cell-renderer-5492.md b/.changeset/lookup-dropdown-cell-renderer-5492.md
deleted file mode 100644
index d43f0e19a..000000000
--- 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 eae0d1622..000000000
--- 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 `` 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/marketplace-catalog-runtime-before-admin-5557.md b/.changeset/marketplace-catalog-runtime-before-admin-5557.md
deleted file mode 100644
index 69a66da58..000000000
--- 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 b50d52248..000000000
--- 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 672d7f993..000000000
--- 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 4462a0913..000000000
--- 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/metadata-client-actor-retired-4834.md b/.changeset/metadata-client-actor-retired-4834.md
deleted file mode 100644
index 2aa8de93c..000000000
--- 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 785458cbf..000000000
--- 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/objectchart-fieldoptionlabel-ref-5587.md b/.changeset/objectchart-fieldoptionlabel-ref-5587.md
deleted file mode 100644
index 4b61e3f15..000000000
--- 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-schema-data-shorthand-5305.md b/.changeset/objectmap-schema-data-shorthand-5305.md
deleted file mode 100644
index c21345fce..000000000
--- 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/objectview-overlay-pair-completeness-5025.md b/.changeset/objectview-overlay-pair-completeness-5025.md
deleted file mode 100644
index 85273d19a..000000000
--- 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/olive-donkeys-smile.md b/.changeset/olive-donkeys-smile.md
deleted file mode 100644
index e643a315e..000000000
--- 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-pugs-sing.md b/.changeset/olive-pugs-sing.md
deleted file mode 100644
index 0ebe36886..000000000
--- 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/owd-width-single-home-5477.md b/.changeset/owd-width-single-home-5477.md
deleted file mode 100644
index 10aa33c86..000000000
--- 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 476267511..000000000
--- 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 (`` `` ``, `` `` ``) 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 f125f3894..000000000
--- 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 (`` `` ``, `` `` ``) 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/param-dialog-title-fallback-4282.md b/.changeset/param-dialog-title-fallback-4282.md
deleted file mode 100644
index 41c71b681..000000000
--- 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 a3aa770c3..000000000
--- 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/pending-drafts-bar-locale-parity-5705.md b/.changeset/pending-drafts-bar-locale-parity-5705.md
deleted file mode 100644
index 6bafb8435..000000000
--- 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 84dab23b4..000000000
--- 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/pivot-table-stable-empty-rows-5562.md b/.changeset/pivot-table-stable-empty-rows-5562.md
deleted file mode 100644
index d7423a5b5..000000000
--- 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/plugin-form-readme-classname-quantifier-5131.md b/.changeset/plugin-form-readme-classname-quantifier-5131.md
deleted file mode 100644
index 112411d63..000000000
--- 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 ``. 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/quick-reference-release-sync-5394.md b/.changeset/quick-reference-release-sync-5394.md
deleted file mode 100644
index d88e63d9e..000000000
--- 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/react-pages-guide-source-contradictions-5413.md b/.changeset/react-pages-guide-source-contradictions-5413.md
deleted file mode 100644
index dbc67fae9..000000000
--- 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 c14d4ec2e..000000000
--- 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/record-alert-cta-label-i18n-4998.md b/.changeset/record-alert-cta-label-i18n-4998.md
deleted file mode 100644
index c42fa2e98..000000000
--- 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-row-binding-4807.md b/.changeset/record-alert-row-binding-4807.md
deleted file mode 100644
index ecca389da..000000000
--- 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 f8c16e205..000000000
--- 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-picker-empty-text-i18n-5590.md b/.changeset/record-picker-empty-text-i18n-5590.md
deleted file mode 100644
index 8c53ac4a9..000000000
--- 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/registerfunction-casefold-doc-5363.md b/.changeset/registerfunction-casefold-doc-5363.md
deleted file mode 100644
index def190682..000000000
--- 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 31f50fe34..000000000
--- 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/report-view-datasource-object-key-5116.md b/.changeset/report-view-datasource-object-key-5116.md
deleted file mode 100644
index 2d3435113..000000000
--- 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-theme-component-schema-5489.md b/.changeset/retire-theme-component-schema-5489.md
deleted file mode 100644
index 121ce3a10..000000000
--- 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-usebranding-hook-5368.md b/.changeset/retire-usebranding-hook-5368.md
deleted file mode 100644
index bf5f6e24d..000000000
--- 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 ca91ef84f..000000000
--- 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 aa246fa96..000000000
--- 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 8b1448c2d..000000000
--- 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 `
Rich text
`,
-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/safe-field-label-identity-5564.md b/.changeset/safe-field-label-identity-5564.md
deleted file mode 100644
index b7ae0e40f..000000000
--- 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/schema-input-bridge-permanent-4622.md b/.changeset/schema-input-bridge-permanent-4622.md
deleted file mode 100644
index 4b7805128..000000000
--- 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-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md
deleted file mode 100644
index f14b09da7..000000000
--- 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 ``'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 2f405f211..000000000
--- 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 32ece0aee..000000000
--- 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 11941d4db..000000000
--- 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/shard-coverage-job-5403.md b/.changeset/shard-coverage-job-5403.md
deleted file mode 100644
index 8e30f8848..000000000
--- 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/skill-provider-envelope-teaching-5372.md b/.changeset/skill-provider-envelope-teaching-5372.md
deleted file mode 100644
index 4e6dd9e2a..000000000
--- 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 adebbd2ba..000000000
--- 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.` 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-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md
deleted file mode 100644
index 261be17ee..000000000
--- 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/stale-4163-pointers-5591.md b/.changeset/stale-4163-pointers-5591.md
deleted file mode 100644
index 7b77930b5..000000000
--- 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/standing-pending-drafts-bar-5694.md b/.changeset/standing-pending-drafts-bar-5694.md
deleted file mode 100644
index f1d669f65..000000000
--- 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-metadata-form-ux-5416.md b/.changeset/studio-metadata-form-ux-5416.md
deleted file mode 100644
index 551450b9e..000000000
--- 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 73ce02c26..000000000
--- 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 05f2f1e0c..000000000
--- 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 bed857904..000000000
--- 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/surface-deeplink-live-channel-5476.md b/.changeset/surface-deeplink-live-channel-5476.md
deleted file mode 100644
index 9515eb71b..000000000
--- 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=:` 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/table-renderer-declared-column-contract-5350.md b/.changeset/table-renderer-declared-column-contract-5350.md
deleted file mode 100644
index a405857ba..000000000
--- 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/tenant-header-edge-contract-5279.md b/.changeset/tenant-header-edge-contract-5279.md
deleted file mode 100644
index 3ba15b941..000000000
--- 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/tidy-donkeys-attack.md b/.changeset/tidy-donkeys-attack.md
deleted file mode 100644
index a7314fd9e..000000000
--- 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/unbundled-node-boundary-5384.md b/.changeset/unbundled-node-boundary-5384.md
deleted file mode 100644
index cefdbb061..000000000
--- 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/uniqueness-deprecation-jsdoc-4765.md b/.changeset/uniqueness-deprecation-jsdoc-4765.md
deleted file mode 100644
index 64fc315d6..000000000
--- 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/utilities-index-phantom-provider-5360.md b/.changeset/utilities-index-phantom-provider-5360.md
deleted file mode 100644
index 4c7a34666..000000000
--- 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/view-switcher-chart-gantt-icons-5586.md b/.changeset/view-switcher-chart-gantt-icons-5586.md
deleted file mode 100644
index 257d32d34..000000000
--- 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/visiblewhen-record-binding-5454.md b/.changeset/visiblewhen-record-binding-5454.md
deleted file mode 100644
index 8cf9b2e4b..000000000
--- 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.`"*. `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-invocation-guard-package-cwd-coverage-5406.md b/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md
deleted file mode 100644
index 61e50a177..000000000
--- 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 511b33d16..000000000
--- 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/zod-base-schema-mirror-parity-4605.md b/.changeset/zod-base-schema-mirror-parity-4605.md
deleted file mode 100644
index 1d0ba8c9f..000000000
--- 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/QUICK_REFERENCE.md b/QUICK_REFERENCE.md
index 45a066827..3247a938f 100644
--- a/QUICK_REFERENCE.md
+++ b/QUICK_REFERENCE.md
@@ -120,7 +120,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 8ef1818ba..f160c2f64 100644
--- a/apps/console/CHANGELOG.md
+++ b/apps/console/CHANGELOG.md
@@ -1,5 +1,523 @@
# @object-ui/console
+## 17.7.0
+
+### Patch Changes
+
+- 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 `` 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.
+- 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.
+- 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.
+- 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%.
+- 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.
+- 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.
+- 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.
+- 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 [0b1326d]
+ - @object-ui/react-runtime@17.7.0
+ - @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 0d050ae94..23cb4fb0e 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 c3876b79a..7606fe5bc 100644
--- a/packages/app-shell/CHANGELOG.md
+++ b/packages/app-shell/CHANGELOG.md
@@ -1,5 +1,1166 @@
# @object-ui/app-shell — Changelog
+## 17.7.0
+
+### Minor Changes
+
+- 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: })`, 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.
+- 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.
+- 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.
+- 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.
+- 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.
+
+### Patch Changes
+
+- 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 ``, 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.
+- 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.
+- 029e2fb: 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`.
+- 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.
+- 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.
+- 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.
+- 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.
+- 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.
+- 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.
+- 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.
+- 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=:` 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.
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [b2e85a9]
+- Updated dependencies [77f846a]
+- Updated dependencies [76ae729]
+- Updated dependencies [dd19463]
+- Updated dependencies [a31adc6]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [84d2e98]
+- Updated dependencies [f24195a]
+- Updated dependencies [56f4e34]
+- Updated dependencies [4db5989]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [9e22085]
+- Updated dependencies [b655a9d]
+- Updated dependencies [c574dfb]
+- Updated dependencies [02f48b6]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cad512f]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [32ef595]
+- Updated dependencies [15236e0]
+- Updated dependencies [ec9fdaa]
+- Updated dependencies [d6613a2]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [8e00bfd]
+- Updated dependencies [8d37efb]
+- Updated dependencies [6c5ee71]
+- Updated dependencies [6f017e9]
+- Updated dependencies [f1c27f0]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [0ccbdc1]
+- Updated dependencies [83ec618]
+- Updated dependencies [4bb940b]
+- Updated dependencies [0068348]
+- Updated dependencies [8a44390]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [934a532]
+- Updated dependencies [26a2238]
+- Updated dependencies [cfcff30]
+- Updated dependencies [3c73d99]
+- Updated dependencies [6b348d2]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/data-objectstack@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/plugin-calendar@17.7.0
+ - @object-ui/plugin-chatbot@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/permissions@17.7.0
+ - @object-ui/plugin-form@17.7.0
+ - @object-ui/plugin-dashboard@17.7.0
+ - @object-ui/plugin-designer@17.7.0
+ - @object-ui/plugin-detail@17.7.0
+ - @object-ui/layout@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/plugin-grid@17.7.0
+ - @object-ui/plugin-view@17.7.0
+ - @object-ui/plugin-charts@17.7.0
+ - @object-ui/auth@17.7.0
+ - @object-ui/plugin-list@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/plugin-editor@17.7.0
+ - @object-ui/plugin-kanban@17.7.0
+ - @object-ui/plugin-report@17.7.0
+ - @object-ui/collaboration@17.7.0
+ - @object-ui/providers@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json
index 37335039e..45d66f90b 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",
"license": "MIT",
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md
index 06a23c79b..532968121 100644
--- a/packages/auth/CHANGELOG.md
+++ b/packages/auth/CHANGELOG.md
@@ -1,5 +1,82 @@
# @object-ui/auth
+## 17.7.0
+
+### Patch Changes
+
+- 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.
+- 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.
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [3c73d99]
+- Updated dependencies [4d73b07]
+ - @object-ui/types@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 195443c69..332bff441 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 f54db40ee..ba2f3be6e 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,103 @@
# @object-ui/cli
+## 17.7.0
+
+### Minor Changes
+
+- 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
+
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@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 0ff445a08..bf53a792e 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 85e1e442d..1bbaa8833 100644
--- a/packages/collaboration/CHANGELOG.md
+++ b/packages/collaboration/CHANGELOG.md
@@ -1,5 +1,33 @@
# @object-ui/collaboration
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [77f846a]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [38a9568]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [3c73d99]
+- Updated dependencies [fb96ecb]
+- 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 cacb880af..8e8ded0c2 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 d8a3eadc6..55bdbbfa4 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -1,5 +1,525 @@
# @object-ui/components
+## 17.7.0
+
+### Minor Changes
+
+- 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`, 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`.
+- 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
+
+- 3b147a3: `ActionParamDialog` (the `custom` barrel's published dialog) now resolves each
+ `select` param's options through `@object-ui/core`'s shared option evaluator, so a
+ per-option `visibleWhen` narrows the offered list here exactly as it does on the
+ app-shell action dialog and in the object form (objectui#4758).
+
+ This surface is the repo's second action-param dialog, and its `select` branch
+ rendered `param.options?.map(...)` straight into Radix items. A per-option
+ `visibleWhen` was not evaluated wrongly — it was not evaluated at all, so an option
+ gated on `record.*` (a sibling param) or on `current_user.*` was offered
+ unconditionally, while the app-shell dialog filtered the identical field metadata.
+ Triage ruled the governed side authoritative; the dialog rebinds to
+ `resolveVisibleOptions`, resolving predicates against the dialog's own in-progress
+ values (the objectui#3765 Option B ruling) plus the ambient predicate scope.
+
+ Rebind, not removal: the component stays a published export, its props and every
+ other branch are untouched, and retiring it remains a separate decision.
+
+ A selection the predicate stops offering is now cleared rather than kept as a hidden
+ value — the same `isValueStillOffered` clear `SelectField` already performs. Without
+ it, filtering alone would let a picked-then-gated-out option vanish from the trigger
+ while still riding in the submitted payload. Params whose options declare no
+ predicate are untouched.
+- 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.
+- 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 `
` 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.
+- 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 (`` `` ``, `` `` ``) 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.
+- 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.` 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.
+- Updated dependencies [77f846a]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/react-runtime@17.7.0
+ - @object-ui/sdui-parser@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/components/package.json b/packages/components/package.json
index fba2d9db8..9d7dbc4a9 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 8da3d5884..dcd723c1c 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,386 @@
# @object-ui/core
+## 17.7.0
+
+### Minor Changes
+
+- 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` 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.
+- 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`.
+- 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
+
+- 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.
+- 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.
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [3c73d99]
+- 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 068fcc60f..39e1a5744 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 9e3b4d302..5e1f0eae0 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 38fbec8df..66611aca1 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 731d4a9c8..82215ea6f 100644
--- a/packages/data-objectstack/CHANGELOG.md
+++ b/packages/data-objectstack/CHANGELOG.md
@@ -1,5 +1,113 @@
# @object-ui/data-objectstack
+## 17.7.0
+
+### Minor Changes
+
+- 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`.
+- 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
+
+- 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.
+- 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.
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [91783c4]
+- Updated dependencies [2d36552]
+- Updated dependencies [3c73d99]
+- Updated dependencies [4d73b07]
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json
index c4db2a5d9..c97bbcf31 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 c64ede524..f50f02769 100644
--- a/packages/fields/CHANGELOG.md
+++ b/packages/fields/CHANGELOG.md
@@ -1,5 +1,272 @@
# @object-ui/fields
+## 17.7.0
+
+### Minor Changes
+
+- 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
+
+- 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.
+- 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.
+- 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 `
Rich text
`,
+ 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 [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@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 5e442f18c..8226417c6 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 5fa542ac9..967c338ca 100644
--- a/packages/i18n/CHANGELOG.md
+++ b/packages/i18n/CHANGELOG.md
@@ -1,5 +1,317 @@
# @object-ui/i18n
+## 17.7.0
+
+### Minor Changes
+
+- 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: })`, 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.
+- 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
+
+- 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.
+- 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.
+- 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.
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [e719ebd]
+- Updated dependencies [fa429cf]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [f2158ec]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [91783c4]
+- Updated dependencies [2d36552]
+ - @object-ui/core@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index a1173f659..08ec2ead5 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 728f273d4..3b76ba3fa 100644
--- a/packages/layout/CHANGELOG.md
+++ b/packages/layout/CHANGELOG.md
@@ -1,5 +1,83 @@
# @object-ui/layout
+## 17.7.0
+
+### Patch Changes
+
+- 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 [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@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 6581d4f15..45bd6519c 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 4161e7dc5..8bc31c64d 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,5 +1,22 @@
# @object-ui/mobile
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [3c73d99]
+- 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 11bf8a8e9..caf6c9ba7 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 70e748b38..49230fbeb 100644
--- a/packages/permissions/CHANGELOG.md
+++ b/packages/permissions/CHANGELOG.md
@@ -1,5 +1,26 @@
# @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
+
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [3c73d99]
+- 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 5c457f527..9ba91d28c 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 028227253..d10cd5642 100644
--- a/packages/plugin-ai/CHANGELOG.md
+++ b/packages/plugin-ai/CHANGELOG.md
@@ -1,5 +1,47 @@
# @object-ui/plugin-ai
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-ai/package.json b/packages/plugin-ai/package.json
index c925294af..77323afe9 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 428938088..be5887a14 100644
--- a/packages/plugin-calendar/CHANGELOG.md
+++ b/packages/plugin-calendar/CHANGELOG.md
@@ -1,5 +1,90 @@
# @object-ui/plugin-calendar
+## 17.7.0
+
+### Patch Changes
+
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [0068348]
+- Updated dependencies [8a44390]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-detail@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/mobile@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-calendar/package.json b/packages/plugin-calendar/package.json
index b2b8d67e1..5bec20d43 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 f8807b8da..6c4649dbc 100644
--- a/packages/plugin-charts/CHANGELOG.md
+++ b/packages/plugin-charts/CHANGELOG.md
@@ -1,5 +1,144 @@
# @object-ui/plugin-charts
+## 17.7.0
+
+### Minor Changes
+
+- 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
+
+- 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.
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@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 ef57547a3..f2ef1019e 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 525453d4d..eb5662de9 100644
--- a/packages/plugin-chatbot/CHANGELOG.md
+++ b/packages/plugin-chatbot/CHANGELOG.md
@@ -1,5 +1,81 @@
# @object-ui/plugin-chatbot
+## 17.7.0
+
+### Patch Changes
+
+- 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`.
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@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 da3497e4c..04fb3e77f 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 0f5ae3c43..ffe459145 100644
--- a/packages/plugin-dashboard/CHANGELOG.md
+++ b/packages/plugin-dashboard/CHANGELOG.md
@@ -1,5 +1,326 @@
# @object-ui/plugin-dashboard
+## 17.7.0
+
+### Minor Changes
+
+- 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.
+
+### Patch Changes
+
+- 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.
+- 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 [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json
index 0c866f67b..9b58ef1ee 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 38c0b0fee..6b084d859 100644
--- a/packages/plugin-designer/CHANGELOG.md
+++ b/packages/plugin-designer/CHANGELOG.md
@@ -1,5 +1,117 @@
# @object-ui/plugin-designer
+## 17.7.0
+
+### Patch Changes
+
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [b2e85a9]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [9e22085]
+- Updated dependencies [b655a9d]
+- Updated dependencies [c574dfb]
+- Updated dependencies [02f48b6]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [8e00bfd]
+- Updated dependencies [8d37efb]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [83ec618]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [26a2238]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/data-objectstack@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-form@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/plugin-grid@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json
index 905734071..5ce5762e7 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 1076d9b13..feb6d2d46 100644
--- a/packages/plugin-detail/CHANGELOG.md
+++ b/packages/plugin-detail/CHANGELOG.md
@@ -1,5 +1,265 @@
# @object-ui/plugin-detail
+## 17.7.0
+
+### Minor Changes
+
+- 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.
+- 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 `` 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.
+
+### Patch Changes
+
+- 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.
+- 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 [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/permissions@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json
index 071e61823..25994af3b 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 9aca54cdc..f1b82ae11 100644
--- a/packages/plugin-editor/CHANGELOG.md
+++ b/packages/plugin-editor/CHANGELOG.md
@@ -1,5 +1,47 @@
# @object-ui/plugin-editor
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-editor/package.json b/packages/plugin-editor/package.json
index 0ac4c314e..0015b4620 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 8907c722c..395f47368 100644
--- a/packages/plugin-form/CHANGELOG.md
+++ b/packages/plugin-form/CHANGELOG.md
@@ -1,5 +1,145 @@
# @object-ui/plugin-form
+## 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.
+- 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 `` 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
+
+- 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 ``. 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.
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/permissions@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json
index c27f526da..c02879638 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 88b03a315..009a4a7ae 100644
--- a/packages/plugin-gantt/CHANGELOG.md
+++ b/packages/plugin-gantt/CHANGELOG.md
@@ -1,5 +1,67 @@
# @object-ui/plugin-gantt
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [0068348]
+- Updated dependencies [8a44390]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-detail@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json
index 6ef13f936..83ca6a4ab 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 06a5f2e8d..bddf6a0c2 100644
--- a/packages/plugin-grid/CHANGELOG.md
+++ b/packages/plugin-grid/CHANGELOG.md
@@ -1,5 +1,199 @@
# @object-ui/plugin-grid
+## 17.7.0
+
+### Minor Changes
+
+- 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 `` 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
+
+- 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` (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..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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/permissions@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/mobile@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-grid/package.json b/packages/plugin-grid/package.json
index 7883e0cd2..a533e1ccd 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 055d64165..49f8058d6 100644
--- a/packages/plugin-kanban/CHANGELOG.md
+++ b/packages/plugin-kanban/CHANGELOG.md
@@ -1,5 +1,67 @@
# @object-ui/plugin-kanban
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [0068348]
+- Updated dependencies [8a44390]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-detail@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json
index d30f7bbf5..4ab67722f 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 b539389c4..ff8491dfd 100644
--- a/packages/plugin-list/CHANGELOG.md
+++ b/packages/plugin-list/CHANGELOG.md
@@ -1,5 +1,264 @@
# @object-ui/plugin-list
+## 17.7.0
+
+### Minor Changes
+
+- 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
+
+- 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 [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/permissions@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/mobile@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-list/package.json b/packages/plugin-list/package.json
index 3eb263d6b..9f5b69220 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 e03920fb6..cdded96bf 100644
--- a/packages/plugin-map/CHANGELOG.md
+++ b/packages/plugin-map/CHANGELOG.md
@@ -1,5 +1,119 @@
# @object-ui/plugin-map
+## 17.7.0
+
+### Patch Changes
+
+- 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.
+- 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.
+- 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.
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json
index c4d6e79a5..e834a1987 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 71b410866..808375dfc 100644
--- a/packages/plugin-markdown/CHANGELOG.md
+++ b/packages/plugin-markdown/CHANGELOG.md
@@ -1,5 +1,47 @@
# @object-ui/plugin-markdown
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-markdown/package.json b/packages/plugin-markdown/package.json
index 85553a5af..9bf6bbca1 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 d46584102..b67b6b130 100644
--- a/packages/plugin-report/CHANGELOG.md
+++ b/packages/plugin-report/CHANGELOG.md
@@ -1,5 +1,68 @@
# @object-ui/plugin-report
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [9a1fb41]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [9e22085]
+- Updated dependencies [b655a9d]
+- Updated dependencies [c574dfb]
+- Updated dependencies [02f48b6]
+- Updated dependencies [a865c73]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [7a28e1e]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [4bb940b]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f1690d4]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/fields@17.7.0
+ - @object-ui/plugin-grid@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 e138a6354..c3ead0a02 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 1752c39e3..3e6c9162e 100644
--- a/packages/plugin-timeline/CHANGELOG.md
+++ b/packages/plugin-timeline/CHANGELOG.md
@@ -1,5 +1,59 @@
# @object-ui/plugin-timeline
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/mobile@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json
index 4bb697caf..855d50f54 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 70c8973ff..ef2ea6059 100644
--- a/packages/plugin-tree/CHANGELOG.md
+++ b/packages/plugin-tree/CHANGELOG.md
@@ -1,5 +1,58 @@
# @object-ui/plugin-tree
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json
index 3efeb26d0..579ff23a7 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 e0af743c3..4be72081d 100644
--- a/packages/plugin-view/CHANGELOG.md
+++ b/packages/plugin-view/CHANGELOG.md
@@ -1,5 +1,190 @@
# @object-ui/plugin-view
+## 17.7.0
+
+### Minor Changes
+
+- 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
+
+- 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`, 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.
+- 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 [3b147a3]
+- Updated dependencies [77f846a]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [3c9fca3]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [485f096]
+- Updated dependencies [9e22085]
+- Updated dependencies [b655a9d]
+- Updated dependencies [c574dfb]
+- Updated dependencies [02f48b6]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [20e317c]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [83ec618]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [38a9568]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [490f482]
+- Updated dependencies [26a2238]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-form@17.7.0
+ - @object-ui/plugin-grid@17.7.0
+ - @object-ui/react@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json
index aedb1d31f..4d5bf4139 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 06d4ccf64..04ddcedb1 100644
--- a/packages/providers/CHANGELOG.md
+++ b/packages/providers/CHANGELOG.md
@@ -1,5 +1,22 @@
# @object-ui/providers — Changelog
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [0e05aac]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [3c73d99]
+- 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 8f4c97516..d08ce5dcd 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 761adb0e2..c092a21a8 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 (`` `` ``, `` `` ``) 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 f2f6f79ac..2c748e790 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 c6d1af825..a79d27c01 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,151 @@
# @object-ui/react
+## 17.7.0
+
+### Minor Changes
+
+- 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 `` 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`.
+- 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.`"*. `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
+
+- 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).
+- Updated dependencies [b2e85a9]
+- Updated dependencies [77f846a]
+- Updated dependencies [100547e]
+- Updated dependencies [3a58149]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [c40f3b8]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [20e317c]
+- Updated dependencies [8e00bfd]
+- Updated dependencies [8d37efb]
+- Updated dependencies [a691c0b]
+- Updated dependencies [1e66879]
+- Updated dependencies [c5200f0]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [38a9568]
+- Updated dependencies [91783c4]
+- Updated dependencies [2d36552]
+- Updated dependencies [b2437a7]
+- Updated dependencies [7a90afd]
+- Updated dependencies [3c73d99]
+- Updated dependencies [fb96ecb]
+- Updated dependencies [4d73b07]
+ - @object-ui/data-objectstack@17.7.0
+ - @object-ui/i18n@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 780971cbe..e1bc72683 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 63cb4bab6..8d71352df 100644
--- a/packages/runner/CHANGELOG.md
+++ b/packages/runner/CHANGELOG.md
@@ -1,5 +1,51 @@
# @object-ui/runner
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [3b147a3]
+- Updated dependencies [dd19463]
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [fe76ece]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [485f096]
+- Updated dependencies [b655a9d]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [ebce5a3]
+- Updated dependencies [6c5ee71]
+- Updated dependencies [6f017e9]
+- Updated dependencies [a691c0b]
+- Updated dependencies [0b1326d]
+- Updated dependencies [fa140b8]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [42887e0]
+- Updated dependencies [f90b8fb]
+- Updated dependencies [91783c4]
+- Updated dependencies [5a07e67]
+- Updated dependencies [2d36552]
+- Updated dependencies [490f482]
+- Updated dependencies [3c73d99]
+- Updated dependencies [c86185e]
+- Updated dependencies [4d73b07]
+ - @object-ui/components@17.7.0
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+ - @object-ui/plugin-charts@17.7.0
+ - @object-ui/react@17.7.0
+ - @object-ui/plugin-kanban@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/runner/package.json b/packages/runner/package.json
index ac0769d5d..428a21437 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 195115f84..9c316f600 100644
--- a/packages/sdui-parser/CHANGELOG.md
+++ b/packages/sdui-parser/CHANGELOG.md
@@ -1,5 +1,55 @@
# @object-ui/sdui-parser
+## 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 (`` `` ``, `` `` ``) 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
### Minor Changes
diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json
index 756e4ee6a..1928d43ec 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 ee6986d27..1ccfe5062 100644
--- a/packages/types/CHANGELOG.md
+++ b/packages/types/CHANGELOG.md
@@ -1,5 +1,390 @@
# @object-ui/types
+## 17.7.0
+
+### Minor Changes
+
+- 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.
+- 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`.
+- 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.
+- 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
+
+- 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`, 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.
+- 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 (`` `` ``, `` `` ``) 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.
+- 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.
+
## 17.6.0
### Minor Changes
diff --git a/packages/types/package.json b/packages/types/package.json
index e65f4efde..f0192cddb 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 c9ae926d1..895443d93 100644
--- a/packages/vscode-extension/CHANGELOG.md
+++ b/packages/vscode-extension/CHANGELOG.md
@@ -1,5 +1,32 @@
# Changelog
+## 17.7.0
+
+### Patch Changes
+
+- Updated dependencies [100547e]
+- Updated dependencies [d7573b3]
+- Updated dependencies [bf3edfe]
+- Updated dependencies [0e05aac]
+- Updated dependencies [e719ebd]
+- Updated dependencies [f9e4f91]
+- Updated dependencies [fa429cf]
+- Updated dependencies [8ebd57f]
+- Updated dependencies [7138bc1]
+- Updated dependencies [cef27e2]
+- Updated dependencies [4e8622b]
+- Updated dependencies [dffd752]
+- Updated dependencies [a691c0b]
+- Updated dependencies [f2158ec]
+- Updated dependencies [78cbdb5]
+- Updated dependencies [6c6cee7]
+- Updated dependencies [91783c4]
+- Updated dependencies [2d36552]
+- Updated dependencies [3c73d99]
+- Updated dependencies [4d73b07]
+ - @object-ui/types@17.7.0
+ - @object-ui/core@17.7.0
+
## 17.6.0
### Patch Changes
diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json
index 110f58c29..8d0ae06ae 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",