`, the same `Omit`-with-named-narrowings form
-`metadata-admin/form-spec.ts` uses for `FormFieldSpec.options`, with the drop
-and the retained `default` each written out next to its reason.
-
-No behaviour change and no released surface moves: the type is module-local,
-the value it annotates arrives from `useMetadataItem` as `any`, and
-`StateMachineView`'s `labelOf` / `colorOf` / `initial` logic is untouched — the
-object-field `default` it reads stays the ruled-`enforce` key it always was.
diff --git a/.changeset/6888-location-residue-refusal-keyed.md b/.changeset/6888-location-residue-refusal-keyed.md
deleted file mode 100644
index 125bddc61c..0000000000
--- a/.changeset/6888-location-residue-refusal-keyed.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/fields': patch
-'@object-ui/i18n': patch
----
-
-Key `LocationField`'s THIRD refusal sentence — the residue arm — into the locale
-packs (objectui#6888).
-
-Typing a half that is only PARTLY a number (`12abc, 34`) is refused by
-`LocationField` with its own sentence, added by objectui#6715. objectui#6755 had
-ruled two weeks earlier that a widget's own refusal sentence goes through
-`useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences — but it was
-written on 2026-08-29 14:53 and this arm landed after, so it stayed a hard-coded
-English literal while its two siblings were keyed.
-
-**The consequence was worse than one more English string.** All three refusal arms
-render through the SAME `` and the same `refusalError` state, so after #6755
-landed that one line spoke the reader's language when the format or range arm fired
-and English when the residue arm did — objectui#4028's shape ("four Chinese labels
-around one English one") compressed into a single sentence position, which reads to
-a user as a bug rather than as a missing translation.
-
-**Arity is answered explicitly, not defaulted.** Unlike the other two sentences, this
-one had grammatical number: `verb` was `is not a number` / `are not numbers`, chosen
-in TypeScript. Handing a pack an English verb form through a `{{hole}}` gives it a
-fragment it cannot inflect around — Arabic has a DUAL, and two halves is exactly that
-case. So the verb is not a hole. It lives inside two SIBLING keys picked at the call
-site, `fields.location.refusedResidue` and `fields.location.refusedResidueOne`,
-following this repo's own plural convention rather than i18next's `_one`/`_other`
-suffixes — the same shape `RecordPickerDialog` already uses in this very defaults map
-(`lookup.recordCount` / `lookup.recordCountOne`), and for the reason `ReactionPicker`
-states in source: zh/ja/ko have no separate singular form, would legitimately omit a
-`_one` half, and `all-locales-key-parity` reads that as a missing key. The `ar` pack
-now uses its dual (`ليسا رقمين`), which the old implementation could not have produced.
-
-The English conjunction `' and '` and the coordinate NOUNS go the same way: each pack
-writes its own conjunction inside the two-half value, and `latitude` / `longitude`
-become `fields.location.latitude` / `fields.location.longitude`, keyed once each and
-interpolated into both arities so no locale holds two spellings of the same word. The
-only holes carrying untranslated data are `{{text}}` / `{{otherText}}` — the
-characters the person actually typed.
-
-**No behaviour moves.** The English values are byte-identical to the literal they
-replace in both arities, verified by `check:i18n-drift` (0 en values changed, 4 added)
-and by objectui#6715's own `LocationField.strictNumeric.test.tsx` and `plugin-form`'s
-`ObjectForm.locationResidue.test.tsx` passing untouched. Provider-less rendering is
-unchanged, the refusal itself is unchanged, and the four new keys are bound from here
-on by `check:i18n-keys` and `all-locales-key-parity` like their three siblings.
diff --git a/.changeset/6896-retire-chart-inline-data.md b/.changeset/6896-retire-chart-inline-data.md
deleted file mode 100644
index 835f5cade4..0000000000
--- a/.changeset/6896-retire-chart-inline-data.md
+++ /dev/null
@@ -1,83 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Retire `ChartDataSeries.data`, and correct `ChartSchema.categories`' prose to the read it
-has always had (objectui#6896, ADR-0049 enforce-or-remove; maintainer ruling 2026-08-31).
-
-⚠️ **BREAKING for anyone authoring a static `ChartSchema` node**, shipped as `minor`
-because this repository's `major` is a cross-repo pin to `@objectstack`'s major rather than
-a severity dial. The break is announced here, which is the channel that carries it.
-
-## `ChartDataSeries.data` — RETIRED
-
-`data: number[]` was **required** on every authored series and read by nothing.
-`normalizeChartSchema`'s `normalizeSeries` (`@object-ui/plugin-charts`) reads `dataKey` /
-`name`, `label`, `chartType` / `type`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis`
-and `color` — `data` is not among them. Rows come from the chart node's **chart-level**
-`data`, a key `ChartSchema` never declared at all, which survives only because `BaseSchema`
-carries an index signature and so suppresses excess-property checking on chart literals.
-The declaration therefore demanded numbers no reader consumed, and no author could omit
-them.
-
-FROM → TO:
-
-- `data: number[]` (required) → **`data?: never`**, an ADR-0049 retirement tombstone. Put
- the rows on the chart node's chart-level `data`, name the column with the series' `name`
- (or `dataKey`), and put the category axis on `xAxisKey`.
-
-The accept set moves in **both** directions, and both are deliberate:
-
-- **narrowing** — `{ name: 'Revenue', data: [1, 2, 3] }` was accepted and is now refused;
-- **widening** — `{ name: 'Revenue' }` was refused (`data` was required) and is now
- accepted, which is the shape the renderer has always read.
-
-Deleting the member outright was the option **not** taken: `ChartDataSeriesSchema` is a
-non-strict `z.object`, which strips an undeclared key in silence — one silent no-op traded
-for another. The tombstone keeps the key declared and unwritable, so an authored value is a
-**named refusal carrying its own remedy**: `?: never` on the interface (a `tsc` error at the
-authoring site) and `retirementTombstone()` on the Zod mirror (`code: 'invalid_type'`, the
-key named in the issue `path`, the migration note as the message). Per the ruling —
-创业阶段不渐进 — the tombstone is immediate: there is no deprecation window and no
-dual-reading period.
-
-## `ChartSchema.categories` — NOT retired; its prose was wrong
-
-The key keeps its behaviour. It is read as an **alternative series list**, consulted only
-when `series` is absent, each entry normalized through the same series normalizer where a
-bare string means `{ dataKey }` — so the strings name **columns to plot**. The category axis
-comes from `xAxisKey` / `xAxis`. The docblock said "X-axis labels/categories", so an author
-following the documentation got a different chart from the documented one. Prose follows
-machine: the docblock, the `ChartDataSeries` header (which promised numbers "positionally
-aligned with the chart's `categories`", a model that never existed) and the Zod
-`.describe()` were corrected to the read. No behaviour changed, and `categories` remains
-writable.
-
-## The census behind the retirement, re-measured on this branch
-
-Re-measured at merge-base `2c3cd1b75` rather than inherited from the card, with a control
-that had to hit in the same query — the instrument was not blind: it scores
-`packages/types/src/__tests__/report-schema-authoring-face.test.ts` **4** authoring sites.
-
-⚠️ One correction to the record the ruling rests on. The ruling states *zero* authorship of
-a populated `series[].data` outside tests across `packages/` / `apps/` / `examples/`. The
-re-measurement finds **one such site inside those roots** —
-`packages/types/examples/data-display-examples.json` (2 series) — plus **four outside**
-them, in documentation: `content/docs/api/schema-reference.md` (3) and
-`content/docs/core/report-schema.mdx` (1).
-
-Every one of the five is documentation or an unreferenced example, **not a consumer**: none
-is imported, type-checked, parsed by a test or rendered anywhere, `packages/types` does not
-publish its `examples/` directory, and each authors the *documented* model — month names in
-`categories` next to inline `series[].data` — which renders an empty chart today, because
-`data` is dropped and `categories` is ignored whenever `series` is present. They are
-instances of the divergence this change closes rather than users of a working inline-data
-model, so the ruling's conclusion is unaffected. Their migration is filed separately; until
-it lands, an author copying them now trips the tombstone and reads the remedy instead of
-being silently dropped.
-
-Pinned in `packages/types/src/__tests__/chart-inline-data-retired.test.ts` — both channels,
-the announcement itself, and a counter-probe that builds the deletion this retirement did
-not choose and measures the contrast in the same run — and in
-`packages/plugin-charts/src/normalizeChartSchema.test.ts`, where the `categories` read now
-has behaviour coverage it never had.
diff --git a/.changeset/6898-objectgrid-select-fls.md b/.changeset/6898-objectgrid-select-fls.md
deleted file mode 100644
index 1ad21568fd..0000000000
--- a/.changeset/6898-objectgrid-select-fls.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-ObjectGrid: field-level security on the server `$select` projection
-(objectui#6898) — the FETCH half of the gap objectui#6799 closed on the RENDER
-half.
-
-`getSelectFields()` built the projection from the authored `columns` / `fields`
-with no FLS gate, so after objectui#6799 hid the column the field name was still
-being ASKED for. `perms.checkField(object, field, 'read')` now gates the
-projection, on both authored arms and on the predicate-operand harvest.
-
-Measured, because the grade depended on it: ObjectStack's own server enforces
-FLS on the RECORD, not on the projection — `plugin-security`'s read middleware
-deletes an unreadable key from every returned row, and its `predicate-guard`
-says in terms that the projection is deliberately unguarded because the masker
-strips the value anyway (pinned over real HTTP by objectstack's
-`showcase-fls-read-mask-strip.dogfood.test.ts`, where `?select=name,`
-answers 200 with the key absent). So against ObjectStack this is
-defence-in-depth; it becomes load-bearing for any backend that does not strip.
-
-Two limits are deliberate and pinned:
-
-- Only keys the object DECLARES are judged. `checkField` answers `false` for a
- field no policy mentions, so judging an undeclared key would strip a host's
- derived or joined column out of its own query.
-- `id` survives even a policy that denies it, structurally — `ensureId` composes
- after the gate — so row navigation cannot break. Readable predicate operands
- are untouched, so objectui#3501 does not regress.
-
-The fetch effect now also depends on `perms.isLoaded`: `/me/permissions`
-resolves asynchronously, so without it nothing would rebuild the projection
-after the policy answered and the gate would never run on the only fetch most
-grids make.
diff --git a/.changeset/6906-hook-selftests-collector.md b/.changeset/6906-hook-selftests-collector.md
deleted file mode 100644
index a54a1a44ab..0000000000
--- a/.changeset/6906-hook-selftests-collector.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
----
-
-CI-only change: no published package's `src/` changed.
-
-`.github/workflows/hook-selftests.yml` now **discovers** the matrices it runs
-instead of hand-enumerating them. Its single step runs
-`find .claude/hooks -type f -name '*.selftest.sh' | sort` and executes each hit,
-so a new `.claude/hooks/**/*.selftest.sh` is picked up with no edit to the
-workflow — the property the file's own header already claimed ("the standing
-caller for `.claude/hooks/*.selftest.sh`", whose matrices "are meant to grow in
-`.claude/**` WITHOUT this file being touched") and did not have: the previous
-`steps:` were a hand-kept list, and `guard-tree-enum.selftest.sh` had to be
-added to it by hand while the sibling repo's collector picked the same file up
-automatically.
-
-Ported from objectstack's collector in its `lint.yml`, keeping both load-bearing
-properties: an **empty** discovery is RED (a step that verified nothing is not a
-pass, so a renamed or moved directory cannot degrade the gate into a silent
-no-op), and the loop **tolerates and collects** rather than sequencing, so one
-red matrix cannot abort the run and hide the others — it still fails the job and
-names every matrix that failed.
-
-Coverage is unchanged, measured rather than asserted: discovery returns exactly
-the three self-tests the removed steps ran, and no self-test file was moved,
-renamed or skipped. Does not modify the hooks or their self-tests (`.claude/**`
-is governed surface). Paired write:
-`content/docs/guide/ci-cd-pipeline.md` (the workflow's own section hand-listed
-two of the three matrices). `scripts/dependabot-merge-gate.mjs` was read and
-left untouched — its `OPTIONAL_CONTEXTS` description of this check is already
-count-free.
diff --git a/.changeset/6907-gantt-date-spelling-rule.md b/.changeset/6907-gantt-date-spelling-rule.md
deleted file mode 100644
index 78decea6dd..0000000000
--- a/.changeset/6907-gantt-date-spelling-rule.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/plugin-timeline': patch
----
-
-Spell a refused gantt date by a RULE, not by `String` (objectui#6907)
-
-`spellGanttDateValue` fills the `{{value}}` hole of the unusable-gantt-date
-alert, whose job is to name the value the author wrote. Its `String(value)`
-fallback was written when nothing but `[object Object]`-shaped values could
-reach it; objectui#6905's type rule routes the whole non-date type space
-through it, and it failed three ways — measured on `b458300ca`:
-
-- it **VANISHES**: `endDate: []` rendered "endDate is , which is not a valid
- date", the value gone from its own sentence;
-- it **LIES**: `['2024-01-01']` read as `2024-01-01` and `[0]` / `0n` as `0` —
- a text that IS a valid date, and a number that IS an accepted one (`0` is a
- kept gantt date), so the author was told a correct-looking value was invalid
- with no hint the wrapper was at fault;
-- it **THROWS**: `{ toString() { throw } }`, a throwing `Symbol.toStringTag`
- getter and `Object.create(null)` each crashed the render outright. #6759 built
- this helper "total by construction" and #6905 made that load-bearing, but the
- type gate only stopped `new Date` from throwing — `String(value)` handed
- control to author code one line later, so the crash class moved into the
- speller instead of going away.
-
-The rule now recorded on the helper:
-
- Spell the value when the LANGUAGE owns its spelling.
- Name its TYPE when producing text would run AUTHOR code.
-
-Every primitive keeps a spelling fixed by the grammar, so it is spelled as the
-author typed it — including `bigint`, which gains its `n` (`0n`, no longer the
-accepted `0`). A `Date` uses `Date.prototype.toString.call`, byte-identical to
-`String` but not hijackable by a subclass. Everything else is named:
-`an array`, `a function`, `an object`, chosen with `Array.isArray` and `typeof`,
-which read no author-controlled property.
-
-`JSON.stringify` is refuted, not overlooked: it throws on a `bigint` and on a
-cycle, which would put the crash class straight back. A bounded rendering is
-refused on the same ground — even an element count is not total, because
-`Array.isArray` is true of a Proxy whose `length` trap throws.
-
-Which values are refused is unchanged (that is objectui#6781's ruling), the
-`undefined` / `null` / quoted-string spellings pinned by #6759 and #6770 do not
-move, the shared inverted-range diagnostic reads identically, and no new i18n
-key is added — the article rides in the existing `{{value}}` hole.
diff --git a/.changeset/6909-fieldeditwidget-dom-pass-through.md b/.changeset/6909-fieldeditwidget-dom-pass-through.md
deleted file mode 100644
index 3dedb5d414..0000000000
--- a/.changeset/6909-fieldeditwidget-dom-pass-through.md
+++ /dev/null
@@ -1,60 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-`FieldEditWidget` now DELIVERS the DOM pass-through block it DECLARES
-(objectui#6909).
-
-Its props are `FieldWidgetComponentProps` — the controlled-input keys
-intersected with `FieldWidgetDomProps`, `AriaAttributes` and the open `data-`
-family — so a host could always pass `id`, `name`, `autoFocus`, `tabIndex`,
-`onBlur`, `onFocus`, `onClick`, any `aria-*` and any `data-*` with no type
-error. The body then destructured five keys and rendered the widget with those,
-so `autoFocus` was the ONLY survivor of the whole block and everything else was
-silently dropped. That is this package's own first-class defect class, named in
-`widgets/toDomProps.ts`: a key that type-checks, reads as supported, and
-silently never reaches the element (objectui#3290's `aria-required`,
-objectui#3222's validation slot).
-
-Not a widening, and not a contract change. The keys were already declared, and
-each widget still re-filters through its own `toDomProps` before anything
-reaches a DOM element — what any widget accepts or rejects is unchanged. The
-factory was simply the one link in the chain nothing bound to the declaration:
-`toDomProps` binds the WIDGET contract to its whitelist with compile-time
-assertions in both directions, and the factory sat above them, bound to
-neither.
-
-The fix hands the widget `toDomProps(props)` — this package's own executor —
-rather than a second key list written out in the factory. That reuse is the
-guard: `toDomProps.ts`'s direction-2 assertion already makes
-`keyof FieldWidgetDomProps extends DomPassThroughKey` a compile error to
-violate, so a key added to the declared DOM block now reaches the widget
-through this factory automatically. One mechanism, one judge — a private list
-here would have been free to drift, which is how the factory came to deliver
-one key out of seven.
-
-The forwarded set is a deliberate superset of `FieldWidgetDomProps`: it also
-carries `className` and `disabled`, declared on the controlled-input block and
-forwarded by the same executor for the reason stated there — withholding them
-makes it a silent styling- and interactivity-dropper. The semantic props
-(`field`, `value`, `onChange`, `readonly`, and `compact` for the relational
-pickers) stay explicit and are applied after the spread, so a host cannot
-displace them.
-
-**No host in this repo changes behaviour.** Measured on all three call sites
-before the fix: `ObjectGrid.renderCellEditor` passes `{ field, value, onChange }`,
-`InlineFieldInput` passes those plus `autoFocus` (the key that already worked),
-and `RequiredFieldsDialog` passes those plus `readonly`. None passes a dropped
-key, so this is a plain repair rather than a live regression — but
-`RequiredFieldsDialog` had already worked *around* the drop, wrapping each
-control in a `label` because "`FieldEditWidget` … takes no `id` to associate
-with". It does now.
-
-Also corrects a comment in `@object-ui/components`' `data-table.tsx` that this
-change falsifies. It justified the injected editor's document-level
-`pointerdown` listener partly with "`FieldEditWidget` forwards `autoFocus` and
-nothing else out of the DOM block, so a host handler could not reach the
-control through it even if one were passed" — no longer true. The listener is
-still load-bearing for the other half of that reason, which is untouched: the
-`renderCellEditor` context object has nowhere to put an `onBlur` in the first
-place. Comment only; no behaviour change in that package.
diff --git a/.changeset/6931-tombstone-guidance-remainder.md b/.changeset/6931-tombstone-guidance-remainder.md
deleted file mode 100644
index cb29f6bcdc..0000000000
--- a/.changeset/6931-tombstone-guidance-remainder.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/types': patch
----
-
-The remaining eleven ADR-0049 tombstones now refuse with their remediation text
-(objectui#6931).
-
-objectui#6105 converted nine tombstones on `StaticTableColumnSchema` to
-`retirementTombstone()`, which writes a guidance string ONCE into both
-author-facing channels — `z.never({ error })` (the parse-time issue message)
-and `.describe()` (generated JSON-Schema and docs). Eleven declarations were
-left on the bare `z.never().optional().describe(...)` spelling and kept
-emitting zod's own `Invalid input: expected never, received string`: which key
-is wrong, nothing about why it was retired or what to write instead.
-
-Five of those eleven sat on `StaticTableColumnSchema` itself, so an author of a
-static-table column read guidance on nine keys and zod's generic on five — a
-shape that teaches the message means something and then withholds it. This
-converts all eleven:
-
-- `StaticTableColumnSchema`: `headerIcon`, `fitContent` (objectui#6424),
- `format`, `options`, `currency` (objectui#6425)
-- `TableSchema`: `hoverable`, `striped` (objectui#5474)
-- `TimelineSchema`: `timeScale` (objectui#6355)
-- `MenuItemSchema`, both union arms: `type` (objectui#6523)
-- `ActionSchema`: `confirm` (objectui#4314) — the key that ESTABLISHED this
- convention, and the last one still answering with zod's generic message
-
-Authoring `timeScale: 'day'` on a timeline now reports `RETIRED
-(objectui#6355) — author scale instead`; authoring the structured `confirm`
-object on an action now reports `RETIRED (objectui#4314) — author confirmText
-instead`.
-
-The accept set is untouched. For every converted member, plus the nine already
-converted and six live-value controls, `safeParse` reports the same `success`,
-the same issue `path`, the same issue `code` (`invalid_type`) and the same
-`expected` (`never`) before and after — only the message differs. Every
-`.description` on the five affected schemas (132 members) is byte-identical:
-`retirementTombstone()` passes the same string `.describe()` already carried.
-
-One nuance for `MenuItemSchema`, which is a union: its top-level issue is
-still zod's own `invalid_union` / `Invalid input` at path `[]`, and the
-converted guidance rides the per-arm issues underneath it. That is a property
-of the union rather than of the tombstone, and it is pinned in the tests so the
-unchanged top-level message is not misread later as a failed conversion.
diff --git a/.changeset/6938-checkbox-wrapper-class.md b/.changeset/6938-checkbox-wrapper-class.md
deleted file mode 100644
index edd69c7d0b..0000000000
--- a/.changeset/6938-checkbox-wrapper-class.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Declare `wrapperClass` on `CheckboxSchema`, on both faces (objectui#6938 — the
-residue of that card; its `context-menu` half landed with objectui#6939 group 1).
-
-`packages/components/src/renderers/form/checkbox.tsx:36` reads
-`cn("flex items-center space-x-2", schema.wrapperClass)` — classes on the wrapper
-`div` around the box and its label — and neither the TypeScript interface in
-`packages/types/src/form.ts` nor the zod mirror in `zod/form.zod.ts` declared the
-key. It compiled through `BaseSchema`'s index signature and parsed through
-`.passthrough()`, admitted unexamined. The same key, on the same class of read, is
-declared on `FileUploadSchema` and `FilterBuilderSchema` (objectui#6150); the
-checkbox was left out only because its doc page's schema block is a six-line
-summary.
-
-**patch, not minor: the accept set only widens toward what already renders.** The
-key is optional; no document that validated before stops validating. In the value
-dimension the mirror now REFUSES a non-string `wrapperClass` it used to admit
-unexamined — enforcement of the declared type, not a new capability.
diff --git a/.changeset/6939-filter-builder-mirror.md b/.changeset/6939-filter-builder-mirror.md
deleted file mode 100644
index 18272d05b4..0000000000
--- a/.changeset/6939-filter-builder-mirror.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Repair the `filter-builder` mirror: the field key is `value`, the type
-vocabulary is the value families the component actually folds a column into,
-and a filter group is `{ id, logic, conditions }` (objectui#6939, maintainer
-ruling recorded 2026-09-02 — one of the eight groups on that card, dispatched
-as its own PR per the ruling).
-
-Three independent mis-declarations sat in one member, and each is a key-name or
-vocabulary **move** rather than a missing optional key:
-
-1. **`FilterFieldSchema` required `name`.** Every read site matches an entry by
- `value` — `fields.find((f) => f.value === …)` in `getOperatorsForField`,
- `changeField`, `getInputType` and `renderValueInput`, `fields[0]?.value` in
- `addCondition`, and `` in the field dropdown
- (`packages/components/src/custom/filter-builder.tsx`). `name` had zero read
- sites, and `FilterBuilderProps.fields` in that same file already declares
- `Array<{ value, label, type? }>`. `@object-ui/fields`' `deriveFilterFields`,
- the real producer that builds this list from an object schema, emits `value`
- too, and the published doc
- (`content/docs/components/complex/filter-builder.mdx`) has declared
- `value: string` all along.
-2. **Its `type` enum was `string | number | date | boolean | select`.** `string`
- is a phantom, and `text`, `datetime` and `time` — three of the six
- `FilterValueFamily` members the component folds a column into — were all
- refused.
-3. **`FilterGroupSchema` was `{ operator, conditions }`.** The gate is
- `isValidGroup`, which tests `Array.isArray(v.conditions)` and
- `v.logic === "and" || v.logic === "or"` and nothing else.
-
-All five `components-complex-filter-builder/*` catalog entries author
-`{ value, label, type }` fields and a `{ id, logic, conditions }` group — the
-registration's own `inputs` / `defaultProps` spelling — so the mirror refused
-them while the renderer drew them. Re-measured on `origin/main` at `3e01cb55f`,
-both faces untouched: four refusals at the root (`search-interface` roots at
-`stack`, so `objectui check` counts four for this row, and the `filter-builder`
-it wraps is refused on its own), and five renders this change leaves
-byte-identical (11 / 76 / 65 / 11 / 57 elements, same tag census, same
-`textContent` SHA-256).
-
-**This is `minor`, not `patch`, and the reason is that the accept set MOVES.**
-The ruling grades this class "patch where the accept set only widens toward what
-already renders"; that grading does not hold here. Three refusal classes are
-created, each of which was a legal document before:
-
-- **`fields[].name` as the field key.** `{ name, label, type }` now refuses:
- `value` is required and `name` is not declared, so a field entry spelled the
- old way has no identity at all. A document carrying BOTH keys still validates
- (the undeclared one is stripped), which is the migration path.
-- **`type: 'string'`.** Refused outright. It reached the text control only
- through the unrecognised-word fallthrough in `valueFamilyForFieldType` —
- measured indistinguishable from a nonsense spelling — so nothing that read the
- key ever saw it; but a document that carried it did validate before and does
- not now.
-- **`{ operator, conditions }` as the group shape.** Refused: `logic` is
- required. This is the loudest of the three at render time — a group spelled
- that way already failed `isValidGroup`, fell back to `EMPTY_GROUP` and drew an
- **empty board** (76 elements and three condition rows became 11 and none), so
- the mirror was blessing a shape that never rendered.
-
-The widening half, for completeness: `fields[].value`, `type: 'text'` /
-`'datetime'` / `'time'`, and the `{ id, logic, conditions }` group all become
-legal. On the TypeScript twin the same move applies to `FilterField.value` /
-`FilterField.type` and to `FilterGroup.logic`, so a consumer reading
-`field.name` or `group.operator` stops compiling. `major` is not available in
-this repository (`check-changeset-no-major`), so a breaking change is `minor`.
-
-**Two places this departs from a literal reading of the ruling, both measured
-and both flagged for contract review rather than made quietly:**
-
-- **`select` is RETAINED** in the type vocabulary. The ruling's six-member list
- inherits the finding card's description of `select` as "extra"; it is not.
- `selectLikeTypes = ["select", "status"]` gives it its own operator bucket
- (`equals` / `in` / `notIn`) and its own value control — measured, a `select`
- column draws the option-driven Select and no ` ` at all, against a text
- box for an unrecognised spelling. Dropping it would refuse a spelling this
- mirror accepts *today* and the renderer draws distinctly, which is a fresh
- instance of the class objectui#6939 exists to close.
-- **The group's `id` is declared OPTIONAL.** `isValidGroup` never consults it
- and nothing reads `filterGroup.id`; deleting it from an authored group renders
- byte-identically. Requiring it would invent a refusal the renderer does not
- make. It stays *declared* because a plain `z.object` strips unknown keys in
- silence, so an undeclared `id` would be admitted unvalidated — declaring it
- buys the type check (`id: 42` now refuses) for a key the catalog authors,
- `EMPTY_GROUP` emits and `onChange` round-trips.
-
-**What this does NOT reach, stated rather than left as an absence.** Two of the
-four census entries — `product-search` and `with-conditions`, plus the
-`filter-builder` nested in `search-interface` — still refuse afterwards, on a
-FOURTH divergence the ruling does not address: they author
-`conditions[].operator` as `eq` / `gt` / `lt`, while `FilterOperatorSchema` is
-the spec's canonical `equals` / `greater_than` / `less_than`. Swapping only
-those three spellings makes both entries parse, which is pinned, so the claim
-"the three ruled divergences are gone from all four" is measured. That
-vocabulary is a genuine fork needing its own ruling — the builder's dropdown ids
-are `notEquals` / `greaterThan`, which this mirror also refuses, while the
-canonical spellings it accepts render a **blank** operator trigger — and it is
-reported on objectui#6939 rather than decided here. Seven further live field
-types (`status`, `currency`, `percent`, `rating`, `lookup`, `master_detail`,
-`user`) each have their own bucket and control and are still refused; they were
-refused before this change as well, so that gap is pre-existing rather than a
-regression introduced here, and it is reported on the same card. The published
-doc for this component already offers all fourteen spellings and already marks
-`type` OPTIONAL, which the mirror still does not — a third declaration that
-agrees with the renderer, pinned here so the gap is measured rather than
-asserted, and left for the same review.
diff --git a/.changeset/6939-kanban-column-cards.md b/.changeset/6939-kanban-column-cards.md
deleted file mode 100644
index 293b4d8519..0000000000
--- a/.changeset/6939-kanban-column-cards.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Rename `KanbanColumn.items` to `cards`, in both halves of the published surface
-(objectui#6939, maintainer ruling 2026-09-02).
-
-**Breaking, deliberately.** `KanbanColumn` declared its card list as `items` in
-`complex.ts` and in the zod mirror `complex.zod.ts`. Every board reads `cards`.
-Measured on `origin/main` `78a3cc238`: `KanbanImpl.tsx` reads `.cards` on 12
-lines, `KanbanEnhanced.tsx` on 8, and `bucketCardsIntoColumns` twice more as
-`col.cards || []`; `.items` had **zero** read sites in either board (a
-same-shaped `.title` control on the same two files returns 8 and 3, so those
-zeros are readings and not a mis-shaped probe). Both catalog entries, the
-plugin docs and `content/docs/api/schema-reference.md` all author `cards`.
-
-The consequence was `declared !== enforced` on a published mirror: every
-authored kanban document failed `safeValidateSchema` with `: Invalid input`
-while rendering perfectly, which is how the type sat in objectui#6318's
-"carries a registered component type but did not validate" bucket.
-
-**Why the rename went this way and not the other.** Renaming the twelve read
-sites to `items` was considered and rejected: `bucketCardsIntoColumns` reads
-`col.cards || []`, so the `items` spelling buckets every column to zero cards.
-Measured through the render harness in
-`examples/schema-catalog/test/kanban-column-cards-6939.test.tsx`, the
-`basic-kanban-board` entry goes from 64 elements reading `To Do2 … Design new
-feature …` to 45 elements reading `No cards3 columnsTo Do0 …` — an empty board.
-The declaration, not the corpus, was the wrong side.
-
-**Migration.** If you author `KanbanColumn` objects against `@object-ui/types`
-or validate them through `@object-ui/types/zod`, rename `items` to `cards`.
-Documents that already author `cards` — which is every document in this
-repository, and what the boards have always rendered — need no change and now
-validate. Documents authoring `items` are refused rather than silently drawn as
-an empty board.
-
-`@object-ui/plugin-kanban` is unchanged; it already declared `cards`.
diff --git a/.changeset/6939-objectql-record-source-refinement.md b/.changeset/6939-objectql-record-source-refinement.md
deleted file mode 100644
index d1e5f5fe6a..0000000000
--- a/.changeset/6939-objectql-record-source-refinement.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
-and a refinement requires that at least one of `data`, `staticData`,
-`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
-this is one of the eight groups on that card, dispatched as its own PR per the
-ruling).
-
-Both renderers resolve their records from one of three keys, in this order —
-`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
-`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
-`objectName`. Both mirrors required `objectName` alone, so a document authored
-on `staticData` drew correctly and was refused by `safeValidateSchema` — six
-catalog entries, three per component.
-
-- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
- mirror and on the TypeScript twin in the same stroke, and each member ends in
- `requireRecordSource`, whose issue sits at the root path, carries
- `params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
- can supply.
-- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
- spelling `object-map` already used): it is the FIRST key that resolver reads
- and was undeclared on both faces, which would have left the refinement naming
- a key the validator had never heard of.
-
-**patch, not minor: the accept set only widens toward what already renders.**
-Every document that validated before still validates — `objectName` alone,
-including an empty one, still parses, because presence is `!== undefined` and
-not the renderer's truthiness. The one shape the refinement refuses (none of
-the three) was refused before too, when `objectName` was required. Documents
-the renderers already draw start validating.
diff --git a/.changeset/6939-overlay-trigger-mirror.md b/.changeset/6939-overlay-trigger-mirror.md
deleted file mode 100644
index 5a1eb782a3..0000000000
--- a/.changeset/6939-overlay-trigger-mirror.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Repair the `tooltip` and `context-menu` mirrors: declare the keys their renderers
-actually read, and stop requiring the `children` neither of them reads
-(objectui#6939, maintainer ruling recorded 2026-09-02 — this is one of the eight
-groups on that card, dispatched as its own PR per the ruling).
-
-Both members demanded `children` and omitted keys the renderer reads first, so
-`safeValidateSchema` refused two catalog entries that draw correctly:
-
-- **`tooltip`** now declares `trigger`, and `content` / `body` as the two halves of
- one read (`renderers/overlay/tooltip.tsx:28,31` — `renderChildren(schema.trigger)`
- and `schema.content || renderChildren(schema.body)`). The registration's own
- `inputs` list `trigger` / `content` / `body` and never `children`. `trigger`
- follows `HoverCardSchema` two entries below, which is the settled in-repo shape
- for this slot.
-- **`context-menu`** now declares `triggerClassName`, `contentClassName` and
- `modal` (read at `renderers/overlay/context-menu.tsx:87,88,91`), which survived
- only on `BaseSchema.passthrough()`.
-
-**patch, not minor: the accept set only widens toward what already renders.**
-Every key involved is optional, and `children` stays legal — it is `BaseSchema`'s
-own optional key, merely no longer demanded here. No document that validated
-before this change stops validating; documents the renderers already draw start
-validating. The TypeScript twins in `packages/types/src/overlay.ts` move in the
-same stroke, so the published declaration and the published validator keep saying
-the same thing.
-
-⛔ A tooltip's trigger is authored under `trigger`, never `children`: the catalog
-entry was already moved to `trigger` once on render evidence (objectui#4626 — it
-was a measured blank tile) and moving it back is a known regression.
diff --git a/.changeset/6939-tree-view-nodes-mirror.md b/.changeset/6939-tree-view-nodes-mirror.md
deleted file mode 100644
index 239c1e5d54..0000000000
--- a/.changeset/6939-tree-view-nodes-mirror.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Repair the `tree-view` mirror: `data` is optional, so the `nodes` spelling the
-renderer reads FIRST is a legal document on its own (objectui#6939, maintainer
-ruling recorded 2026-09-02 — this is one of the eight groups on that card,
-dispatched as its own PR per the ruling).
-
-`TreeViewSchema` REQUIRED `data`, the limb the renderer reads THIRD:
-
- const rawNodes = boundData || schema.nodes || schema.data || [];
- // packages/components/src/renderers/data-display/tree-view.tsx:105
-
-The registration's own `inputs` and `defaultProps` spell it `nodes`, and the
-four `components-data-display-tree-view/*` catalog entries ARE those
-`defaultProps` — so `safeValidateSchema` refused every one of them
-(`: Invalid input`) while the renderer drew them correctly. Re-measured on
-`origin/main` at `fe4e7a9e8`: four refusals, and four renders that are
-byte-identical under either spelling (28 / 28 / 12 / 34 elements, same tag
-census, same `textContent` SHA-256). Identical output under the "correction" is
-objectui#6318's own triage test for *the schema was the wrong side*.
-
-**For AUTHORS this widens on both faces.** `data` goes from required to optional
-on the mirror and on the TypeScript twin in the same stroke; nothing that
-validated before validates less, and no document that type-checked as a literal
-stops doing so. A document authored on `data` — such as the tree-view entry in
-`packages/types/examples/data-display-examples.json` — is untouched, and both
-spellings together stay legal.
-
-**For a READER of the TypeScript twin this is a narrowing, and that is the half
-worth stating.** `TreeViewSchema['data']` is now `TreeNode[] | undefined`, so
-code that read `schema.data` and relied on its presence needs a guard and will
-otherwise stop compiling (measured on a consumer probe: exit 0 before, `TS2322`
-plus `TS18048` after). The only in-repo reader already has that guard —
-`renderers/data-display/tree-view.tsx:105` reads
-`boundData || schema.nodes || schema.data || []` — and it type-checks clean, so
-nothing in this repository changes. An out-of-repo consumer that reads the key
-unguarded is the population this paragraph exists for.
-
-Still `patch`: the required-ness was never a guarantee the renderer honoured (it
-reads the key third, behind a default), the accept set only grows, and this is
-the same shape as the two sibling groups of this card that have already landed.
-
-**`data` stays DECLARED rather than being deleted**, and the difference is
-measured rather than assumed: `BaseSchema` already declares `data`
-(`z.any().optional()`; `data?: any` on the TS face), so removing the member
-would not reject the key — it would admit it *unvalidated* while the renderer
-went on reading it. Optional-and-typed is the only shape in which `declared` and
-`enforced` agree for a key that is still read.
-
-**No refinement was added**, deliberately, unlike this card's
-`object-map` / `object-gantt` group. A tree-view carrying no data source at all
-becomes legal here, and that admits no new rendering outcome: `{ data: [] }` was
-already legal and already drew the same empty tree, so an "at least one of
-`nodes` / `data` / `bind`" rule would forbid a spelling of an empty state the
-contract already permits rather than buy a guarantee.
-
-`nodes` and `title` are objectui#6150's declarations and are unchanged; that
-card declared the reads and said in as many words that relaxing `data` was a
-separate accept-set change. This is that change.
diff --git a/.changeset/6940-rowactions-boolean-mirror.md b/.changeset/6940-rowactions-boolean-mirror.md
deleted file mode 100644
index 845de6de5b..0000000000
--- a/.changeset/6940-rowactions-boolean-mirror.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`DataTableSchema.rowActions` validates as the boolean it has always been declared to be
-(objectui#6940, maintainer ruling 2026-09-02, director seat summon #8, option A).
-
-The hand-written zod mirror in `zod/data-display.zod.ts` declared
-`rowActions: z.array(z.any()).optional()`. Every other face of the same key says
-**boolean**: the TS declaration it mirrors (`rowActions?: boolean`), the renderer's
-destructuring default (`rowActions = false`), its two truthiness gates and two
-`colSpan` arithmetic sites, the registered authoring input
-(`{ type: 'boolean', label: 'Show Row Actions' }`), `defaultProps: { rowActions: true }`,
-and the renderer's own docblock example, which authors `"rowActions": true`. The mirror
-was the single outlier — and the published one, so `safeValidateSchema` refused the
-exact spelling the component's documentation, defaults and authoring UI all teach. Two
-shipped `examples/schema-catalog` entries (`user-table.json`, `full-featured-table.json`)
-failed validation for this and no other reason; both now validate **unchanged**.
-
-**Patch, not minor or major, and the reasoning is the ruling's own:** no author can have
-relied on an array value. The renderer never reads the array — it only truthiness-tests
-the key — so the smallest zod-valid array, `[]`, rendered the actions column identically
-to `true` (objectui#6318 measured both at 42 elements with the `Actions` header present,
-against 39 with the key absent). An array authored here could therefore never have
-carried meaning to any consumer: it either behaved exactly like `true` or, if empty,
-still behaved exactly like `true`. Narrowing it takes away a spelling that was accepted
-but inert, not one anything could have depended on.
-
-A `boolean | array` union was considered and **not** taken: it would permanently accept
-a shape the renderer cannot act on, which is the same second de-facto contract that the
-array spelling already was.
-
-The list view's same-named `rowActions` in `zod/objectql.zod.ts` — `z.array(z.string())`,
-the legacy bare-name action list on `ObjectGridSchema` — is a **different key** that is
-correct as it stands, is in parity with its own TS twin (`rowActions?: string[]`), and is
-not touched.
diff --git a/.changeset/6943-fields-tooling-dirs-out-of-emit.md b/.changeset/6943-fields-tooling-dirs-out-of-emit.md
deleted file mode 100644
index 25630dfee6..0000000000
--- a/.changeset/6943-fields-tooling-dirs-out-of-emit.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-Stop shipping `dist/__tests__/numberInputBrowserReadings.d.ts` in the published tarball
-(objectui#6943). `packages/fields/tsconfig.json` now excludes the tooling DIRECTORIES
-(`__tests__`, `__mocks__`, `__benchmarks__`), not just the `*.test.*` NAME.
-
-`numberInputBrowserReadings.ts` holds the measured Chromium/happy-dom readings the number
-widget suites share. It is deliberately not a `*.test.ts` — it carries no assertions — so
-the name-only exclude list did not catch it, and it was emitted into `dist` and published
-while its 79 neighbours in the same directory were kept out. That made
-`check:published-dist` red on `main`, and because the same script is the first link in
-`changeset:publish`, it also failed the publish command at its first step.
-
-This is the third instance of the same name-versus-directory mismatch (objectui#4006 here,
-objectui#4836 in plugin-grid / plugin-view / plugin-designer), so the exclude table is now
-the directory convention itself rather than a list of names to extend.
-
-Which program had to be fixed was measured rather than assumed, because this package's
-build is `tsc && vite build` and the `tsc` leg inherits the root's `noEmit`: run alone the
-`tsc` leg exited 0 and wrote zero files, while `vite build` alone produced the whole
-81-file output including the offending declaration. vite-plugin-dts is the emitting
-program, and it builds its declaration program from this package's `tsconfig.json`, so
-that is where the exclude belongs.
-
-No type coverage moves with the change and no API surface moves: `numberInputBrowserReadings.ts`
-is the only file the directory patterns newly remove from the build program, and the
-`tsconfig.test.json` chained off `type-check` already reads it as a transitive input of the
-three suites that import it. The name patterns stay, because 52 `*.test.ts(x)` files in this
-package sit outside any `__tests__/` directory.
diff --git a/.changeset/6950-binding-framework-injected-input.md b/.changeset/6950-binding-framework-injected-input.md
deleted file mode 100644
index 8f225fd009..0000000000
--- a/.changeset/6950-binding-framework-injected-input.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/sdui-parser': minor
-'@object-ui/core': patch
----
-
-`binding` on a component input is framework-set, not author-declared: `@object-ui/types` gains `InjectedComponentInput`, the `'field'` binding arm is retired from `@object-ui/sdui-parser`'s `RegistryConfigLike`, and the `as ComponentMeta` cast at the injection seam in `@object-ui/core` is gone (objectui#6950; maintainer ruling of 2026-09-07, director decision batch #69; ADR-0049 enforce-or-remove).
-
-**What was measured.** `binding` was published — the manifest serializer forwards it — and read — `validateTree` records a binding site for it — while `ComponentInput`, the authoring type every registration writes against, did not declare it. The one writer in the tree, `ELEMENT_DATA_SOURCE_INPUT`, therefore carried a hand-written inline type and reached a registration's `inputs` through `as ComponentMeta` in `Registry.register`. Declared narrower than enforced, on a published type — and `ComponentInput`'s own docblock listed `binding` among the forwarded per-input keys.
-
-**The ruling** answered the product question the card asked — may an ordinary registration declare a binding input? — with no. So:
-
-- **`@object-ui/types`** exports `InjectedComponentInput`, an `interface … extends ComponentInput` with the required marker `binding: 'object'`. `ComponentInput` itself does not change: no member is added, and authoring `binding` on a registration stays an excess-property `tsc` error — now on purpose and documented at the interface. The two tombstone docblocks that listed `binding` as a forwarded key now say it is forwarded from the framework's injected input, not authored.
-- **`@object-ui/core`** types `ELEMENT_DATA_SOURCE_INPUT` as `InjectedComponentInput` and splices it through a typed local; the cast is gone. Runtime behaviour is unchanged — the same key, `type`, `binding` and `description` reach the manifest, and `validateTree` still records the binding site.
-- **`@object-ui/sdui-parser`** narrows `RegistryConfigLike.inputs[].binding` from `'object' | 'field'` to `'object'`. The `'field'` arm had zero writers — every `binding:` literal in `packages/`, `apps/` and `examples/` is `'object'`, 7 of 7 at this change's merge-base — and nothing on either side of the manifest resolved a field binding. **Breaking, deliberately:** a config that feeds `manifestFromConfigs` a `binding: 'field'` input is now a type error instead of a manifest entry the server would never resolve. `ManifestInput.binding`, the manifest reader's vocabulary, is not narrowed by this change.
-
-If a real need for author-declared bindings is ever measured, it is filed as a widening of `ComponentInput` with the vocabulary decided then — not by putting the cast back.
diff --git a/.changeset/6951-text-value-retired.md b/.changeset/6951-text-value-retired.md
deleted file mode 100644
index 2aebdb2f93..0000000000
--- a/.changeset/6951-text-value-retired.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': minor
-'@object-ui/plugin-dashboard': patch
----
-
-**Breaking for authored metadata:** `TextSchema.value` is RETIRED (objectui#6951,
-maintainer ruling A1 of 2026-09-04; objectui#7016; ADR-0049 enforce-or-remove).
-A `text` node that authors `value` no longer validates: the parse fails loudly on
-the `value` path with the explanation in the message, the TS member is a
-`?: never` tombstone so the same document is refused at compile time, and the
-renderer no longer reads the key. Write `content`.
-
-**What was measured, on this branch's base.** `TextSchema` declared two spellings
-for its one content slot — `content` (read first) and `value` (the fallback limb
-of `{schema.content || schema.value}` at `renderers/basic/text.tsx:162` and
-`:167`) — both declared by objectui#6150, whose docblock called the pair "a
-dialect, not a design" and deferred the choice. The ruling's premise, that
-`value` is the minority spelling, was measured before any edit over the four
-roots it named: **776 `content`-only `text` nodes, 25 `value`-only, 0 authoring
-both** across `examples/` (674 / 13), `apps/` (59 / 0), the `examples/`
-directories under `packages/` (0 / 1) and `content/docs/**` (43 / 11) — a
-thirty-to-one majority for `content`, so the retirement went ahead as ruled.
-(A further 14 `{ value, label, type: "text" }` objects in the filter-builder
-catalog entries are field descriptors whose `type` is a field type, not `text`
-nodes, and were excluded by kind.)
-
-**Who is affected — a `value` authored on a `text` node:**
-
-```json
-{ "type": "text",
- "value": "Hello" } // ← was tolerated (rendered as the fallback)
-```
-
-now fails validation with:
-
-> RETIRED (objectui#6951) — `value` is no longer part of TextSchema; write
-> `content`. It was a second spelling of the one content slot, read only as the
-> fallback limb of `schema.content || schema.value`, and was retired under
-> ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling A1,
-> 2026-09-04). The renderer reads `content` alone now, so an authored `value`
-> would render nothing. Rename the key; the string is unchanged.
-
-**Two published faces, one retirement.** The TypeScript interface `TextSchema`
-(`@object-ui/types`, `layout.ts`) declares `value?: never`; the Zod mirror
-`TextSchema` (`@object-ui/types/zod`, `layout.zod.ts`) declares `value` as a
-`retirementTombstone()`, so the key stays DECLARED and is refused BY NAME —
-a plain deletion would have let an authored `value` ride `BaseSchema`'s
-`.passthrough()` into a silent blank, which is worse than the tolerated
-fallback it replaces. The `value?: string` members of `TextSpanSchema` and
-`TabsSchema` in the same file are other schemas' contracts and are unchanged.
-
-**`@object-ui/components`** — the `text` renderer renders `{schema.content}` at
-both arms (the `|| schema.value` limb is gone from each), and the `context-menu`
-renderer's built-in fallback trigger node now spells `content`. Nothing else in
-the package moves. **`@object-ui/plugin-dashboard`** — its three placeholder
-`text` nodes ("chart type is not supported yet", "Custom widget — set
-`component`…", the retired-widget notice) spell `content` so they keep rendering;
-their wording is unchanged and still pinned.
-
-**Who is NOT affected.** A document that already wrote `content` is untouched;
-`content`, `variant`, `align` and `className` are unchanged; `absent` stays
-valid (`{ "type": "text" }` still parses). Every in-repo document that authored
-`value` on a `text` node was rewritten to `content` in the same change: nine
-`examples/schema-catalog` entries, `packages/types/examples/zod-validation-example.ts`,
-eleven doc fences under `content/docs/`, and the `@object-ui/components`,
-`@object-ui/react` and `@object-ui/types/zod` README samples; the catalog is now
-pinned tree-wide against the retired spelling.
-
-**Migration:** rename `value` to `content` on every `text` node; the string is
-unchanged. If a document authored both, `content` was already the value that
-rendered — delete `value`.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the tolerated spelling. It is not `major` per
-this repo's fixed-group convention (objectui's own breaking changes ship as
-`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
-mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/6951-tree-view-data-retired.md b/.changeset/6951-tree-view-data-retired.md
deleted file mode 100644
index 6bdeff42c8..0000000000
--- a/.changeset/6951-tree-view-data-retired.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': minor
----
-
-**Breaking for authored metadata:** `TreeViewSchema.data` is RETIRED (objectui#6951,
-maintainer ruling B1 of 2026-09-04; ADR-0049 enforce-or-remove). A `tree-view`
-node that authors `data` no longer validates: the parse fails loudly on the
-`data` path with the explanation in the message, the TS member is a `?: never`
-tombstone so the same document is refused at compile time, and the renderer no
-longer reads the key. Write `nodes` — or bind the tree with `bind`, which is
-unchanged and still read first.
-
-**What was measured, on this branch's base.** `TreeViewSchema` declared two
-spellings for its one inline-nodes slot — `nodes` (read second) and `data` (read
-third: `boundData || schema.nodes || schema.data || []` at
-`renderers/data-display/tree-view.tsx:105`), both declared by objectui#6150.
-`data` had been REQUIRED until objectui#6939 / PR #7533 made it optional, so
-this retirement starts from a declared-and-optional member on both faces. The
-in-repo corpus at the retirement: seven `tree-view` nodes under
-`examples/schema-catalog` and `packages/types/examples` plus one `content/docs`
-fence — six on `nodes`, two on `data` (`packages/types/examples/data-display-examples.json`
-and `content/docs/api/schema-reference.md`), both rewritten; no package source
-authored either spelling.
-
-**Who is affected — a `data` authored on a `tree-view` node:**
-
-```json
-{ "type": "tree-view",
- "data": [{ "id": "root", "label": "Project" }] } // ← was tolerated (read third)
-```
-
-now fails validation with:
-
-> RETIRED (objectui#6951) — `data` is no longer part of TreeViewSchema; write
-> `nodes` (or bind the tree with `bind`). It was the second spelling of the one
-> inline-nodes slot, read only as the last limb of
-> `boundData || schema.nodes || schema.data || []`, and was retired under
-> ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling B1,
-> 2026-09-04). The renderer reads `bind` then `nodes` now, so an authored `data`
-> would render an empty tree. Rename the key; the array is unchanged.
-
-**Two published faces, one retirement — and why a tombstone, not a deletion.**
-The TypeScript interface `TreeViewSchema` (`@object-ui/types`, `data-display.ts`)
-declares `data?: never`; the Zod mirror `TreeViewSchema` (`@object-ui/types/zod`,
-`data-display.zod.ts`) declares `data` as a `retirementTombstone()`. `BaseSchema`
-already declares `data?: any` (`z.any().optional()` on the mirror), so DELETING
-the member would not have refused the key — it would have ADMITTED it,
-unvalidated, through the base member, and the renderer would have drawn an empty
-tree. The tombstone on the extended schema shadows the base member on both
-faces; the pin measures the base accepting the very document the extended
-schema refuses.
-
-**What the ruling kept, deliberately.** `nodes` stays OPTIONAL and no "at least
-one of" presence rule was added: `{ "type": "tree-view", "bind": "treeNodes" }`
-is a legal, rendering document (`bind` is the first source the renderer reads),
-and a bare `{ "type": "tree-view" }` stays legal as PR #7533 left it.
-`TreeNode.data` — the per-node payload on each tree node — is a different
-member on a different schema and is untouched.
-
-**`@object-ui/components`** — the `tree-view` renderer's read is
-`boundData || schema.nodes || []`; nothing else in the package moves.
-
-**Who is NOT affected.** A document that already wrote `nodes` (the four
-`components-data-display-tree-view/*` catalog entries and the nested tree in
-`components-complex-resizable/editor-interface.json`) is untouched; `title`,
-`bind`, the selection / expansion keys and `className` are unchanged. The
-catalog is now pinned tree-wide against the retired spelling.
-
-**Migration:** rename `data` to `nodes` on every `tree-view` node; the array is
-unchanged. If a document authored both, `nodes` was already the value that
-rendered — delete `data`.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the tolerated spelling. It is not `major` per
-this repo's fixed-group convention (objectui's own breaking changes ship as
-`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
-mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/6956-listview-export-options-spec-mirror.md b/.changeset/6956-listview-export-options-spec-mirror.md
deleted file mode 100644
index 076eb55ce2..0000000000
--- a/.changeset/6956-listview-export-options-spec-mirror.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Breaking for authored metadata:** the `exportOptions` member of the ListView
-zod mirror (`ListViewSchema` in `@object-ui/types/zod`) is now `@objectstack/spec`'s
-own `ListViewSchema.shape.exportOptions`, bound by reference rather than
-restated (objectui#6956). A `list-view` document that authors the retired `'pdf'`
-format — in either spelling, `exportOptions: ['csv', 'pdf']` or
-`exportOptions: { formats: ['pdf'] }` — or a sixth key on the object form
-(`{ formats: ['csv'], compression: 'gzip' }`) no longer validates through this
-package's mirror. It never validated at the platform's publish gate:
-`@objectstack/spec` 17.0.0 removed `'pdf'` from the format enum (objectstack#8010;
-PDF export itself was declined as objectstack#1301 NOT_PLANNED) and made the
-object form strict, so the mirror was passing locally what the platform refuses
-with an `os migrate meta --from 16` prescription — an author saw green here and
-a refusal upstream. `streaming`, the fifth spec key, is now declared on this face
-(the renderer honoured it; no local declaration carried it).
-
-**What was measured, on this branch's base.** The mirror declared a pre-#8010
-shape of its own — `'pdf'` in both branches, no `streaming`, a non-strict
-`z.object` — and `ListViewInferred` is `z.input` of that mirror, so the
-`ListViewSchema` TYPE the ListView renderer is written against disagreed with
-its sibling `ObjectGridSchema['exportOptions']` (the clean five-key
-`ListViewExportOptions`), and the renderer could only read `streaming` through
-`as any`. Against the installed pin (`@objectstack/spec@17.2.0`, not a working
-tree), `ListViewSchema.shape.exportOptions` from `@objectstack/spec/ui` lifts
-`['csv', 'xlsx']` to `{ formats: ['csv', 'xlsx'] }`, refuses `['csv', 'pdf']`
-with the migration prescription, refuses `{ formats: ['csv'], compression: 'gzip' }`
-(strict), and accepts `{ formats: ['csv'], streaming: true }` with the value
-intact. The mirror now IS that schema object, so the four verdicts are the
-spec's by construction; `export-options-spec-parity.test.ts` pins the identity,
-the four verdicts, and the survival of `streaming` through a parse.
-
-**The TS face follows.** `ListViewSchema['exportOptions']` is now the spec's
-INPUT type: `ListViewExportFormat[] | ListViewExportOptions` — the bare array
-stays admissible on input because nothing on the render path parses, and the
-object arm IS the same `ListViewExportOptions` that `ObjectGridSchema` and
-`NamedListView` carry. One spec key, one type, on every local authoring surface;
-`'pdf'` is a compile-time refusal in both spellings.
-
-**Who is NOT affected.** A document authoring `['csv', 'xlsx']`,
-`{ formats: ['csv', 'json'] }` or any combination of the five spec keys is
-untouched; absent stays valid; the member's description is now the spec's own
-text. The spec's parse-time lift of a bare array to `{ formats }` now runs for
-whoever parses through this mirror as well.
-
-**Migration:** delete `'pdf'` (the surviving formats are `'csv'`, `'xlsx'` and
-`'json'`); delete any key outside `formats` / `maxRecords` / `includeHeaders` /
-`fileNamePrefix` / `streaming`. `os migrate meta --from 16` lists the mechanical
-edits for existing sources.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the tolerated value. It is not `major` per
-this repo's fixed-group convention (objectui's own breaking changes ship as
-`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
-mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/6956-plugin-list-export-options-casts.md b/.changeset/6956-plugin-list-export-options-casts.md
deleted file mode 100644
index 3c632ff3c7..0000000000
--- a/.changeset/6956-plugin-list-export-options-casts.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-`ListView` reads `exportOptions.streaming` without a cast (objectui#6956). The
-two `as any` reads — the `exportableFormats` server-availability check and
-`handleExport`'s server-eligibility gate — and the `'pdf'` in the bare-array
-fold's cast are gone: the `ListViewSchema` type now carries `streaming` and not
-`'pdf'`, because `@object-ui/types`' zod mirror binds the spec's `exportOptions`
-field by reference. No behaviour change: the same formats are offered,
-`streaming: false` still forces the client-side path, and the bare-array fold
-(`resolvedExportOptions`, a stored `['csv', 'xlsx']` folded to `{ formats }`)
-STAYS — nothing on the render path parses and `ObjectView` forwards a stored
-value verbatim, so the spec's parse-time lift never runs before this renderer
-and the fold is load-bearing rather than legacy. A `'pdf'` stored before the
-retirement still arrives as data and is still dropped from the export menu with
-the existing one-time warning.
diff --git a/.changeset/6958-visiblewhen-clears-the-field-it-hides.md b/.changeset/6958-visiblewhen-clears-the-field-it-hides.md
deleted file mode 100644
index 4042a06dea..0000000000
--- a/.changeset/6958-visiblewhen-clears-the-field-it-hides.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/components': minor
----
-
-A field its own `visibleWhen` hides is now cleared, so it stops carrying a stale value
-to the server (objectui#6958).
-
-**Breaking, deliberately.** Until now a field the form renderer hid because the field's
-own `visibleWhen` (or its deprecated view-level sibling `visibleOn`) resolved FALSE kept
-its value in form state and submitted it anyway. Measured against a real running app: an
-object with four mutually-exclusive party columns and a type column naming which one
-applies could not be saved once a party column had been filled and then hidden — the
-server refused the row, correctly, by naming a column that was no longer on screen to
-clear. On the edit path the stored value was re-sent on **every** attempt, so such a
-record could never be retyped through the UI at all. The perverse consequence: *not*
-declaring `visibleWhen` produced an uglier but strictly **more usable** form, because the
-offending column stayed visible and therefore clearable.
-
-**What changes.** When a field's own visibility verdict goes VISIBLE → HIDDEN and the
-field holds a value, the renderer clears it: `null` for a scalar, `[]` for a multi-value.
-The key is deliberately PRESENT and `null` rather than withheld — objectui#6848 measured
-the write contract (`driver-memory` merges `{ ...stored, ...data }`, `driver-sql` issues
-`SET` for the keys present), so an absent key means "leave it unchanged" and only an
-explicit `null` overwrites the stored value. Omitting the key would have left the edit
-path exactly as dead as before.
-
-**What deliberately does NOT change.** Only the field's own conditional-visibility
-predicate clears. A field claimed by a hidden section (objectui#6236), a field on a hidden
-tab (objectui#6237) and a statically `hidden: true` field all keep the ruled semantics —
-visibility decides what is DRAWN and nothing else, and their values still submit. A broken
-predicate still fails OPEN, so a typo cannot silently null a stored column. The clear is
-transition-only and never writes over an empty value, so merely opening a record cannot
-strip the stored values of columns the user never saw, and a create form still omits the
-key for a field it never populated (objectui#4069).
-
-**Migration.** If you relied on `visibleWhen` to hide a field while still submitting its
-value, that value is now cleared on the transition. Carry such a value on a statically
-`hidden` field, or on a field claimed by a conditionally-hidden section, both of which are
-unaffected.
diff --git a/.changeset/6959-gantt-lazy-read-flake.md b/.changeset/6959-gantt-lazy-read-flake.md
deleted file mode 100644
index dda172e9c3..0000000000
--- a/.changeset/6959-gantt-lazy-read-flake.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
----
-
-Test-only: `ObjectGantt.referenceArms-6837.test.tsx` now reads the chart through an
-asynchronous query instead of racing it. No published behaviour changes; no runtime code
-was touched.
-
-The block at `:257` mounted through the file's `mount()` helper, whose gate settles a MOCK
-CALL — `find('task', { $expand })` was ISSUED — and then read the DOM synchronously with
-`getByTestId('gantt-view')`. Those are two different facts one promise-resolution apart:
-`ObjectGantt` flips `loading` false in the `finally` of `reload()`, i.e. after that find
-RESOLVES, so on return from `mount()` the component can still be painting its
-`Loading Gantt chart...` branch. The synchronous read raced it and lost twice in fifteen
-minutes in the merge queue — a heavier environment than PR CI — ejecting two pull requests
-that do not touch `plugin-gantt` at all, one of which passed and failed the queue on
-byte-identical content.
-
-The assertion is unchanged, and `mount()` keeps its find-call gate: the refusal probe above
-this block needs proof of the SCHEMA-dependent commit, which the chart's presence alone does
-not carry (`loading` flips false after the FIRST find resolves, before `objectSchema`
-lands). The two gates measure different facts, so the block now waits for both rather than
-trading one for the other.
-
-Measured rather than assumed: with the data source's resolution delayed 500ms — the timing
-FACT mutated, never the assertion — the old spelling throws
-`Unable to find an element by: [data-testid="gantt-view"]` against a DOM reading exactly
-`Loading Gantt chart...`, reproducing the queue failure, while the new spelling resolves
-through that fallback and reads `data-count=2`.
diff --git a/.changeset/6972-markdown-inert-keys-retired.md b/.changeset/6972-markdown-inert-keys-retired.md
deleted file mode 100644
index 1f19c51e0c..0000000000
--- a/.changeset/6972-markdown-inert-keys-retired.md
+++ /dev/null
@@ -1,115 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-markdown': minor
----
-
-**Breaking for authored metadata:** `MarkdownSchema.sanitize` and
-`MarkdownSchema.components` are RETIRED (objectui#6972, ADR-0049
-enforce-or-remove). A `markdown` node that authors either key no longer
-validates: the parse fails loudly on that key's path with the explanation in
-the message, and both TS members are `?: never` tombstones, so the same document
-is refused at compile time. The two keys do not share a disposition — triage
-recorded the asymmetry — so each is argued below.
-
-## `sanitize`
-
-**What was measured, on this branch's base.** `sanitize` was declared
-`?: boolean` with `@default true` on both published faces — `data-display.ts`
-and the Zod mirror — documented, and read by NOTHING. Worse than an ordinary
-inert key, it implied a switch that does not exist: sanitization is
-**unconditional**. `rehypePlugins` in `plugin-markdown/src/MarkdownImpl.tsx` is
-a module-level `const` array whose last link is `[rehypeSanitize, sanitizeSchema]`,
-handed to `ReactMarkdown` as-is — no ternary, no `if`, no runtime assembly.
-`MarkdownRenderer` forwards exactly `content` and `className`, and
-`MarkdownImplProps` accepts only those two. A repo-wide grep for
-`schema.sanitize` over `packages/` and `apps/` returns nothing, against a
-control of 20 `.tsx` files reading `schema.content` in the same query shape, so
-the zero is a reading, not a blind query. An author writing `sanitize: false`
-believed they turned XSS filtering off; one writing `sanitize: true` believed
-they turned it on. Neither was true.
-
-**Why remove and not enforce.** The enforce arm of enforce-or-remove for this
-key is a switch that DISABLES XSS sanitization, which is not an acceptable
-outcome; for `sanitize` the ruling collapses to remove (triage on
-objectui#6972).
-
-**Who is affected — a `sanitize` authored onto a `markdown` node:**
-
-```json
-{ "type": "markdown",
- "content": "# Hello",
- "sanitize": false } // ← was tolerated, changed nothing
-```
-
-now fails validation with:
-
-> RETIRED (objectui#6972) — sanitization is unconditional: rehype-sanitize is a
-> fixed last link of the markdown renderer's rehype chain, and no value of this
-> key ever switched it. There is no authored spelling that disables XSS
-> sanitization; delete the key.
-
-## `components`
-
-**What was measured, on this branch's base.** `components` was declared
-`?: Record` ("custom components for markdown elements") on both
-faces and read by NOTHING: the `components` map `MarkdownImpl` hands to
-`ReactMarkdown` is its own module-level `mdComponents` (the mermaid / metadata
-fence overrides), never merged with anything off the schema, and
-`grep -rn "schema.components"` over `packages/` and `apps/` returns nothing
-against the same `schema.content` control. The premise the PM declared
-falsifiable — *no host path consumes a `components` map* — was re-measured
-before this half was written: `MarkdownImplProps` has no such prop, `LazyMarkdown`
-receives only `content` and `className`, and no plugin API, app-shell or runner
-site passes one.
-
-**Why remove and not wire, and why not a runtime slot.** A map of React
-component overrides is not a value a JSON document can author — the same shape
-as the handler keys objectui#6124 retired ("JSON has no function value"). The
-`runtime-slot` disposition keeps a TypeScript twin callable when a host-supplied
-value actually reaches a renderer; nothing reaches this one, so there is no twin
-to keep and the TS face refuses it outright. This half is the PM's disposition
-under a declared veto window on objectui#6972, not a maintainer ruling; the PR
-stays draft for contract review. A real override slot must arrive as a proposal
-WITH its enforcing reader, not by reviving this key.
-
-```json
-{ "type": "markdown",
- "content": "# Hello",
- "components": { "h1": "h2" } } // ← was tolerated, changed nothing
-```
-
-now fails validation with:
-
-> RETIRED (objectui#6972) — never read: the markdown renderer forwards only
-> `content` and `className`, and a map of React component overrides is not a
-> JSON-authorable value. Delete the key; the fenced mermaid / metadata block
-> overrides are the renderer's own fixed map, not an authoring surface.
-
-## Both keys
-
-**Two published faces.** `@object-ui/plugin-markdown` re-exports `MarkdownSchema`
-from `@object-ui/types` (one authority since objectui#6172) rather than
-declaring a copy, so the retirement reaches its consumers through the same
-declaration — which is why this changeset names the plugin as well: no plugin
-source changes, but the type its published face exposes narrows, and its own
-test now pins that the refusal arrives there.
-
-**Who is NOT affected.** A document that never wrote either key is untouched
-(`absent` stays valid), `content` and `className` are unchanged, and the
-renderer's behaviour is byte-identical — it sanitized unconditionally before
-and does now, and its fenced-block overrides are the same fixed map. One
-in-repo fixture authored either key
-(`packages/types/examples/data-display-examples.json#examples.markdown`,
-`"sanitize": true`); the key is deleted from it and the fixture is now pinned
-to parse green. No fixture, catalog entry, doc snippet, skill or app in this
-repository authored `components` on a markdown node.
-
-**Migration:** delete the keys. There is nothing to replace either with — the
-behaviour `sanitize` claimed to control is always on, and no authored spelling
-overrides markdown elements.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the tolerated key. It is not `major` per
-this repo's fixed-group convention (objectui's own breaking changes ship as
-`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
-mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/6977-hook-remediation-no-track.md b/.changeset/6977-hook-remediation-no-track.md
deleted file mode 100644
index 63225cdf2a..0000000000
--- a/.changeset/6977-hook-remediation-no-track.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
----
-
-PreToolUse-hook prose only: no published package's `src/` changed, so nothing ships.
-
-**Both worktree guards now print `--no-track` in the recipe they emit when they block.**
-`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh` each
-end their block message with the worktree recipe to run instead, and both created the branch
-with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and `AGENTS.md`; the
-hooks' copy was left behind, and it is the higher-leverage one — it is delivered at the exact
-moment an agent is about to act, so it is the copy most likely to be run verbatim.
-
-The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
-(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
-worktree of a repo shares. That write can fail *after* the branch is created, leaving a branch
-with no worktree — a half state the error text does not name. Read as "the worktree exists",
-the agent starts editing the shared primary checkout, which is the one thing these two guards
-exist to prevent. Measured on git 2.43.0 in #6976: `--no-track` removes the config write and
-still bases the worktree on `origin/main`, and `git push -u origin BRANCH` sets the upstream
-one command later.
-
-Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
-(`git fetch origin main && … origin/main`, landed for ui#6208), so the only surviving
-divergence from the prescribed form was the missing flag. One word added per file, net ±0
-lines in both.
-
-Deliberately unchanged, each for its own reason:
-
-- `.changeset/6208-worktree-recipe-fetch-base.md` quotes the pre-#6208 recipe as the record of
- a past change. Editing a historical changeset would rewrite that record (adjudicated in
- PR #6976).
-- The two `-cmp` comparison-tree lines in `CLAUDE.md` / `AGENTS.md` create no branch and take
- an explicit ref from the caller, so there are no upstream keys to write and the flag does not
- apply — the same reading #6208 recorded for them.
-- `.claude/hooks/guard-tree-enum.selftest.sh`'s allow-case is a command payload fed to a
- different guard to assert that ordinary commands pass through it. It is test input, not text
- any agent is told to run.
-
-Verified by re-running all three self-test matrices this repo ships (`guard-main-checkout-bash`
-121, `guard-shared-stash` 41, `guard-tree-enum` 36 — 0 failed each, unchanged from before the
-edit, because no case pins the remediation string), and by invoking both hooks directly and
-reading the text they emit.
diff --git a/.changeset/6980-save-gate-zod-advisory.md b/.changeset/6980-save-gate-zod-advisory.md
deleted file mode 100644
index 47bda5f28c..0000000000
--- a/.changeset/6980-save-gate-zod-advisory.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
----
-
-Records, at the Save gate, why the metadata editor's two author-time error
-sources are treated differently — and pins the asymmetry so it cannot be
-"fixed" by accident (objectui#6980). Comment + test only; no behaviour change.
-
-`ResourceEditPage`'s Save gate has one validation term, `inspectorBlocking`.
-The live client Zod pass (`validateMetadataDraft`) feeds the banner, the inline
-field errors and `previewDiagnostics`, and gates nothing. From outside that
-reads as a defect. Measured, it is the right shape, and the discriminator is
-whether the server is a backstop for the class:
-
-- Inspector issues (a CEL predicate that does not parse) have none —
- objectui#4306 measured such a formula saving 200 and publishing as the live
- field definition, and the runtime authoring gate reports only advisory
- findings on an already-successful save (#4133). The client is the only gate.
-- Schema issues have one — `saveMetaItem` runs the same contract, and `doSave`
- already maps its 422 / `INVALID_METADATA` / `INVALID_PAYLOAD` refusal back
- into the same inline issues. The live Zod pass is a preview of the server's
- verdict, not the thing keeping the draft out of storage.
-
-So the only failure a blocking Zod gate could add is the client being STRICTER
-than the server, which is not hypothetical (objectstack#5316: stored views
-carrying the platform's own `isPinned` / `sortOrder`). `clientValidation.ts`'s
-root-cure covers exactly one class of that skew — a top-level required field,
-absent, that the server marks optional (`path.length === 1`). Measured on the
-bundled spec 17.2.0, 14 of the 15 wired schemas reject an undeclared key, and
-`unrecognized_keys` arrives at `path.length` 0 or 2, which that filter passes
-through. A server that gains one authorable key would, under a blocking gate,
-dead-bolt Save on a draft the server accepts, with no on-screen editor able to
-take the key back out — the same wedge every gate in this family is built to
-avoid.
-
-`ResourceEditPage.schemaAdvisory.test.tsx` pins both halves against one draft:
-an undeclared root key is reported in the banner while Save stays enabled, and
-one added CEL parse fault on the same draft disables it.
diff --git a/.changeset/6982-clientvalidation-stale-cures.md b/.changeset/6982-clientvalidation-stale-cures.md
deleted file mode 100644
index 93cbaeb233..0000000000
--- a/.changeset/6982-clientvalidation-stale-cures.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Retire the unreachable flow node-type shim in the metadata admin's client
-validator, and re-pin the `sharing_rule` edit-door boundary on the reason that
-is still true.
-
-No published behaviour changes. The removed shim was measured unreachable on the
-resolved `@objectstack/spec` 17.2.0 — `FlowNodeSchema.type` is an open non-empty
-string, so the closed-enum mismatch it suppressed can no longer be produced, and
-the one issue that path still yields (`too_small` on the empty string) is one the
-shim already kept. The `sharing_rule` opt-out's membership is unchanged; only its
-stated reason and its pins moved.
diff --git a/.changeset/6985-wizard-card-r-alignment.md b/.changeset/6985-wizard-card-r-alignment.md
deleted file mode 100644
index 504e84a80f..0000000000
--- a/.changeset/6985-wizard-card-r-alignment.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/types': patch
-'@object-ui/plugin-form': patch
----
-
-Wizard view v1, the objectui half (Card R, objectui#6985) — alignment + pins for the
-ruled `type: 'wizard'` tightening (objectstack#13622 D1–D8, maintainer ruling
-2026-08-31; spec half objectstack PR #13733).
-
-The renderer was already aligned: `WizardStepConfig` carries no predicate/collapse
-keys (objectui#6237's ruled split), the wizard route drops-and-reports an authored
-step `visibleWhen`, and `allowSkip` has been navigation-freedom-not-validation-
-exemption since #2959. This card lands the residue:
-
-- **metadata-admin view create seeds one starter step for a wizard** (app-shell
- `anchors.ts`): the create body used to emit `sections: []` for every form type,
- which for `type: 'wizard'` is exactly the shape the tightened spec refuses at
- parse (D7 — a stepless wizard silently rendered as a plain simple form). Same
- seed-the-required-shape move the flow anchor makes for its `type` enum
- (objectui#2326). Other form types keep the bare `[]` — only the wizard variant
- refuses emptiness.
-- **`@object-ui/types` TSDoc states the ruled wizard boundary** where the shared
- section/form types restate the form-view family: `ObjectFormSection.visibleWhen`
- / `collapsible` / `collapsed` name the wizard drop + spec-door refusal;
- `ObjectFormSchema.sections` states sections-ARE-steps and array-order-is-step-
- order; `allowSkip` states the D4 semantics. Type SHAPES are unchanged — the
- spec's own ruled mechanism is a parse-time refinement over the single shared
- section schema (D2 option A), which these types mirror at the type level.
-- **Consumer-side behaviour pins** (`wizardRuledSemantics-6985.test.tsx`): the
- wizard-inert step keys are dropped, never honoured (a denying `visibleWhen`
- does not remove a step; `collapsible`/`collapsed: true` produce no collapse
- affordance, with a positive control on the affordance probe); the empty-steps
- wizard's measured degradation to a simple form is pinned as the shape the spec
- door now refuses (one-step wizards stay legal — no arity floor); array order
- is step order (with a reversed-array control).
-- **Installed-spec door pins** (`wizardSpecDoor-6985.test.ts`), gated on a
- capability probe of the installed `FormViewSchema` rather than a version
- string: the post-Card-S half (refusal messages, prescriptions, the authored-
- `false` collapse boundary, the wizard-scoped control) activates by itself on
- the lockfile bump that brings the tightening in; until then the pre-tightening
- half records the 17.2.x accept-set it measured. `steps:` is pinned refused on
- every spec line.
-
-No teaching material — the #13337/#13086 fence lifts only after both halves land;
-docs changes here are TSDoc/comments only.
diff --git a/.changeset/7004-cli-root-path-line.md b/.changeset/7004-cli-root-path-line.md
deleted file mode 100644
index 1b9708c16d..0000000000
--- a/.changeset/7004-cli-root-path-line.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/cli': patch
----
-
-`objectui validate` now says when a validation issue sits at the document root
-(objectui#7004, mechanical half).
-
-The printer guarded its Path line with `issue.path.length > 0`, so an issue at
-`path: []` printed no Path line at all — silent in exactly the case a reader
-most needs oriented. That case is the common one, not an edge: the CLI validates
-against `AnyComponentSchema`, a union over every component arm, so any document
-matching no arm reports a single top-level issue (`invalid_union` · `Invalid
-input` · root path). Authors saw a bare verdict on a whole document with nothing
-saying which node had been judged:
-
-```
-1. Invalid input
- Code: invalid_union
-```
-
-Every reported issue now carries a Path line; a root-level one reads
-`Path: (root)`, parenthesised so it cannot be mistaken for a real key named
-`root`. Non-root issues print their authored path exactly as before.
-
-Scope: the printer still reads only top-level issues. Whether a failing union
-should also surface its per-arm diagnoses — and if so which arm's — is an
-author-facing diagnostic contract left open on objectui#7004 for a maintainer
-ruling, and is deliberately not decided here.
diff --git a/.changeset/7004-cli-union-arm-selection.md b/.changeset/7004-cli-union-arm-selection.md
deleted file mode 100644
index 690dede100..0000000000
--- a/.changeset/7004-cli-union-arm-selection.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/cli': minor
----
-
-`objectui validate` now prints the failing union arm the document selected, instead of a
-bare "Invalid input" (objectui#7004, maintainer ruling 2026-09-02 — option B).
-
-`safeValidateSchema` checks a document against `AnyComponentSchema`, a `z.union`. When a
-document matches no arm, Zod reports ONE top-level issue — `invalid_union` · `Invalid
-input` · path `(root)` — and hangs every arm's real diagnosis off that issue's `errors`
-array, which nothing read. So a menu whose item used the divider spelling retired in
-objectui#6523 printed a bare verdict on the whole document, while the remediation text
-objectui#6931 wrote into that arm sat one level down, unreachable.
-
-**What is printed now.** When the top-level issue is a failing union:
-
-- the document's `type` selects exactly one arm ⇒ that arm's issues are printed beneath
- the entry as `1.1`, `1.2` … with their real paths (`Path: items → 0 → type`) and codes,
- and **nothing** from the other arms;
-- no arm accepts the `type` ⇒ `No arm accepts type "dropdwn-menu".` plus the nearest few
- of the accepted values, ranked by edit distance and **capped** at five
- (`MAX_UNION_ARMS_REPORTED`);
-- the document declares no `type` at all ⇒ the note says so and offers no candidates —
- "nearest" needs something to be near, and an alphabetical slice of 108 arm names
- presented as guidance would be a bogus suggestion;
-- a union with no `type` discriminator to select on — `MenuItemSchema`, whose two arms
- both declare `type` as an ADR-0049 retirement tombstone — reports every arm, labelled
- and capped by the same constant. This is the path that finally delivers the
- objectui#6523 text to the author.
-
-Printing EVERY arm was rejected in the ruling: `AnyComponentSchema` resolves to 108 leaf
-arms, so one mistyped `type` would have produced hundreds of lines.
-
-`objectui check` is unchanged and deliberately so: it has no zod-issue printer, using
-`safeValidateSchema(...).success` as a boolean recogniser. Printing issues behind a
-*negative* recognition would flood its report with diagnoses of non-ObjectUI files, the
-failure objectui#5127 and objectui#6075 exist to prevent.
-
-Nothing about which documents are ACCEPTED changes — this is diagnostic output only.
diff --git a/.changeset/7007-relatedlist-schema-chain-gate.md b/.changeset/7007-relatedlist-schema-chain-gate.md
deleted file mode 100644
index a19c1bed1f..0000000000
--- a/.changeset/7007-relatedlist-schema-chain-gate.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
----
-
-Test-only: `RelatedList.unmaterializedSort.test.tsx` now gates the `getObjectSchema` chain
-as well as the fetch chain. No published behaviour changes; no runtime code was touched.
-
-Both `#3950` refusal blocks settled only the FETCH chain — `columnSortable('name')` for the
-column headers, `h.schema?.type === 'data-list'` for the sort-button row — and then read a
-value that arrives on a DIFFERENT chain: `withheldFromServerSort` derives its verdict from
-`objectSchema.fields[field]`, fetched by the effect at `RelatedList.tsx:430`. With no schema
-in state yet the predicate falls through to `isUnmaterializedFieldType(undefined)`, which is
-`false` — "offer the sort" — so `Total` is still sortable and still has a button. The blocks
-were green only because that effect is declared before the fetch effect and both mocks
-resolve on the same microtask; nothing pinned that ordering, and a flaky RED in the merge
-queue ejects unrelated pull requests.
-
-Measured rather than assumed: with the timing FACT mutated and nothing else — the mock
-schema resolving after a 50ms delay instead of on the first microtask — the old spelling
-fails at `:123` (`expected true to be false`) and at `:153` (`expected Total
-to be null`), with the stored-column controls present in both blocks, reproducing the race.
-The new spelling passes under the same delay.
-
-Each block KEEPS its existing gate and ADDS the second one (the objectui#6959 precedent and
-its trap): the first gate proves the view committed with rows at all, which is what keeps
-`queryBy…).toBeNull()` from passing vacuously; the second proves the schema the verdict is
-derived from has landed. Trading one for the other would have left the block blind on
-whichever side it dropped.
-
-The gate is not `expect(getObjectSchema).toHaveBeenCalled()` — a mock CALL is issued one
-resolution before its value reaches state, which is the trap #6959 recorded. It awaits the
-promise the component itself awaited and flushes the commit through `act`.
diff --git a/.changeset/7008-field-edit-widget-host-plumbing.md b/.changeset/7008-field-edit-widget-host-plumbing.md
deleted file mode 100644
index fe56d13482..0000000000
--- a/.changeset/7008-field-edit-widget-host-plumbing.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/fields': minor
-'@object-ui/plugin-kanban': patch
----
-
-`FieldEditWidget` now delivers the NON-DOM half of the contract it declares (objectui#7008).
-
-objectui#7009 made the factory forward its declared DOM pass-through block. The rest of
-`FieldWidgetComponentProps` was still dropped: `error`, `onUploadingChange`, and the whole
-"Host plumbing" block (`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`,
-`emptyHint`, `onSelectRecord`, `onCreateNew`). A host could pass any of them with no type
-error and the widget never received it — the "declared but not delivered" class this
-package treats as first-class.
-
-`error` was the live one. `InlineFieldInput` has passed `error` into this factory since
-PR #7109 and the factory dropped it, so an inline-edit control that had failed validation
-never reported `aria-invalid`: a sighted user saw the red hint, a screen-reader user was
-told nothing. The kanban `RequiredFieldsDialog` had the same hole from the other side — it
-computes the validation state and could not hand it over — and now passes `error`, so its
-controls are marked. Delivering `error` buys the a11y MARKING only; the message text stays
-with the host, per the objectui#3222 contract.
-
-The keys travel through a new sibling executor, `toHostProps` (exported alongside
-`toDomProps`), never through the DOM whitelist — none of them is DOM-legal, and routing a
-`dataSource` adapter there is the `[object Object]` leak that whitelist exists to stop.
-Three compile-time assertions make the two executors partition the contract, so a future
-declared key cannot go undelivered silently.
-
-`dataSource` precedence is stated rather than left to emerge: a host's explicit
-`dataSource` prop WINS over `SchemaRendererContext`. That is the order `LookupField`
-already implements; the factory is a conduit and resolves nothing. A host that passes no
-`dataSource` keeps reading the context exactly as before, so no in-repo host changes
-behaviour.
diff --git a/.changeset/7014-q1-tier1-select-option-convergence.md b/.changeset/7014-q1-tier1-select-option-convergence.md
deleted file mode 100644
index 27c27512e2..0000000000
--- a/.changeset/7014-q1-tier1-select-option-convergence.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Converge the two named select-option types onto one spec-derived base (objectui#7014, Q1).
-
-`SelectOptionMetadata` (`field-types`, the object-metadata read model) and `SelectOption`
-(`form`, the SDUI form vocabulary) each restated the select-option vocabulary by hand.
-Both now extend the new `SelectOptionBase`, which derives the spec's keys from
-`@objectstack/spec/data` **by reference** and writes out only the divergences. A key the
-spec adds now reaches both faces with no edit here; a key it removes becomes a compile
-error at the sites that read it, instead of a hand copy that goes on compiling while the
-contract moves underneath it.
-
-**What widened.** Exactly one key, on one face: `SelectOptionMetadata` gains
-`default?: boolean`. It is a spec key that face could not describe before — ruled
-`enforce` on the object-field face (objectstack#7246), where the engine seeds a new
-record from the option marked `default: true` — and it arrives OPTIONAL, so every
-document that face accepted before is still accepted.
-
-**What narrowed.** Nothing. Both faces resolve to member-for-member what they resolved to
-before (`SelectOption` identically; `SelectOptionMetadata` identically plus `default`),
-pinned invariantly against the pre-convergence member lists in
-`select-option-tier1-convergence-7014.test.ts` so a future "unification" cannot quietly
-drop a key. `SelectOption.value` keeps its deliberate widening past the spec's machine
-identifier (numeric/boolean values for standalone forms, objectui#3090), now named in an
-`Omit` instead of restated.
-
-**The convergence is an EXTENSION, not a replacement.** objectui legitimately carries
-keys the spec does not: `description` (`LookupField` searches it, objectui#6153) on the
-metadata face, and `disabled` / `icon` on both. The spec's `SelectOptionSchema` is strict
-over exactly `{label, value, color, default, visibleWhen}` and refuses each of those
-three **by name**, so they are declared as objectui dialect with that refusal written
-into the published JSDoc rather than described as spec-aligned. These are read-model keys
-and must never reach an authored object document — a field's `options` are routed through
-the strict schema, so one of them fails the whole field.
-
-`SelectOptionBase` is exported from `@object-ui/types` because it appears in the
-`extends` clause of both published interfaces.
diff --git a/.changeset/7014-q2-option-label-empty-string.md b/.changeset/7014-q2-option-label-empty-string.md
deleted file mode 100644
index 7141fea4ec..0000000000
--- a/.changeset/7014-q2-option-label-empty-string.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Field designer: clearing a picklist option's **Label** no longer produces metadata the
-API refuses (objectui#7014 Q2).
-
-`ObjectFieldInspector`'s option writer guarded the key on truthiness —
-`if (o.label) out.label = o.label;` — so an empty Label box was serialised as an option
-with **no `label` key at all**. Measured on `@objectstack/spec` 17.2.0,
-`SelectOptionSchema` accepts `{ value: 'alpha', label: '' }` and rejects
-`{ value: 'alpha' }` with `invalid_type` at `[label]`: the guard was taking a document
-the platform accepts and rewriting it into one it refuses, and the save came back 422
-with nothing on screen to explain it.
-
-The writer now emits `label: o.label ?? ''` — the value the author actually holds,
-empty string included. Nothing is invented: an emptied label stays empty rather than
-falling back to the option's `value`, and the author-facing surface is now exactly as
-wide as the contract instead of narrower. The `??` arm also covers an option that
-arrived without a usable label (a missing or non-string stored `label`, which
-`readOptions` maps to `undefined`) — there is no legal document that omits the key, and
-`''` is what the Label input has been displaying for that option all along.
-
-Pinned in `ObjectFieldInspector.optionLabel.test.tsx`, which ends each case at
-`SelectOptionSchema` / `FieldSchema` rather than merely asserting the key is present —
-the point of the fix is that the contract accepts what the designer emits.
diff --git a/.changeset/7014-select-option-spec-claim-correction.md b/.changeset/7014-select-option-spec-claim-correction.md
deleted file mode 100644
index c086db0875..0000000000
--- a/.changeset/7014-select-option-spec-claim-correction.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Correct three false `@objectstack/spec` alignment claims on field metadata, and pin the
-real boundary (objectui#7014).
-
-**No contract change.** No type, schema, export or runtime path moves. What changes is
-published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
-asserting the opposite of what the spec does.
-
-Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
-fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
-control that accepts the same payload minus the key:
-
-- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
- `SelectOptionSchema.description`". That schema is `.strict()` over exactly
- `{label, value, color, default, visibleWhen}`; `description` fails with
- `unrecognized_keys`.
-- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
- `FieldSchema.rows` declares the key "authorable on exactly the multiline editor
- types". `FieldSchema` refuses `rows` by name on all four of
- textarea/markdown/html/richtext.
-
-The keys themselves stay declared and stay consumed — `LookupField` searches an option's
-`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
-attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
-field's `options` through the strict option schema, so authoring `description` on an
-option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
-`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
-objectui-side read-model extensions that must never reach authored object metadata.
-
-A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
-and each by-name refusal, so if the spec ever adopts one of these names the claim is
-re-opened loudly instead of silently becoming true.
diff --git a/.changeset/7015-text-expression-content-channel.md b/.changeset/7015-text-expression-content-channel.md
deleted file mode 100644
index 4048b82e2c..0000000000
--- a/.changeset/7015-text-expression-content-channel.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
----
-
-Docs only: the expression guides taught `"value": "${...}"` on `type: "text"`
-nodes, which the renderer reads back but never evaluates, so the reader saw the
-literal `${...}` on screen. All 29 authored occurrences now spell the carriage
-`content`, the ruled sole evaluation channel for `text` (objectui#7015,
-maintainer ruling 2026-08-31 on objectstack#13670, option 2).
-
-No package source changed, so this ships nothing — `check-changeset-presence`
-independently reports "no changeset is owed" for this diff. The declaration is
-here to state the release intent explicitly rather than to bump anything.
diff --git a/.changeset/7025-nonoptional-enum-cast.md b/.changeset/7025-nonoptional-enum-cast.md
deleted file mode 100644
index cdf00e8bcf..0000000000
--- a/.changeset/7025-nonoptional-enum-cast.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
----
-
-Internal test-only change, no user-visible behaviour (objectui#7025).
-
-The SIXTH spelling of the Zod-internals reader hazard objectui#5872 catalogues:
-9 spec-parity test files in 7 packages cast an enum node NON-optionally to an
-options-bearing shape and read `.options` straight off it. They converge onto
-`@object-ui/test-support`'s `enumOptions(node)` — the same walk objectui#6924
-converged the optional-cast family onto in PR #7024.
-
-Unlike that family, these sites failed LOUDLY (spreading `undefined` throws), and
-the shared reader deliberately answers `[]` rather than raising. So every site
-keeps its own non-vacuity duty: a throwing wrapper at the seven module-scope
-reads, and the suite's own pre-existing non-vacuity assertion at the two in-test
-reads. A bare conversion would have traded a loud failure for a silent empty
-vocabulary — a regression, not a convergence.
-
-Only test files, two `devDependencies` edges and the lockfile change; no
-package's shipped `dist/` and no public type moves.
diff --git a/.changeset/7027-gantt-date-brand-gate.md b/.changeset/7027-gantt-date-brand-gate.md
deleted file mode 100644
index 49f4be4ac1..0000000000
--- a/.changeset/7027-gantt-date-brand-gate.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/plugin-timeline': patch
----
-
-Gantt date type gate: judge the `[[DateValue]]` slot, not the prototype chain
-(objectui#7027).
-
-`isGanttDateType` asked `value instanceof Date`, which answers "does this
-inherit from `Date.prototype`?" and not "is this a `Date`?". An object that
-inherits the prototype without owning the internal slot passed the gate,
-reached `new Date(value)`, ran ToPrimitive, and threw
-`TypeError: Method Date.prototype.toString called on incompatible receiver` —
-uncaught, mid-render, so the author got a blank screen where #6781's named
-diagnostic belongs. Three spellings crashed on `main`, measured:
-`Object.create(Date.prototype)`, that impostor behind a `Proxy` that throws on
-every get, and a `Proxy` with a throwing `getPrototypeOf` trap (`instanceof` is
-not total on its own terms either).
-
-Both sites now ask a total brand test that invokes the builtin
-`Date.prototype.getTime` with `.call`: it reads the receiver's `[[DateValue]]`
-slot and nothing else, so no author getter runs, no `Symbol.toStringTag` is
-consulted, and no proxy trap fires. The two brand tests the finding suggested
-were measured and rejected — `Object.prototype.toString.call` performs
-`Get(O, @@toStringTag)` unconditionally, and `Number.isFinite(value.getTime())`
-calls the author's `getTime`, which would refuse a real `Date` subclass by
-dying on it. Both are pinned as red rows.
-
-No change to which values are accepted: #6781's accept set
-(`string | finite number | Date`) is untouched, `new Date(NaN)` still passes
-the type gate and is still refused by the parse check with its `Invalid Date`
-spelling, and every newly-refused value is one no authored document can carry
-(ObjectUI metadata is JSON, which cannot spell a prototype).
diff --git a/.changeset/7029-no-invented-calendar-fields.md b/.changeset/7029-no-invented-calendar-fields.md
deleted file mode 100644
index 37fef232ea..0000000000
--- a/.changeset/7029-no-invented-calendar-fields.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-list': minor
-'@object-ui/plugin-view': minor
----
-
-Calendar views no longer render on invented field names (objectui#7029; ruled on
-objectstack#13748, director batch #19, option A).
-
-A view that carried no `calendar:` block used to have a complete-looking calendar
-configuration synthesized for it. `ObjectCalendar` has always decided whether it
-has a usable configuration by asking whether a start-date binding is PRESENT, so
-the fabrication short-circuited its own refusal screen — "Calendar configuration
-required. Please specify startDateField and titleField." — which existed all
-along and was simply unreachable. Measured on a leave-request object whose real
-fields are `start_date` / `end_date`: every record piled onto today's cell under
-titles resolved through the display-name chain. A plausible, fully wrong screen,
-with zero signal to the author.
-
-Three faces were fabricating, on two independent routes to the same renderer:
-
-- `app-shell/ObjectView` emitted `startDateField: 'due_date'` and
- `titleField: 'name'` into `options.calendar` for every object view;
-- `plugin-list/ListView`'s calendar branch floored the same two bindings at
- `'start_date'` / `'end_date'` one layer down;
-- `plugin-view/ObjectView.generateViewSchema` — the authored `object-view`
- element route, which bypasses `ListView` entirely — carried its own copy.
-
-All three now forward only what the author declared. This converges the calendar
-on the shape its siblings already had: `timelineViewOptions` (objectui#3129
-retired this very literal from the timeline axis), the kanban lane detector
-(ADR-0085, "never invents a field the object doesn't have"), and
-`defaultCalendarFromObject` (a binding, or nothing).
-
-**Behaviour change, loud over silent.** With no binding to forward, ADR-0047's
-capability gate stops offering the Calendar toggle to views that configured
-none, and a view forced onto the calendar renderer reaches the refusal screen
-instead of a wrong one. A view that happened to sit on an object carrying a real
-`due_date` field was rendering by luck; it now refuses until its `calendar:`
-block is written. Correctly configured calendars are unaffected — same fields,
-same render. The same deletion also stops the fabricated name from answering for
-the Timeline switcher, which accepts a calendar binding as a legitimate axis.
-
-The spec half — cross-field validation rejecting a half-written declaration at
-authoring time — is objectstack#13817. This half makes the runtime honest
-independent of which spec version the host pins.
diff --git a/.changeset/7063-widget-empty-state-default.md b/.changeset/7063-widget-empty-state-default.md
deleted file mode 100644
index 3de7b6ba56..0000000000
--- a/.changeset/7063-widget-empty-state-default.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/plugin-dashboard': minor
-'@object-ui/i18n': minor
----
-
-Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated
-once for the surface (objectui#7063).
-
-Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that
-renders a bare row-placeholder on an empty result is the PLATFORM's defect and
-must be fixed uniformly — apps must not compensate widget by widget
-(objectstack#13848). The measured scenario is a fresh flagship-demo install:
-eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads
-as "the dashboard failed to load" even though the widget, its declaration and
-its (not yet produced) data are all legitimate.
-
-- New `WidgetEmptyState` is the seam the three dashboard surfaces now share.
- There was no shared placeholder to fix: `DatasetWidget` wrote
- `dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote
- `dashboard.noDataAvailable` — three renders, two strings, no common code.
-- The default now reads as a STATE, not a failure: `role="status"` (the empty
- branches previously carried no role at all, while the failure branches beside
- them are `role="alert"`), muted treatment with an inbox glyph rather than a
- warning triangle, and a title plus an explanation where the placeholder was a
- single terse fragment.
-- It names WHAT is empty with zero authored copy — the widget's data source,
- which is the half the reader cannot already see (the tile's title is rendered
- by the card header directly above). That is `widget.dataset` on the dataset
- path and `schema.objectName` on the object-bound table/pivot; `PivotTable`
- takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable`
- forwards.
-- Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel`
- added to `en` and all nine sibling packs. No inline `defaultValue` and no
- interpolation — the source renders as a labelled value, so no separator is
- concatenated in code and every pack spells its own punctuation.
-
-No new authoring obligation and no new spec key. Note that the `emptyState`
-override the card assumes for this surface does not exist: `emptyState` is a
-LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no
-such key — so there is nothing here for an author to override, and adding one
-would be a contract question rather than a rider.
diff --git a/.changeset/7064-empty-section-default.md b/.changeset/7064-empty-section-default.md
deleted file mode 100644
index 146b859d39..0000000000
--- a/.changeset/7064-empty-section-default.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/plugin-detail': minor
----
-
-**Behaviour change.** `record:details` no longer forces `hideEmpty` on the
-sections it synthesizes, so a sparse record keeps its section skeleton instead
-of collapsing. Applications relying on the old auto-hide of *unauthored*
-sections will now see headings, field labels and empty-value placeholders where
-rows used to vanish. This is the loud-over-silent direction, ruled by the
-maintainer on 2026-08-31: an empty detail body is a platform concern, and a
-metadata application should not have to author its way out of one.
-
-`RecordDetailsRenderer` mapped every authored section with
-`hideEmpty: s.hideEmpty ?? true`. `DetailSection` already states the correct
-rule in its own heuristic — *"If a section is entirely empty (e.g., loading
-state, brand-new record), do NOT auto-hide — the labels themselves are useful
-as a structural skeleton"* — and the forced default overrode exactly the case
-that sentence reserves. On a hand-created record whole sections disappeared and
-the body collapsed to a couple of rows; seeded demo data hid it. Every
-application then had to hand-write `hideEmpty: false` per section to stop
-looking broken, which is per-app tax for a platform defect. The renderer now
-passes the authored value through untouched and lets the heuristic own the
-default.
-
-What changes, precisely:
-
-- an **all-empty** section renders its heading, every field label and one
- empty-value placeholder per field (it used to render nothing at all);
-- a **small** partly-empty section — below `DetailSection`'s auto-hide
- threshold of 4 fields / 25% empty (3 / 20% on mobile) — now shows its empty
- rows;
-- a **large** mostly-empty section with at least one filled row still
- auto-hides, with the "Show N empty fields" toggle unchanged: the
- label-graveyard guard is intact and this is not a return to dense-by-default;
-- empty rows are now visible while inline-editing a section, so an unwritten
- field can be filled in place.
-
-What does **not** change: an authored `hideEmpty` keeps its exact former
-meaning. `hideEmpty: true` remains the explicit opt-in to hiding, and
-`hideEmpty: false` remains what it always was — "not `true`", not an override
-of the auto-hide heuristic (measured, and pinned as pre-existing).
-
-Reference-app hit inside this repo: the Studio metadata-admin page preview
-(`PagePreview`) binds a real sample record, so a `record:details` block over a
-sparse sample now previews the skeleton rather than a collapsed body. No
-application metadata needs editing — that is the point of the change.
diff --git a/.changeset/7069-parity-wider-ledger.md b/.changeset/7069-parity-wider-ledger.md
deleted file mode 100644
index 06a72f97e8..0000000000
--- a/.changeset/7069-parity-wider-ledger.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only change: adds a fifth ledger to the zod-mirror parity suite watching the
-mirror-wider-than-declared direction. No published type, mirror or runtime behaviour
-moves; the file is excluded from the package build by `tsconfig.json`.
diff --git a/.changeset/7070-no-invented-gantt-date-fields.md b/.changeset/7070-no-invented-gantt-date-fields.md
deleted file mode 100644
index aa768c6020..0000000000
--- a/.changeset/7070-no-invented-gantt-date-fields.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-list': minor
-'@object-ui/plugin-view': minor
----
-
-Gantt views no longer render on invented date field names (objectui#7070).
-
-The half PR #7062 fenced out and reported separately. A view that carried no
-`gantt:` block used to have a complete-looking date axis synthesized for it:
-all three faces floored `startDateField` at `'start_date'` and `endDateField`
-at `'end_date'` — field names no view had written and most objects do not
-carry.
-
-`ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props
-are present, so the fabricated pair short-circuited the renderer's own refusal
-screen — "Gantt configuration required. Please specify startDateField,
-endDateField, and titleField." — which existed all along and was simply
-unreachable from every route. The same fabrication answered ADR-0047's
-capability gate in `ListView.availableViews`, so the Gantt toggle was live on
-every object view in the product.
-
-⚠️ The premise was MEASURED before anything was deleted, because #7029's
-mechanic is only correct where a refusal path exists and that had never been
-established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an
-absent binding — it does not render empty, and it does not throw.
-
-Three faces were fabricating, on two independent routes to the same renderer:
-
-- `app-shell/src/views/ObjectView.tsx` — the console object page. The inline
- branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and
- `timelineViewOptions`: the declared block spread whole, title floored at
- `'name'`, no date field invented.
-- `plugin-list/src/ListView.tsx` — the render branch AND the capability gate.
-- `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored
- `object-view` element route, which bypasses `ListView` entirely.
-
-**What changes for an author.** A view that declared no gantt configuration is
-no longer offered the Gantt toggle, and one forced onto the renderer reaches
-the refusal screen instead of a plausible, fully wrong chart. A view that
-declared a binding is unaffected — the declared block is forwarded exactly as
-before, every spec key included.
-
-Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx`
-certified the gantt branch below it as already using the safe two-rung shape.
-It did not. The note now states each sibling branch as measured, and says
-explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the
-two plugin faces, which objectui#7070 routes to a ruling rather than settling
-per-face.
-
-Deliberately out of scope, and left in place: `progressField` / `dependenciesField`
-(not date axes, different absent-value semantics) and the timeline `'created_at'`
-posture conflict.
diff --git a/.changeset/7070-timeline-date-axis-floors-retired.md b/.changeset/7070-timeline-date-axis-floors-retired.md
deleted file mode 100644
index c38bd4a691..0000000000
--- a/.changeset/7070-timeline-date-axis-floors-retired.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/plugin-list': minor
-'@object-ui/plugin-view': minor
----
-
-Retire the `'created_at'` timeline date-axis floors at both plugin faces
-(objectui#7070 step ③, maintainer ruling 2026-09-01, 总监批 #28).
-
-**Breaking, deliberately.** A timeline view that declares **no** date axis anywhere no
-longer renders. `ListView`'s and `ObjectView`'s timeline branches used to hand
-`ObjectTimeline` a `startDateField` of `'created_at'` for such a view; both now forward
-a declared axis or no key at all, and the renderer shows its "declare a date axis"
-refusal instead.
-
-House posture, entered with the ruling: **日期轴永不虚构** — a date axis is never
-fabricated. This is the third and last step of a sequence the ruling ordered and forbade
-reordering: `ObjectTimeline` gained the refusal screen and lost its own internal
-`|| 'date'` floor first (objectui#7459), which by its own measurement changed nothing a
-user could see — precisely because these two faces still supplied a name. They are the
-supply.
-
-The floor was not a harmless default. `'created_at'` is a column nearly every object
-carries, so downstream it was indistinguishable from a real binding and could never
-resolve to nothing — while the `$select` projection is collected from the **declared**
-`timeline` / `options.timeline` blocks and never from this prop. An undeclared view was
-therefore given a timeline bound to a column the query had not requested, and every
-record bucketed into "No date": a screen that looks built, is wrong, and gives the
-author no signal. The ruling also explicitly replaced the written decision that stood on
-the deleted `ListView` line ("`created_at` stays the last resort for a view that
-declares no date axis anywhere") — it was a second, de-facto contract held at one face,
-on the very literal objectui#3129 had retired at the app-shell face.
-
-**Migration.** Declare the axis on the view: `timeline.startDateField` (spec-canonical),
-`timeline.dateField` (legacy alias), or a `calendar.startDateField` — objectui#3129
-established that a calendar binding is a legitimate timeline axis, and it still is. All
-three keep rendering exactly as before; only the *undeclared* case changes. A view that
-really did want records laid out by creation time says so in one key:
-`timeline: { startDateField: 'created_at' }`. The refusal names the accepted keys on
-screen, so an affected view reports its own fix.
-
-`titleField` is unaffected and keeps its `'name'` floor at both faces — it is not a date
-axis. So do gantt's `progressField` / `dependenciesField`, which the ruling scoped out
-for separate evaluation.
diff --git a/.changeset/7072-catalog-menu-value-keys.md b/.changeset/7072-catalog-menu-value-keys.md
deleted file mode 100644
index 342ecc54ce..0000000000
--- a/.changeset/7072-catalog-menu-value-keys.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
----
-
-Internal only, no release: the four overlay-menu fixtures in
-`examples/schema-catalog` no longer author a `value` key on their menu items.
-
-Twenty-one menu items across `components-overlay-menubar/application-menubar`
-(11), `components-overlay-context-menu/basic-context-menu` (4),
-`components-overlay-dropdown-menu/basic-dropdown-menu` (3) and
-`components-overlay-dropdown-menu/with-icons` (3) carried `"value"`. No arm of
-the shipped `MenuItem` union declares it — `MenuCommandItem`
-(`packages/types/src/overlay.ts:363-401`) declares `label`, `icon`, `disabled`,
-`onClick`, `shortcut`, `children`, `separator?: false` and a `type?: never`
-tombstone; `MenuDividerItem` (`:409-419`) declares `separator: true` and the
-same tombstone. Neither declares `value`, and none of the three menu renderers
-under `packages/components/src/renderers/overlay/` reads one.
-
-Nothing rendered differently before or after, which is the point: `MenuItemSchema`
-(`packages/types/src/zod/overlay.zod.ts:147-168`) builds its union from bare,
-non-strict `z.object`s, so zod stripped the key and reported success. The key
-had been inert and invisible since it was authored, and the catalog is a
-declared AI few-shot retrieval source, so an inert key there is a spelling the
-next author copies.
-
-The keys are deleted rather than declared: nothing reads `value`, and
-objectui#6523 deliberately narrowed this union rather than widening it. No
-published package source, no schema, no renderer and no type was touched — the
-diff is four JSON fixtures in a `private: true` example package.
diff --git a/.changeset/7073-overlay-menu-docs-declared-surface.md b/.changeset/7073-overlay-menu-docs-declared-surface.md
deleted file mode 100644
index cae4edd1b4..0000000000
--- a/.changeset/7073-overlay-menu-docs-declared-surface.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
----
-
-Docs only, publishes nothing: the `## Schema` blocks of
-`content/docs/components/overlay/dropdown-menu.mdx` and
-`content/docs/components/overlay/context-menu.mdx` are corrected to the surface
-`packages/types/src/overlay.ts` actually declares. Same defect class objectui#6521
-fixed on the third menu page, and more of it — all three pages publish a locally
-named mirror of the shared `MenuItem` union, and these two were still wrong.
-
-`dropdown-menu.mdx`, four rows:
-
-| page before | authority on `origin/main` | page after |
-| --- | --- | --- |
-| `value?: string` | declared on **neither** arm — not `MenuCommandItem` (`overlay.ts:363-401`), not `MenuDividerItem` (`overlay.ts:409-419`), absent from both zod arms (`zod/overlay.zod.ts:149-166`); no menu renderer reads a `.value` | row deleted |
-| `variant?: 'default' \| 'destructive'` | declared nowhere on the menu types. The only `variant` in `renderers/overlay/dropdown-menu.tsx` is `:130`, inside `defaultProps.trigger` — `{ type: 'button', label: 'Menu', variant: 'outline' }`, a button node, a different object than a menu item | row deleted |
-| *(absent)* | `shortcut?: string` (`overlay.ts:383`, zod `zod/overlay.zod.ts:154`), **read** at `dropdown-menu.tsx:80` | `shortcut?: string` added |
-| *(absent)* | `children?: MenuItem[]` (`overlay.ts:387`, zod `zod/overlay.zod.ts:155`), **read** at `dropdown-menu.tsx:58,66` — branches into `DropdownMenuSub` / `DropdownMenuSubTrigger` / `DropdownMenuSubContent`, a real submenu | `children?: DropdownMenuItem[]` added |
-
-`context-menu.mdx`, four rows:
-
-| page before | authority on `origin/main` | page after |
-| --- | --- | --- |
-| `value?: string` | same as above — undeclared on both arms; no `.value` read in `renderers/overlay/context-menu.tsx` | row deleted |
-| *(absent)* | `shortcut?: string` (`overlay.ts:383`, zod `zod/overlay.zod.ts:154`), **read** at `context-menu.tsx:78`, rendered through `ContextMenuShortcut` | `shortcut?: string` added |
-| *(absent)* | `children?: MenuItem[]` (`overlay.ts:387`, zod `zod/overlay.zod.ts:155`), **read** at `context-menu.tsx:56,64` — branches into `ContextMenuSub` / `ContextMenuSubTrigger` / `ContextMenuSubContent` | `children?: ContextMenuItem[]` added |
-| `trigger: ComponentSchema` (required) | `trigger?: SchemaNode \| SchemaNode[]` — optional at `overlay.ts:486`, `.optional()` at `zod/overlay.zod.ts:191`. The renderer substitutes a placeholder (`context-menu.tsx:95`), so a trigger-less document is legal today, and the declaration's own doc comment records the divergence in words: "Declared OPTIONAL although the docs page shows it required" (objectui#6150) | `trigger?: ComponentSchema` |
-
-`children` and `shortcut` are the rows that matter: declared capabilities with
-working runtime that both pages hid outright, so an author reading either page
-had no way to learn these menus draw nested submenus at all, or that an item can
-carry a keyboard shortcut. That is the reverse of the usual docs defect and
-strictly worse for an AI author, which will not emit a key the reference page
-does not list. `value` and `variant` are the forward direction — keys the pages
-taught and nothing on the platform has ever read.
-
-Interface names stay localized (`DropdownMenuItem`, `ContextMenuItem`) and the
-`type` tombstone stays taught as `## Dividers` prose rather than a block row —
-both are house convention, measured across all three menu pages by objectui#6521's
-PR and unchanged here.
-
-Not in this diff, and deliberately not: declaring `value` or `variant` on
-`MenuCommandItem`. objectui#6523 narrowed that union on purpose; widening a
-published type to match a doc page would invert the fix.
-
-No fence moved: each page holds one `plaintext` fence marker pair before and
-after, so objectui#5867's SHRINK-ONLY declared population is unchanged.
-
-Part of objectui#7073.
diff --git a/.changeset/7077-button-group-retire-authored-surface.md b/.changeset/7077-button-group-retire-authored-surface.md
deleted file mode 100644
index a645e7941a..0000000000
--- a/.changeset/7077-button-group-retire-authored-surface.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`button-group` honors per-button `disabled`, and the catalog stops authoring 29 keys
-nothing reads (objectui#7077, maintainer ruling 2026-09-04, decision batch #25).
-
-**The renderer change.** `ButtonGroupButton` declares `disabled?: boolean` and the
-renderer read it nowhere — it mapped `schema.buttons` to `Button` elements passing
-`variant`, `size`, `className` and `label` and nothing else, so a button declaring
-`disabled: true` rendered live and clickable. It now forwards the value, matching every
-sibling that declares item-level `disabled`: `tabs`, `select`, `dropdown-menu`,
-`menubar`, `context-menu`, and `toggle-group` since objectui#4632. This is mechanical
-consistency with an already-declared contract — no new key, no new type member, and no
-change to what any schema accepts or refuses.
-
-**Migration.** None required. A `disabled: true` you already author starts taking
-effect; that is the declared meaning of the key, and nothing in the shipped corpus
-authored it.
-
-**The corpus change.** `button-group` stays a **presentational** group: no selection
-state, no `selectionMode`, no group-level `value`, no per-button `value`, no per-button
-`icon`. All six catalog fixtures authored those four undeclared keys — 29 occurrences —
-and every one parsed green, because `BaseSchema` is `.passthrough()` and carries
-`[key: string]: any`: admitted unexamined, never refused. The keys are gone from the
-fixtures. Implementing selection instead was weighed and rejected: it is a capability
-addition with zero measured pull, and the only things authoring it were fixtures we
-wrote ourselves — the corpus reverse-defining the product. It remains available as a
-fallback if a real consumer (an app or example needing a segmented control) is produced
-first.
-
-This matters more than a demo tidy-up because the catalog is the corpus AI authoring
-tools retrieve from. An author copying the old `single-selection.json` got a schema that
-validated, published, and did nothing — a failure with no signal to self-correct from.
-
-`with-icons.json` is **removed** rather than emptied: with `icon` retired, a fixture by
-that name authored no icons, and what remained duplicated `basic-button-group.json`. Its
-demo slot on the docs page goes with it. The other five keep their labels and still
-render — `icon-toolbar.json` rendered three blank buttons until objectui#6318 gave it
-labels, and a pin now holds every fixture in the category to a non-empty `label` on
-every button.
-
-`onClick` is **not** wired, and is not a gap: objectui#6124 (PR #7339) retired it two
-days before this ruling. It is `onClick?: never` on the TypeScript face and a refusal by
-name on the Zod mirror, so there is no declared-but-dead handler left to forward.
diff --git a/.changeset/7079-schema-reference-base-props.md b/.changeset/7079-schema-reference-base-props.md
deleted file mode 100644
index dfcfe3b8e3..0000000000
--- a/.changeset/7079-schema-reference-base-props.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
----
-
-Docs only — the canonical `BaseSchema` "Common Properties" table in
-`content/docs/api/schema-reference.md` (objectui#7079). No package source, no
-published contract and no runtime behaviour is touched, hence the empty
-declaration; `apps/site` is `private: true` and sits in `.changeset/config.json`'s
-`ignore` list, so nothing under `content/` ships from this change.
-
-The table narrowed five declared unions to one limb each and omitted five declared
-members outright. Measured against `packages/types/src/base.ts` and its Zod mirror
-`packages/types/src/zod/base.zod.ts`, whose agreement is held by
-`base-schema-zod-mirror-parity.test.ts` reading the mirror's own `.shape`:
-`label` and `description` are `string | I18nLabel`, `ariaLabel` is
-`string | KeyedI18nLabel` (the KEYED form, not the inline locale map), and both
-`visible` and `disabled` take a predicate expression string as well as a boolean —
-the expression limb is on the base key itself, not only on the `visibleOn` /
-`disabledOn` siblings the paired cells implied. `placeholder`, `style`, `data`,
-`bind` and `visibleWhen` had no row at all.
-
-The three combined cells (`visible` / `visibleOn`, `hidden` / `hiddenOn`,
-`disabled` / `disabledOn`) are split into one row per member. Packing two members
-with different types into a single `boolean` / `string` cell is what made the
-error invisible: the pairing reads as a complete, ordered account, and that
-appearance of completeness is precisely what hid the third fact. One row per
-declared member, in declaration order, makes completeness checkable by reading the
-table against the interface.
-
-Nothing in CI reads this table — `check:doc-snippet-types` compiles only `ts` /
-`tsx` / `typescript` fences, `check:doc-component-types` judges `type` string
-literals and key tables anchored on a `Namespaced key | Bare-name fallback`
-header, and no script in the repository parses a Markdown property table.
diff --git a/.changeset/7081-overlay-trigger-union.md b/.changeset/7081-overlay-trigger-union.md
deleted file mode 100644
index cf244ff222..0000000000
--- a/.changeset/7081-overlay-trigger-union.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**The overlay family's `trigger` slot now declares the node array its Zod mirror, its runtime and its own shipped defaults already accept** (objectui#7081).
-
-`trigger` on `DialogSchema`, `AlertDialogSchema`, `SheetSchema`, `DrawerSchema`, `PopoverSchema`, `HoverCardSchema` and `DropdownMenuSchema` widens from `SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face — the spelling `ContextMenuSchema` and `TooltipSchema` already carried. Each member keeps its optionality (the first four optional, the last three required). `SchemaNode` itself is unchanged.
-
-This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/overlay.zod.ts` already spelled every one of these keys `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: every overlay renderer hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and every registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying a renderer's own default into a typed document is no longer a type error against the type that shipped it. The seven docs pages' `trigger` rows follow the declaration.
-
-Triage on the card (2026-09-03): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略").
diff --git a/.changeset/7082-overlay-node-slot-doc-type-names.md b/.changeset/7082-overlay-node-slot-doc-type-names.md
deleted file mode 100644
index 085c52e929..0000000000
--- a/.changeset/7082-overlay-node-slot-doc-type-names.md
+++ /dev/null
@@ -1,73 +0,0 @@
----
----
-
-Docs only, publishes nothing: six component pages spelled a node slot
-`ComponentSchema`, a real shipped export (`packages/types/src/blocks.ts`) that
-is **not** a node slot — it is the concrete `type: 'component'` block. The keys
-carry `SchemaNode` (`packages/types/src/base.ts`), which also admits
-`string | number | boolean | null | undefined`. A reader who looked the
-published name up found a narrow, unrelated type.
-
-Every row is re-derived from its own declaration on `2c3cd1b`, per page — not
-by a string replace, because the declarations disagree with each other on
-whether the array form is admitted.
-
-| page row before | declaration on `main` | page after |
-| --- | --- | --- |
-| `AlertDialogSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:91`) | `SchemaNode` |
-| `ContextMenuSchema.trigger?: ComponentSchema` | `trigger?: SchemaNode \| SchemaNode[]` (`overlay.ts:486`) | `SchemaNode \| SchemaNode[]` |
-| `HoverCardSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:296`) | `SchemaNode` |
-| `HoverCardSchema.content: ComponentSchema` | `content: SchemaNode \| SchemaNode[]` (`overlay.ts:292`) | `SchemaNode \| SchemaNode[]` |
-| `DropdownMenuSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:433`) | `SchemaNode` |
-| `SheetSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:150`) | `SchemaNode` |
-| `SheetSchema.content: ComponentSchema` | `content?: SchemaNode \| SchemaNode[]` (`overlay.ts:146`) | `SchemaNode \| SchemaNode[]` |
-
-**`dropdown-menu` stays singular on purpose.** Its Zod mirror
-(`zod/overlay.zod.ts:176`), its sibling `ContextMenuSchema`, and its own shipped
-`defaultProps` (`renderers/overlay/dropdown-menu.tsx:130`) all use the array
-form, and the published TS type refuses it. Which side is right is objectui#7081,
-open and awaiting a maintainer decision on a published-type widening. Publishing
-the array form here would have put a claim in the docs that the shipped type
-rejects and pre-empted that ruling; the page therefore follows the type an
-author's editor reads. The incoherence is real, and it *is* #7081.
-
-**Two of the nine rows the card named are NOT renamed**, because no honest
-docs-only edit resolves them, and both are filed instead:
-
-- `AlertDialogSchema.actions?: ComponentSchema[]` — declared on neither the TS
- interface (`overlay.ts:78-127`) nor the mirror (`zod/overlay.zod.ts:42-55`),
- and read by nothing. Renaming it would have kept a phantom key alive under a
- second wrong type.
-- `EmptySchema.action?: ComponentSchema` — declared on neither
- (`feedback.ts:210-224`, `zod/feedback.zod.ts:120-125`), but *read* by the
- shipped renderer through a cast (`renderers/feedback/empty.tsx`), which
- requires `typeof === 'object'`. `SchemaNode` admits `string | number |
- boolean`, so publishing it there would have been a new false claim, not a
- correction.
-
-Requiredness is likewise left alone and filed: `AlertDialogSchema.trigger`,
-`SheetSchema.trigger` and `SheetSchema.content` are declared optional and
-published required. That is objectui#7073's defect class, already fixed by it on
-`context-menu.mdx`, and is out of this card's fence.
-
-`packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts` pins the
-result, and this is a page where the pin is the only evidence there is: all
-these rows sit in `plaintext` fences, `check:doc-snippets` compiles
-`ts`/`tsx`/`typescript` only and `check:doc-types` reads only the `type` string
-literals, so no gate parses them (objectui#5250, objectui#5867). A green CI run
-here says "nothing else broke", not "the correction is right".
-
-The pin's authority is the **TS declaration**, not the Zod mirror — the one
-deliberate departure from the objectui#6347 model, because on this tree the two
-disagree on four `trigger` rows and pinning against the mirror would have
-published #7081's answer. Its type-level leg is compiled by
-`packages/types/tsconfig.test.json`, so widening `DropdownMenuSchema.trigger`
-turns it red and whoever lands #7081 is told this page owes an update. The two
-undeclared rows and the three requiredness divergences are pinned as
-divergences, so a fix on either side of any of them also turns it red.
-
-No fence moved: each page holds one `plaintext` fence marker pair before and
-after, so objectui#5867's shrink-only declared population is unchanged. No
-declaration, mirror, renderer or fixture was edited.
-
-Part of objectui#7082.
diff --git a/.changeset/7086-blockquoted-fence-collector.md b/.changeset/7086-blockquoted-fence-collector.md
deleted file mode 100644
index 2676771491..0000000000
--- a/.changeset/7086-blockquoted-fence-collector.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
----
-
-Doc-snippet gate tooling only, no published package source changed.
-
-`check-doc-snippet-types` collects blocks with `scanFences`, whose fence-opening
-anchor accepted a run of leading spaces and tabs and nothing else. A fence
-opened inside a Markdown blockquote carries a `> ` prefix, so the anchor never
-matched, the block was never collected, and the gate compiled nothing for it.
-There was no diagnostic: an uncollected block appears in no count, and its page
-still reports as covered. A callout is a natural home for an import example,
-which is exactly the snippet class that rots when an export is renamed — the one
-class this gate exists to catch.
-
-The opener now tolerates a blockquote prefix and carries the opener's quote
-DEPTH through the rest of the walk: the search for the closing fence reads
-candidate lines at that same depth, and every body line is stripped of that many
-markers before it reaches the compiler. Depth 0 — every unquoted fence in the
-corpus — takes an identity path that returns the line unchanged byte for byte,
-so the other 773 collected blocks scan exactly as they did before. Stripping
-consumes at most one space after each marker, per CommonMark, so indentation
-belonging to the snippet survives.
-
-Carrying the depth to the CLOSING fence is what makes this safe in both
-directions. Without it a blockquoted fence would find no close and swallow the
-rest of the file; and a plain fence would be closed early by any `> ` + backticks
-line quoted inside it as prose. Both directions are pinned.
-
-Measured over the gate's own 224-document population: collected blocks 773 → 774.
-The one newly-collected block is the import callout at
-`content/docs/api/schema-reference.md` line 12, and it compiles — the gate's
-semantic phase judges 272 of 272 with 0 failures. Nothing left the population.
diff --git a/.changeset/7087-disabled-twin-symmetry.md b/.changeset/7087-disabled-twin-symmetry.md
deleted file mode 100644
index a4e1a68a54..0000000000
--- a/.changeset/7087-disabled-twin-symmetry.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`disabled` accepts a predicate string — `boolean | string`, the `BaseSchema` union — on
-the 18 concrete schemas that used to narrow it back to `boolean` (objectui#7087,
-maintainer ruling 2026-09-01: option 1, scoped to `disabled`).
-
-`visible` and `disabled` are twins: objectui#4581 widened both on `BaseSchema` on the same
-evidence — `SchemaRenderer` evaluates both through `evaluator.evaluateCondition` rather
-than reading either as a boolean. After that widening, 0 of the 124 `extends BaseSchema`
-interfaces redeclared `visible`, while 18 still carried a pre-widening
-`disabled?: boolean` of their own, with matching `z.boolean()` mirrors. So
-`disabled: "${data.status === 'locked'}"` — the capability the renderer implements and
-the base type advertises — was a type error and a zod refusal on `ButtonSchema`,
-`InputSchema`, `TextareaSchema`, `SelectSchema`, `CheckboxSchema`, `RadioGroupSchema`,
-`SwitchSchema`, `ToggleSchema`, `SliderSchema`, `FileUploadSchema`, `DatePickerSchema`,
-`CalendarSchema`, `InputOTPSchema`, `FormSchema`, `ComboboxSchema`, `ActionSchema`,
-`CollapsibleSchema` and `ToggleGroupSchema`.
-
-Those 18 redeclarations are removed, on both faces. The interfaces inherit
-`BaseSchema.disabled` the way they always inherited `visible`; the zod mirrors inherit
-`base.zod.ts`'s `z.union([z.boolean(), z.string()])` through `.extend()`'s merged
-`.shape`, so there is no second spelling of the union to drift from — the route
-`ChatbotSchema` took in objectui#6169.
-
-**Additive for authors**: a predicate string is now accepted where it was refused; every
-boolean that parsed before parses unchanged, and a number is still refused at path
-`disabled`. Runtime behaviour does not change — the renderer already evaluated both twins.
-
-**Out of scope, per the ruling**: `label` (29 narrowings) and `description` (32) carry
-`string | I18nLabel` i18n semantics and wait for their own ruling; the independent
-`disabled?: boolean` declarations on shapes that do not extend `BaseSchema`
-(`SelectOption`, `RadioOption`, `FormField`, `ComboboxOption`, `AccordionItem`,
-`ToggleGroupItem`, and the rest of that family) are not narrowings and are untouched.
diff --git a/.changeset/7088-hidden-jsdoc-single-hide-path.md b/.changeset/7088-hidden-jsdoc-single-hide-path.md
deleted file mode 100644
index aca98a42e6..0000000000
--- a/.changeset/7088-hidden-jsdoc-single-hide-path.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Correct `BaseSchema.hidden`'s JSDoc: it hides by NOT RENDERING, exactly as
-`visible: false` does (objectui#7088, maintainer ruling 2026-09-01).
-
-The declaration promised "Controls whether the component is hidden (but still
-rendered) … component is rendered but not visible (visibility: hidden)". The
-renderer has never done that. `visible`, `visibleWhen`, `visibleOn`,
-`visibility`, `hidden` and `hiddenOn` are legs of one `shouldHide` chain in
-`SchemaRenderer`; every leg feeds the same `_hidden` flag, and `_hidden` has
-exactly one consumer — `if (evaluatedSchema._hidden) return null`. No node
-survives for either key, and nothing in the repo emits a `visibility` style. The
-sibling `visible` comment claimed `display: none` on the same false premise and
-is corrected with it.
-
-**Comment-only — no behaviour moves.** The other reading, keeping the node in the
-tree and hiding it visually, was weighed and **declined**: it is a behaviour
-change on a published prop with zero named consumers, so an accessibility or
-animation use-case that wants it reopens the question as its own feature card.
-The JSDoc now records the synonymity as a decision, so the next reader does not
-read "two keys" as "two behaviours", and notes that synonymous in OUTCOME is not
-synonymous in PRECEDENCE — a declared `visible` short-circuits `hidden`, which
-is unchanged and pinned elsewhere.
-
-Why a comment was worth a changeset: the JSDoc is the authority a later docs
-correction is measured against, and this one nearly propagated. While splitting
-the schema-reference `hidden` row, a reader checked it against `base.ts` and
-almost "corrected" the table's "Inverse of `visible`" — the half that describes
-shipped behaviour — toward the declaration. That row is unchanged and stays.
-`SchemaRenderer.hiddenVisibleSynonymy.test.tsx` now pins the claim the comment
-makes: the two keys produce the same rendered output, and `hidden: true` leaves
-no node that could carry a `visibility` style.
diff --git a/.changeset/7092-launcher-exclusion-reason.md b/.changeset/7092-launcher-exclusion-reason.md
deleted file mode 100644
index 19513e04b1..0000000000
--- a/.changeset/7092-launcher-exclusion-reason.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
----
-
-Corrects the `PALETTE_EXCLUSIONS` reason string for `app:launcher` in the Studio
-page-palette ledger. It read "shell singleton — the app shell renders it, not a page",
-and objectui#7091 registered a real `app:launcher` renderer, so a page can now render it
-and the "not a page" clause became false. The reason now reads "shell singleton — lives
-in the app shell chrome", mirroring the neutral form its sibling `global:notifications`
-has carried since objectui#6757 shipped that type a renderer without its wording rotting:
-the entry describes WHERE the thing lives, not whether a renderer exists, so a later
-renderer cannot falsify it.
-
-The exclusion itself is an unchanged decision — `app:launcher` stays out of the page
-palette, and its key and position in the ledger are untouched. Whether it should become
-palette-authorable is a separate product question this does not answer. No published
-behaviour moves: the reason strings are developer-facing ledger prose, read by no runtime
-code path, and `PALETTE_EXCLUSIONS` is not exported from `@object-ui/app-shell`'s entry.
diff --git a/.changeset/7104-alert-dialog-read-dialect-declared.md b/.changeset/7104-alert-dialog-read-dialect-declared.md
deleted file mode 100644
index 6ecda53472..0000000000
--- a/.changeset/7104-alert-dialog-read-dialect-declared.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`AlertDialogSchema` now declares the four keys the `alert-dialog` renderer actually reads
-(objectui#7104): `content` (the dialog body, `SchemaNode | SchemaNode[]` like every sibling
-overlay), `cancelText` and `actionText` (the footer's two button labels — each button renders
-only when its label is set; there is no renderer default) on BOTH faces, and `onAction` (the
-confirm button's click handler) as a RUNTIME SLOT in the objectui#6124 shape: callable on the
-TypeScript face, refused by name in the zod mirror because JSON has no function value.
-
-Until now none of the four was declared anywhere. They were accepted only through
-`BaseSchema`'s `[key: string]: any` and the mirror's `.passthrough()` — no editor completed
-them, no page named them, and a wrong-typed value rode through unexamined — while the keys
-the type DID declare for the same affordance (`cancelLabel` / `confirmLabel` /
-`confirmVariant`) are read by nothing, so a document written strictly against the shipped
-type rendered an empty footer. The renderer's own registered `inputs` and `defaultProps` were
-already written in the read dialect; this change makes that single de-facto contract legible
-instead of minting a second one (AGENTS.md #0.1: one strict contract, not N dialects).
-
-**Accept-set change on the published zod mirror — breaking, shipped as `minor` per this
-repo's version-alignment policy (majors track `@objectstack`).** Declared keys are validated
-even under `.passthrough()`, so three documents that parsed green yesterday are refused
-today, each at its own path: `cancelText` or `actionText` carrying a non-string
-(`cancelText: 123` — the renderer drew it as button text), `content` carrying a value that is
-not a node or node array (an object without `type`), and `onAction` carried at all (a JSON
-author cannot write a function; a string or object there was accepted and forwarded to the
-button, where it did nothing or threw at click). A document in the read dialect with
-well-typed values parses exactly as before and its values now survive the parse typed.
-Undeclared keys still pass through unchanged. On the TypeScript face, `cancelText: 123` is
-now a compile error at the key where the index signature used to absorb it.
-
-**No renderer change; no runtime behaviour changes.** The `alert-dialog` renderer, its
-`inputs` and its `defaultProps` are untouched. The three declared-but-unread keys are
-deliberately NOT retired here — that is a narrowing with its own card and its own grade;
-their per-key liveness readings are on objectui#7104.
-
-Docs: `content/docs/components/overlay/alert-dialog.mdx` now publishes the read dialect in
-its Schema block and no longer lists `actions?: BaseSchema[]`, a key no surface ever carried.
-The four schema-catalog examples that page embeds still author `actions` and render an empty
-footer — filed as objectui#7693, not converted here (the conversion is lossy).
diff --git a/.changeset/7113-chart-data-model.md b/.changeset/7113-chart-data-model.md
deleted file mode 100644
index f39125ffaf..0000000000
--- a/.changeset/7113-chart-data-model.md
+++ /dev/null
@@ -1,115 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ChartSchema` declares the data model it renders — chart-level `data` and `xAxisKey`, with
-the bare-string `xAxis` folded onto the latter — and `ChartDataSeries` accepts both binding
-dialects (objectui#7113 option B, 项目总监席 总监批 #28 2026-09-01 「同意」; and
-objectui#6939's `chart` row, maintainer ruling 2026-09-02 「同意」 — both rulings
-independently instructed declaring these two keys, so they land as one change).
-
-⚠️ Shipped as `minor`, not `patch`, because two document classes that validated before now
-REFUSE. objectui#6939 grades this class "patch where the accept set only widens toward what
-already renders"; this change is not a pure widening, so it takes the level objectui#6896
-set for the same transition in this same file — the mirror starting to refuse — and for the
-same reason: this repository's `major` is a cross-repo pin to `@objectstack`'s major rather
-than a severity dial, so the break is announced here, which is the channel that carries it.
-
-## What now refuses (the narrowing, named)
-
-**Three** classes validated before and refuse now. The first two survived only on
-`BaseSchema`'s `.passthrough()`; the third was silently STRIPPED by the non-strict
-`ChartDataSeriesSchema` object.
-
-```jsonc
-// 1. chart-level `data` that is not an array of row objects
-{ "type": "chart", "chartType": "bar", "data": "oops" } // now: [data] expected array
-{ "type": "chart", "chartType": "bar", "data": [1,2,3] } // now: [data.0] expected object
-
-// 2. a non-string `xAxisKey`
-{ "type": "chart", "chartType": "bar", "xAxisKey": 123 } // now: [xAxisKey] expected string
-
-// 3. a non-string `series[].dataKey` ⚠️ THIS ONE DRAWS A REAL CHART TODAY
-{ "type": "chart", "chartType": "bar",
- "series": [{ "name": "a", "dataKey": 123 }] } // now: [series.0.dataKey] expected string
-```
-
-⚠️ **Class 3 is the sharp one and is called out separately.** Classes 1 and 2 are malformed
-documents whose chart was already broken. Class 3 is not: at base it parsed to
-`series: [{ name: 'a' }]` (the non-string `dataKey` stripped in silence) and
-`normalizeChartSchema` renders it — `str(123)` is `undefined`, so the read falls back to
-`name` and yields `series: [{ dataKey: 'a' }]` (`normalizeChartSchema.ts:239`). So this is a
-narrowing away from a document that **renders today**, which is precisely the distinction
-objectui#6939's grading language turns on. `dataKey: null` behaves identically. Measured on
-both states; the declaration itself is right, and this note is the disclosure it was owed.
-
-## Corrected: what class 2 actually did
-
-An earlier draft of this changeset said `xAxisKey: 123` "drew an EMPTY CHART". The read
-sites do not support that: `ChartRenderer.tsx:133` takes `schema.xAxisKey` raw and the rows
-still reach `data` at `:164`, while the normaliser drops the key (`str(123)` is `undefined`).
-Measured through `normalizeChartSchema`, the result keeps the series and loses only the
-category binding — **a drawn chart with a broken category axis**, not an empty one. Class 1
-(`data` malformed) is the one that leaves nothing to plot.
-
-## Also changed on the published surface: combinators
-
-Both consts now carry a check (`ChartSchema` the `xAxis` fold, `ChartDataSeriesSchema` the
-at-least-one-binding refinement), and on zod 4.4.3 that makes three combinators **throw**
-where they previously returned a schema:
-
-```
-ChartSchema.pick(…) / .omit(…) / .partial() -> throws "cannot be used on object
-ChartDataSeriesSchema.pick(…) / .omit(…) / … schemas containing refinements"
-```
-
-`.extend()` with a NEW key still works and preserves the fold and the refinement;
-`.optional()`, `z.discriminatedUnion`, `z.toJSONSchema` and `safeValidateSchema` are all
-unaffected. Nothing in this repository calls the throwing combinators on either const, and
-the published surface already ships refined mirrors (`objectql.zod.ts`, `complex.zod.ts`,
-`form.zod.ts`, `app.zod.ts`), so the class is not new — but it is a real behaviour change on
-a published export and it belongs in the release note rather than in a reviewer's file.
-
-## What now validates (the widening)
-
-`series: [{ dataKey: 'revenue' }]`. `normalizeSeries` reads
-`str(raw.dataKey) ?? str(raw.name)`, so `dataKey` alone has always been a complete binding
-— but the mirror REQUIRED `name` and refused it. That is why both catalog chart fixtures
-(`advanced-line-chart.json`, `area-chart.json`) failed validation: they are the `chart: 2`
-entry in `objectui check`'s 28-file census. `name` is now optional, `dataKey` is declared,
-and a series binding to NEITHER is refused by name at `series.N.name` — the same path the
-required flag used to report, so the diagnostic did not move.
-
-## `xAxis` folds; it does not become a second name
-
-`xAxis: 'month'` is accepted at input and is ABSENT from the output, having landed on
-`xAxisKey`. When both are written the canonical key is kept and the alias dropped — not a
-precedence rule minted here, but the one already running at `normalizeChartSchema.ts:292`,
-where `xAxisKey` is the first limb of `str(schema.xAxisKey) ?? xAxisSpec?.field ??
-str(xAxisRaw)`. No chart that renders today changes what it renders.
-
-⚠️ The `xAxis` **config object** (`{ field, format, title, showGridLines }`) is NOT folded.
-Only the bare string is a sibling spelling of `xAxisKey`; the object's presentation keys
-survive separately into `out.xAxis` (`normalizeChartSchema.ts:289-291`), and folding it
-would discard them.
-
-## Not done, deliberately
-
-objectui#6939's `chart` row also says "`series[].data` stops being required". On this base
-it already is not: objectui#6896 replaced it with `retirementTombstone(...)` —
-`z.never({ error }).optional()` — which is optional AND refuses any authored value by name.
-Implementing the clause literally would re-widen a retired key and reverse a landed ruling,
-so it is not done.
-
-## FROM → TO
-
-```ts
-// ChartDataSeries
-- name: string;
-+ name?: string;
-+ dataKey?: string;
-
-// ChartSchema
-+ data?: Array>;
-+ xAxisKey?: string;
-```
diff --git a/.changeset/7115-root-readme-doc-gate-surface.md b/.changeset/7115-root-readme-doc-gate-surface.md
deleted file mode 100644
index fe4c97f883..0000000000
--- a/.changeset/7115-root-readme-doc-gate-surface.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
----
-
-Docs and gates only: the root `README.md` — the repository's landing page and the
-most-read authored file in it — was outside the scan surface of every doc gate,
-and had been teaching `stat-card` four times in its flagship "dashboard in JSON"
-example. Nothing registers `stat-card`, so a reader who copied the headline
-snippet got four OBJUI-001 "Unknown component type" panels.
-
-The file now joins the scan surface of `check-doc-component-types` and
-`check-doc-snippet-types`, and the four widgets are retargeted onto `statistic`,
-which is registered and declares a `value` carriage row, so the example keeps its
-expressions. Per the maintainer's 2026-09-01 ruling on the (A)/(B) fork —
-option (B), no new carriage rows — four documentation sites that authored `${…}`
-in keys with no carriage row now teach what actually renders instead.
-
-No package source changed, so this ships nothing: `check-changeset-presence`
-independently reports "no changeset is owed" for this diff. The declaration
-states the release intent rather than bumping anything.
diff --git a/.changeset/7121-studio-canvas-leaf-affordances.md b/.changeset/7121-studio-canvas-leaf-affordances.md
deleted file mode 100644
index fee18dc171..0000000000
--- a/.changeset/7121-studio-canvas-leaf-affordances.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio Interfaces: no Design mode and no "click a block" rail on leaves that have
-no block canvas (objectui#7121).
-
-`registerStudioCanvasPreview(type, …)` opts a type into a surface-specific canvas
-that renders the running app rather than an editable draft — a contract, not a
-habit: `StudioCanvasPreviewProps` carries no `selection`, `onSelectionChange`,
-`onPatch` or `editing`. Two affordances beside such a leaf ignored that.
-
-- The Design/Run switch (objectui#5800) was still offered, though `editing` is
- handed to exactly one canvas branch (`Preview`). On a studio-canvas leaf the
- switch moved `canvasMode` and reached no renderer — a live-looking control
- wired to nothing. It is now gated.
-- The right rail fell through to "Click a block on the canvas, and edit its
- properties right here." beside a canvas that has no blocks, so the instruction
- could not be followed. It now states what the canvas is, and — because this
- canvas has no blocks by contract — promises no recovery.
-- The rail's new branch is ordered ahead of the selection branch, so a block
- selected on a *different* leaf no longer opens a scoped inspector for a block
- this canvas does not contain; the header's "clear selection" button is gated
- with it.
-
-The discriminator is `StudioCanvas`, not `isEditable`. `isEditable` is
-`!!Preview && !StudioCanvas` — a conjunction of two independent causes — so
-gating on it would also strip these affordances from leaves whose only fault is
-that their own type has no designer, the state objectui#6795 part C pinned as
-still deserving the ordinary rail. Behaviour on every leaf with a block canvas
-is unchanged.
diff --git a/.changeset/7122-capability-manage-org-presentation.md b/.changeset/7122-capability-manage-org-presentation.md
deleted file mode 100644
index 1c0c47e35b..0000000000
--- a/.changeset/7122-capability-manage-org-presentation.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/fields': patch
-'@object-ui/i18n': patch
----
-
-Carry `manage_org_presentation`, the ninth platform capability (objectui#7122).
-
-`@objectstack/spec` 17.3.0 declares a ninth member of `PLATFORM_CAPABILITIES`
-and the capability picker's curated set carried eight, so
-`CapabilityMultiSelectField` fell back to the `sys_capability` registry's
-English label for it in every locale — the exact defect objectui#6285 filed
-when `manage_sharing` did the same thing.
-
-The label is the spec artifact's own (`Manage Organization Presentation`), read
-off the installed build rather than invented, and it is authored everywhere the
-widget's docblock requires of any edit to that list: `useFieldTranslation.ts`
-and all ten locale packs. Each non-English string is composed from that pack's
-own established sibling vocabulary (`manage_org_users`,
-`manage_platform_settings`) rather than machine-translated; a native review pass
-is welcome on the nine, and nothing about the capability's behaviour depends on
-the wording.
-
-The parity pin is unchanged and still fails on ANY difference in either
-direction, which is what made this visible before it reached a screen.
diff --git a/.changeset/7122-eager-closure-ceiling-raise.md b/.changeset/7122-eager-closure-ceiling-raise.md
deleted file mode 100644
index 3faac05948..0000000000
--- a/.changeset/7122-eager-closure-ceiling-raise.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
----
-
-Raise the console eager-closure ceiling once, on explicit maintainer
-authorisation, to the measured cost of `@objectstack/spec` 17.3.0
-(objectui#7122). Tooling only; no package is released by this change.
-
-Raising a gate ceiling is a gate weakening and sits on the manual floor, so it
-is a human's decision. The authorisation, verbatim:
-
- 维护者 sam@objectstack.ai 于本轮明确授权:「抬上限,把 7685 弄绿」
-
-recorded on objectui#7122 as decision batch 1 item 1 = "B + A": a one-time,
-cause-recorded adjustment of exactly the measured delta, with the upstream card
-filed alongside it.
-
-**What the bytes buy — this is NOT routine growth.** The residue is
-`@objectstack/spec` 17.3.0's own browser-dist growth: +292.2 KB gzip on the spec
-package alone, whose measured mechanism is that 17.3.0 lengthened the Zod
-`.describe()` doc strings shipped in the browser build. It is authoring
-documentation prose, delivered on every page load.
-
-**It is not duplication.** That was the larger, separate problem and it is
-already fixed: resolving the spec alone to 17.3.0 shipped two copies of it, and
-moving the family together in the lockfile collapsed that (−671 KB; markers
-unique to 17.2.0 fell from 92.3% to 1.0% of 104, the single survivor accounted
-for). No chunk entered or left the closure and the eager chunk count is
-unchanged.
-
-**The honest long-term fix is upstream**, and this ceiling is the marker for it
-rather than the answer to it: a `describe()`-stripped browser build returns
-~292 KB to every consumer of the spec, not just this console. Filed as
-objectstack#16063, and recorded in the gate as the restore condition — when it
-lands, re-measure and bring the ceiling and its baseline back down together.
-
-Four constants move as two ceiling/baseline pairs, measured by `pnpm build`
-(exit 0, 43/43) on `34a1578ef`:
-
-| constant | from | to | headroom vs the 91,136-byte regression |
-|---|---|---|---|
-| `MAX_EAGER_CLOSURE_GZIP_BYTES` | 3,268,000 | 3,597,000 | 45,809 = 0.50x |
-| `BASELINE.gzipBytes` | 3,222,314 | 3,551,191 | — |
-| `PER_CHUNK_GZIP_CEILINGS['vendor-objectstack']` | 967,000 | 1,254,000 | 18,971 = 0.21x |
-| `PER_CHUNK_BASELINE['vendor-objectstack']` | 948,461 | 1,235,029 | — |
-
-Both ratios match the proportions the retiring pairs carried (0.50x and 0.20x),
-so the gate keeps failing on a repeat of the 89 KiB regression that motivated
-it. No other exemption was added, no import was made lazy, and the three
-per-chunk lines that still pass — `i18n-locales`, `framework`, `ui-components` —
-were left exactly as they are.
diff --git a/.changeset/7122-objectstack-family-17-3-0.md b/.changeset/7122-objectstack-family-17-3-0.md
deleted file mode 100644
index 7fe38e7a4e..0000000000
--- a/.changeset/7122-objectstack-family-17-3-0.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/data-objectstack': patch
----
-
-Reconcile `@object-ui/data-objectstack` with the `@objectstack/*` family at
-17.3.0 (objectui#7122).
-
-`@objectstack/client`, `core`, `formula` and `lint` each pin `@objectstack/spec`
-EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO
-copies of it. Moving the family with it in `pnpm-lock.yaml` collapses the
-duplicate; every declared range already admitted 17.3.0, so no manifest moved.
-
-The one source change the family bump forces is a type reconciliation, not a
-behaviour change. `client.analytics.query` resolved to `Promise` at 17.2.0
-and resolves to `Promise` at 17.3.0, so the pre-envelope
-branches of `aggregate`'s row-shape fallback stopped type-checking. Those
-branches are read through a widened alias rather than deleted: the client's own
-docblock records the runtime change behind the narrower type ("BREAKING since
-objectstack#13079 — read `result.rows`, not `result.data.rows`"), and deleting
-them is a runtime compatibility decision about servers older than that, not a
-type repair. The alias restores exactly the compile-time latitude 17.2.0 gave
-the same expression and changes no runtime byte of it.
diff --git a/.changeset/7122-postcss-dedupe.md b/.changeset/7122-postcss-dedupe.md
deleted file mode 100644
index 71203b4c10..0000000000
--- a/.changeset/7122-postcss-dedupe.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
----
-
-Dev-time only: collapse the `postcss` duplicate this branch's first lockfile
-bump introduced (objectui#7122).
-
-`aabc527cb` resolved `@objectstack/spec` to 17.3.0 and, as a side effect of that
-install, re-resolved `autoprefixer`'s auto-installed `postcss` peer to 8.5.28
-while `@tailwindcss/postcss` kept 8.5.26. `@object-ui/cli` depends on both and
-declares `postcss` itself, so its type-check program held TWO postcss copies
-with two structurally distinct `Plugin` types — and comparing
-`Plugin & ExportedAPI` against `Plugin` across them exceeded the compiler's
-instantiation budget: `TS2321: Excessive stack depth`, in a file this branch
-never touched. `origin/main` carries one copy for that pair and is green.
-
-The split is resolved in the lockfile only. All eight manifests that declare
-`postcss` already declare `^8.5.26`, which admits 8.5.28, so no declared range
-moved and no published dependency declaration differs by a byte. Empty
-frontmatter is the deliberate "no release" declaration.
diff --git a/.changeset/7122-spec-17-3-0-parity-reconciliation.md b/.changeset/7122-spec-17-3-0-parity-reconciliation.md
deleted file mode 100644
index 33c86a70e6..0000000000
--- a/.changeset/7122-spec-17-3-0-parity-reconciliation.md
+++ /dev/null
@@ -1,89 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': patch
-'@object-ui/cli': patch
-'@object-ui/plugin-detail': patch
----
-
-Reconcile the declared surface with `@objectstack/spec` 17.3.0 (objectui#7122).
-
-⚠️ **`@object-ui/types` is graded `minor` for a breaking surface change.**
-The exported `ObjectSchemaClientExtensions` narrows from
-
-```ts
-export interface ObjectSchemaClientExtensions { editMode?: 'modal' | 'page' }
-```
-
-to
-
-```ts
-export type ObjectSchemaClientExtensions = Record;
-```
-
-Two breaking consequences for a consumer that names the type directly. **(1)** It
-no longer declares `editMode`; the key is now carried by the spec's
-`ServiceObject`, so `ObjectSchemaMetadata` still has it, but code written against
-the extension type ALONE loses it. **(2)** `interface` → type alias also ends
-**declaration merging**: a consumer that reopened
-`declare module '@object-ui/types' { interface ObjectSchemaClientExtensions { … } }`
-to add its own client-side member no longer compiles, because an alias cannot be
-reopened. `minor` rather than `major` per `AGENTS.md`'s version-alignment rule —
-objectui's own breaking changes are graded `minor` with the semantics stated in
-the body, since any `major` in the fixed group would push all 39 packages off
-`@objectstack`'s major.
-
-**`ObjectSchema.editMode` is now the spec's.** 17.3.0 adopted the key (measured:
-the accept set went 42 → 43, gained set exactly `['editMode']`, lost set empty,
-declared as the same `'page' | 'modal'` union objectui carried). Its local copy
-is retired from `ObjectSchemaClientExtensions`, which is what that type's own pin
-prescribed for this event, leaving the client delta empty. Nothing is removed
-from the product: `editMode` stays authorable and stays typed on
-`ObjectSchemaMetadata`, carried by the spec's `ServiceObject` instead of by a
-local member — and a published, spec-validated object document may now carry it,
-which at 17.2.0 was refused by name.
-
-**`user:profile` is retired across all three sites.** 17.3.0 dropped it from
-`PageComponentType` (measured: the enum went 34 → 32 options, lost set exactly
-`['user:profile', 'element:form']`, gained set empty). objectui went on knowing
-it in three places, so all three moved together: the Studio palette exclusion
-ledger, `PROTOCOL_COMPONENTS` in `renderers/placeholders.tsx`, and the
-regenerated `known-schema-types.ts` the CLI checks schemas against. Nothing
-user-reachable went with it — neither type had a renderer, `user:profile` had
-only the dashed "Component Placeholder" scaffold, and the app shell's own
-profile affordance is a React slot, never this block type. A page schema still
-naming it now draws the loud "Unknown component type" panel rather than a silent
-grey box, which is this repo's standing treatment for a type outside the
-supported surface.
-
-**`record:details` sections document the eight keys 17.3.0 added.**
-`group`, `hideEmpty`, `collapsible`, `showBorder`, `defaultCollapsed`, `icon`,
-`description` and `headerColor` are now declared on a section entry (4 → 12
-members). Six of the eight are already honoured by `DetailSection`, so the
-`sections` input description now teaches all of them, and says plainly which two
-are not read here. Designer controls for them are a separate feature and are
-deliberately not added.
-
-**`@object-ui/types` raises its declared `@objectstack/spec` floor `^17.0.0` →
-`^17.3.0`, and this is the second half of its `minor`.** The package's emitted
-`dist/spec-report.d.ts` names `FilterCondition` from `@objectstack/spec`, which
-`17.0.0` does not export, so the old range was a claim the artifact did not
-support — `scripts/check-spec-range-floors.mjs` reports it as `[floor-too-low]`
-and names `^17.3.0` as the lowest version carrying every symbol the package
-references. Breaking for a consumer pinned below 17.3.0: it can no longer
-resolve this package. That is the range stating the truth rather than a new
-restriction — the artifact already required those symbols — and it is the
-remedy the gate itself prescribes ("Raise that package's range to the lowest
-version that exports the symbol… Do not add a tolerant re-declaration on this
-side: the range is the claim, and the claim is what is wrong", objectui#5793).
-`@object-ui/core` and `@object-ui/data-objectstack` already declare `^17.2.0`
-and `@object-ui/plugin-detail` `^17.1.0`, so a floor above the family minimum is
-this repo's normal state, not an exception.
-
-⚠️ **Measured on both sides, because it is bump-caused rather than pre-existing
-and objectui#7688 records the opposite.** The gate is a scheduled / push-to-main
-workflow that cannot red a pull request, and `main` is green on it — the last
-eight runs, most recently at `c2e3cee2c`. On this branch's built tree it exits 1
-with CI's own `--cross-check` invocation, and exits 0 with this raise, judging
-278 (subpath, symbol) pairs across 19 published packages either way. Its blocking
-copy runs on the publish path, so leaving it would have surfaced as a cancelled
-release rather than as a red check. The correction is recorded on objectui#7688.
diff --git a/.changeset/7122-spec-17-3-0-pin-repointing.md b/.changeset/7122-spec-17-3-0-pin-repointing.md
deleted file mode 100644
index 86678dc722..0000000000
--- a/.changeset/7122-spec-17-3-0-pin-repointing.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
----
-
-Re-point the pins `@objectstack/spec` 17.3.0 moved, and harvest three
-exemptions the contract outgrew (objectui#7122). Test only; no package is
-released by this change.
-
-- **The `#7496` family, 14 assertions across three files.** 17.3.0 kept every
- `submitBehavior.url` refusal and its reasoning and only restated provenance as
- `(ruled 2026-08-11)`. The pins asserted the citation FORM, not the citation —
- objectui#7702's defect shape — so they now assert the durable property via a
- documented `CITES_ITS_RULING`, which discriminates the spec's prose from a
- locally hand-written sentence. Deliberately not re-pinned to the new prose
- verbatim, which would move the brittleness rather than remove it.
-- **`CITES_ITS_RULING` is `/\(ruled \d{4}-\d{2}-\d{2}(?: on #\d{3,})?\)/`,
- and its first spelling was not good enough.** That draft was
- `/\(ruled \d{4}-\d{2}-\d{2}\)|#\d{3,}/`, whose loose alternative
- discriminated NOTHING — this repo's own hand-written messages routinely cite
- `objectui#NNNN`, so a local sentence satisfied it, and telling those two apart
- is the assertion's whole job. It was also unnecessary: measured on both
- installed artifacts, 17.2.0 says `(ruled 2026-08-11 on #7496)` and 17.3.0 says
- `(ruled 2026-08-11)`, so the issue number never appears outside that
- parenthesis and `(ruled ` + a date already matched both. The optional
- ` on #NNNN` tail keeps the 17.2.0 spelling admissible without admitting a bare
- local `#7122`.
-- **The fourth file joined the other three.**
- `packages/plugin-form/src/submitRedirect.test.ts` had been re-pointed to the
- literal `'ruled 2026-08-11'` while its three siblings used the shape — the
- same brittleness one word along, a date instead of an issue number, and four
- files disagreeing about what the pinned property is. It now uses
- `CITES_ITS_RULING` too.
-- **`object-grid.defaultSort` became an ADR-0087 D2 tombstone**, so its
- unpublished-key exemption, its GA-pending entry and its carve-out row are
- deleted — the ninth harvest by the mechanism the eight before it were designed
- to die by. The block's prose claiming a tombstone would NOT make an entry
- stale is corrected; its own cited counter-example was harvested that way.
-- **`OFF_SPEC_ARM_EXEMPTIONS` is now empty.** `element:number.filter` accepts
- the array form (objectui#6206) and `object-grid.data` accepts the
- discriminated `ViewDataSchema` object (objectui#6207) — both converged
- upstream in the direction their own reasons named, with no declaration edited.
- Both issues are closable as resolved-upstream.
-- **Two negative pins re-derived, not inverted.** The `object-master-detail-form`
- `formType` boundary now refuses what it used to admit (`invalid_value`, plus a
- bespoke ADR-0001 prescription for `wizard`), and the location value schema is
- now strict (`unrecognized_keys` naming the retired `latitude`/`longitude`
- pair). Each pin records the new fact, keeps a control opposite it, and pins the
- refusal's SHAPE. The `LocationField` fence explicitly re-derives why its
- key-level assertions stay: no parse stands on the emit path, so strictness
- changed the consequence of a spread regression, not the widget's exposure.
diff --git a/.changeset/7122-spec-17-3-0-reconciliation.md b/.changeset/7122-spec-17-3-0-reconciliation.md
deleted file mode 100644
index d098104278..0000000000
--- a/.changeset/7122-spec-17-3-0-reconciliation.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/auth': patch
----
-
-Reconcile `@object-ui/app-shell` with `@objectstack/spec` 17.3.0 (objectui#7122).
-
-`SchemaDiffEntryKind` gained an `unreachable` member in 17.3.0, and the external
-datasource validation panel labels those kinds through a map that is TOTAL over
-the union on purpose — so an upstream addition fails the build rather than
-rendering a blank cell. That mechanism fired: the package did not compile against
-17.3.0 until the kind was labelled. It now reads "Not checked — remote
-unreachable", following the spec's own ruling that this kind asserts nothing
-about the remote schema and must never be surfaced as "schema changed": it means
-introspection could not complete, which is often transient, and labelling it like
-a mismatch would tell an operator to repair a schema nobody has read.
-
-Two internal flow-inspector types were renamed (`FlowNodeLike` →
-`InspectorFlowNode` / `ScopeFlowNode`) because 17.3.0 began exporting its own
-`FlowNodeLike`; neither name is in this package's published entry, so no consumer
-import changes.
-
-`@object-ui/auth`: a README sentence claiming the preview-mode prop aligns with
-the spec's `PreviewModeConfig` is corrected — 17.3.0 removed that symbol. The
-`previewMode` prop itself is host-supplied, unchanged, and unaffected.
diff --git a/.changeset/7125-dashboard-empty-state-keys-retired.md b/.changeset/7125-dashboard-empty-state-keys-retired.md
deleted file mode 100644
index a71b407ce7..0000000000
--- a/.changeset/7125-dashboard-empty-state-keys-retired.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-`dashboard.noRows` and `dashboard.noDataAvailable` are retired — two rows removed from
-each of the ten locale packs, 20 entries, zero readers (objectui#7125).
-
-objectui#7063 routed the three dashboard empty-state renders (`DatasetWidget`,
-`ObjectDataTable`, `PivotTable`) through one shared `WidgetEmptyState`, which resolves its
-own copy from the `dashboard.empty.*` family. The two keys the old per-widget placeholders
-used outlived their call sites in `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`.
-
-Removed under objectui#4658's evidence standard, re-measured on this branch rather than
-inherited from the card: zero `t()`/`tt()` call sites for either fully qualified key, no
-dynamic `dashboard.` head a substitution could resolve onto them, and every surviving
-textual occurrence in `packages/` is a comment recording the consolidation. `pnpm
-check:i18n-keys` stays green across the deletion with the `en` pack at 2,962 keys (2,964
-before) and every in-scope call-site key still resolving.
-
-Not touched: `table.noRows` (`'No rows to display'`) and `engine.form.noRows`
-(`packages/app-shell/src/views/metadata-admin/i18n.ts`, read at `widgets.tsx`) — two
-different, same-named keys in different namespaces. Nor the comments in
-`WidgetEmptyState.tsx`, `DatasetWidget.tsx`, `ObjectDataTable.tsx` and `PivotTable.tsx`
-that record WHY three widgets with three strings became one shared empty state; the packs'
-own comment keeps that rationale and now names the retirement instead of a row that is
-gone.
-
-`packages/i18n/src/__tests__/dashboard-emptyState-keys-retired-7125.test.ts` pins the removal
-by name, following the five prior retirements (objectui#4145, objectui#4392, objectui#4730,
-objectui#5504, objectui#6310). Every i18n gate here runs call site → key, so none of them can
-see a dead key come BACK into the packs: the reverse sweep that found these is report-only by
-design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and
-`check:i18n-drift` reported the deletion as `2 removed — those are all-locales-key-parity's`.
diff --git a/.changeset/7126-inline-widgets-read-error.md b/.changeset/7126-inline-widgets-read-error.md
deleted file mode 100644
index 2229bcffde..0000000000
--- a/.changeset/7126-inline-widgets-read-error.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-The last five inline edit widgets read the delivered `error` slot, so a failed
-required `text` / `boolean` / `date` / `datetime` / `time` control finally
-reports `aria-invalid` (objectui#7126).
-
-objectui#7008 made `FieldEditWidget` DELIVER the declared `error` key to
-whichever widget it resolves. Of the 27 distinct components in `EDIT_WIDGETS`,
-21 read it; five did not — `TextField`, `BooleanField` (serving both `boolean`
-and `toggle`), `DateField`, `DateTimeField` and `TimeField` — so for their field
-types the delivery was inert and the attribute was still never set.
-
-`text` being in that set is what made this a live defect rather than tidiness.
-It is the most common field type in any object, so it is the likeliest thing a
-kanban column makes required: `RequiredFieldsDialog` computed the failure, drew
-the red "Required" hint, handed the state to the control, and the control said
-nothing to assistive tech. The grid's inline cell editor and the detail page's
-inline edit (`InlineFieldInput`) compose the same seam.
-
-Each of the five now computes `aria-invalid={!!error}` **after** its DOM
-pass-through spread — one existing idiom, the objectui#3222 discipline the other
-21 already share, so a valid field says an explicit `"false"` rather than staying
-mute. Two judgements worth stating:
-
-- **The FORM path was never broken and is unchanged.** `` is a
- Radix `Slot` whose `aria-invalid` reached each control through the props
- spread; the form also produces `error`, so the widget's own computation now
- agrees with the value it replaces. The gap was every host WITHOUT that Slot.
-- **`BooleanField` is the one composite here, and the mark goes on the
- control.** Its Radix `Checkbox` / `Switch` renders a real
- `button[role=checkbox]` / `button[role=switch]`; the wrapping flex `div` is
- deliberately not the target, because a wrapper mark satisfies a subtree query
- while telling a screen-reader user nothing (objectui#5223). The three
- date/time widgets each render one native input, so the browser's picker raises
- no second-element question.
-
-This buys the MARKING only. The objectui#3222 slot drives `aria-invalid` and
-renders no text: the visible message stays with the host, and nothing that was
-invisible becomes visible.
diff --git a/.changeset/7127-record-details-refuses-not-strips.md b/.changeset/7127-record-details-refuses-not-strips.md
deleted file mode 100644
index fb463e50c6..0000000000
--- a/.changeset/7127-record-details-refuses-not-strips.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Comment-only. The `record:details` and `record:highlights` registration comments in
-`@object-ui/plugin-detail` stated that the spec STRIPS an undeclared key on parse.
-Measured against the installed `@objectstack/spec` 17.2.0, it REFUSES it — `safeParse`
-returns `success: false` with `unrecognized_keys` naming the key — so both comments now
-state the mechanism they actually rely on. No published behaviour changes: the decision
-both comments record (do not publish those keys as authorable inputs) is unchanged, and
-no declaration, type or renderer was touched.
diff --git a/.changeset/7129-retire-detailviewsection-hideempty.md b/.changeset/7129-retire-detailviewsection-hideempty.md
deleted file mode 100644
index 0273356e77..0000000000
--- a/.changeset/7129-retire-detailviewsection-hideempty.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-detail': minor
----
-
-**Retired: `DetailViewSection.hideEmpty`.** The `record:details` section key is
-gone from `@object-ui/types` and `RecordDetailsRenderer` no longer reads it.
-Emptiness on a detail section is now decided entirely by `DetailSection`'s
-auto-hide heuristic — hide empty rows only while the section still has at least
-one filled row, never on an all-empty section — with the reader's
-"Show N empty fields" toggle as the escape hatch.
-
-**Minor, not major, and deliberately so.** The key was never authorable on any
-validated page: `@objectstack/spec` `RecordDetailsProps` REFUSES it, returning
-`unrecognized_keys: ['hideEmpty']` on the `sections[]` element (measured on the
-installed 17.2.0, against a `columns: 2` control that parses and whose value
-survives). So a spec-compliant document could not carry the key, and a document
-that carried it anyway failed to parse before it ever reached the renderer.
-What this release removes is a *declaration* that invited authors — and code
-generators reading the published `.d.ts` — to write a key the platform refuses.
-That narrows a published type surface, which is what makes it a minor rather
-than a patch; it retires no capability anyone could exercise.
-
-One key had four contracts and three answers: `@object-ui/types` declared it,
-`RecordDetailsRenderer` honoured it, the `DetailViewSectionSchema` zod mirror
-omitted it, and the spec refused it. The maintainer converged the four on the
-spec's answer (2026-09-01): the spec keeps refusing, the mirror stays absent,
-and the declaration and the read are retired. All four are now pinned together
-in `record-details.hideEmptyRetired-7129.test.tsx`.
-
-Going with it is the paradox the key carried: `DetailSection` tested
-`!section.hideEmpty`, so an authored `hideEmpty: false` was indistinguishable
-from an unauthored section and overrode nothing. There is no longer a lever to
-misread.
-
-**Supersedes one paragraph of the `record:details` empty-section changeset in
-this same release.** Its closing "What does not change: an authored `hideEmpty`
-keeps its exact former meaning" no longer holds — an authored `hideEmpty` of
-either polarity is now inert, and the release notes should read that way.
-Everything else in it stands: the unauthored default is unchanged, and so is
-the label-graveyard guard.
-
-**Migration:** delete `hideEmpty` from any `record:details` section you author.
-A section that used `hideEmpty: true` to hide an all-empty block will now show
-that block's skeleton — headings, field labels and one empty-value placeholder
-each. That is the platform's answer for a sparse record, and it is a UI
-decision, not something metadata should have to make.
-
-**Not affected**, despite the shared name: `record:reference_rail`'s own
-`hideEmpty` prop, which is a different surface and still live; and the
-`detail.hideEmptyFields` i18n label behind the toggle.
diff --git a/.changeset/7132-empty-state-role-default.md b/.changeset/7132-empty-state-role-default.md
deleted file mode 100644
index 4b6cce93ac..0000000000
--- a/.changeset/7132-empty-state-role-default.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-'@object-ui/components': minor
-'@object-ui/plugin-list': patch
----
-
-`DataEmptyState` now declares `role="status"` by default, so an empty result is
-distinguishable from a failed one on every surface that renders it
-(objectui#7132).
-
-This is the convergence half of the two rulings that landed as objectui#7063 and
-objectui#7064, both resting on objectstack#13848: uniform behaviour belongs to
-the platform, and per-surface compensation is the per-app tax being ruled
-against. Those two fixed their own surfaces deliberately and locally; this card
-measured whether the shared primitive should carry the property. It did not.
-
-**Measured, not assumed.** All the surfaces were rendered and their empty boxes
-read directly:
-
-| surface | `role` before |
-|---|---|
-| `DataEmptyState` bare default | *none* |
-| `plugin-list` empty list | *none* |
-| `plugin-list` load-error panel | *none* |
-| `plugin-detail` activity timelines | *none* |
-| `ui:empty` schema renderer | *none* |
-| `plugin-dashboard` `WidgetEmptyState` (#7063) | `status`, typed at the call site |
-| `plugin-kanban` empty board | `status`, typed at the call site |
-
-The sibling states in the same file had always declared themselves —
-`DataLoadingState` is `role="status"`, `DataErrorState` is `role="alert"` — and
-the empty state alone declared nothing. So the surfaces were not legitimately
-differing: the ones that wanted the property had each hand-typed the same line,
-and the ones that had not yet done so were silently missing it. That is one
-platform default, copied by hand, at package level.
-
-**It is a default, not a fixed attribute** — `role` is spread from props, so a
-call site keeps the last word. That is what makes this inert for the two ruled
-surfaces: both already pass `role="status"` explicitly and receive the identical
-attribute with or without it. Neither surface's behaviour changes.
-
-**One real defect fell out of the measurement.** `plugin-list` renders its load
-FAILURE through `DataEmptyState`, borrowing it for layout — so a 403 saying "You
-don't have access" and a young object saying "Nothing here yet" were the same
-node shape, with no role on either. That panel now declares `role="alert"`,
-which both fixes the pre-existing indistinguishability and stops the new default
-from announcing an outage as a routine status.
-
-Metric/KPI widgets are untouched: their carve-out (`rows.length === 0 &&
-!isMetric`) gates whether an empty state is rendered *at all*, upstream of this
-component, so a KPI still reads `0` rather than "no data".
diff --git a/.changeset/7137-selection-expires-with-leaf.md b/.changeset/7137-selection-expires-with-leaf.md
deleted file mode 100644
index d05e6f0f57..0000000000
--- a/.changeset/7137-selection-expires-with-leaf.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio Interfaces pillar: a canvas block selection no longer outlives the leaf
-it was made on (objectui#7137).
-
-`InterfacesPillar`'s only clear of `selection` sat inside the draft-load effect,
-*after* its `if (!current || !isEditable) … return` guard, so it never ran on the
-early-return path. Since `isEditable = !!Preview && !StudioCanvas` is a
-conjunction, that is two populations of leaf: a studio-canvas leaf (`object`),
-and a leaf whose own type has no registered designer. Walking to either from a
-leaf with a block selected carried the selection across, still describing a block
-on the previous leaf's canvas.
-
-Two symptoms, both measured before and after:
-
-- the scoped inspector opened for a foreign block — recorded three renders as
- `page:home_page:block:blk_1`, with `blk_1` a dashboard block — and the header
- offered to clear a selection belonging to another leaf;
-- in the folded (chat-dock) layout, `hasInspectorTarget` stayed true across the
- leaf change, so `nextCenterTab` saw no edge and stranded the author on the
- Properties tab of a leaf with no properties to show.
-
-The selection is now stamped with its leaf and read back through that key — the
-same "expires by construction" shape `blockingReport` already uses against
-`inspectorKey` in this component. It goes null in the *same* render as the leaf
-change rather than one committed render later, and there is no imperative clear
-left for a future guard to strand. Within a leaf nothing changes: the Design/Run
-round trip still keeps its selection, and a same-leaf reload (`publishNonce`)
-still clears it.
-
-objectui#7121's gating of the rail and the Design/Run switch is untouched, and
-its discriminator remains `StudioCanvas` — not `isEditable`.
diff --git a/.changeset/7140-sankey-no-positive-flow.md b/.changeset/7140-sankey-no-positive-flow.md
deleted file mode 100644
index ff8b43ca82..0000000000
--- a/.changeset/7140-sankey-no-positive-flow.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/plugin-charts': minor
----
-
-A sankey with no positive flow says so, instead of rendering an empty div
-(objectui#7140).
-
-`AdvancedChartImpl`'s sankey arm keeps only strictly positive measures, so a
-chart handed **real rows** whose measure is all `0`, all `null`, all negative,
-or unparseable built no links and returned a bare ``. Measured in Chromium
-against a populated control: the control drew 1 `
` / 7 `` /
-26 descendants; each of those four tiles rendered `descendantCount: 1`,
-`svgCount: 0`, `textContent: ''`, and their screenshots hashed identical to one
-another. No marks, no text, no `role` — a tile indistinguishable from a widget
-that had crashed, which is the one distinction the file's other refusals exist
-to make.
-
-It now renders through the `ChartRefusal` shell those refusals already use —
-same box, same `role="status"`, and a new `data-chart-error="no-positive-flow"`
-— reading *"This chart has no flow to draw: no row's `` is above
-zero."*
-
-Two boundaries are deliberate and pinned:
-
-- **No rows at all is untouched.** That is the empty-result question, answered
- upstream in `ObjectChart` where the query outcome is known; a sentence about
- what the rows contain would be false about a dataset with no rows in it.
-- **One positive row among zeros still draws.** The refusal fires on an empty
- link set, never on a thin one.
-
-One code and one sentence for three causes (a genuinely all-zero flow, values a
-flow cannot represent because they are negative, and measures `Number(…) || 0`
-folds to zero): naming any single cause would be false for the other two, so
-the copy names the predicate the filter actually applies, which is true for all
-three. No recovery is promised. Every other chart family is byte-identical —
-eight of the twelve tiles in the browser sweep hashed unchanged.
diff --git a/.changeset/7142-activity-timeline-empty-i18n.md b/.changeset/7142-activity-timeline-empty-i18n.md
deleted file mode 100644
index 268d7ec355..0000000000
--- a/.changeset/7142-activity-timeline-empty-i18n.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`ActivityTimeline`'s empty state speaks the session locale (objectui#7142).
-
-The title was a raw English JSX literal — `title="No activity recorded"`, not a
-`t()` call and not an inline `defaultValue` — so it never reached the pack
-system and stayed English in all ten locales. Measured before the fix by
-rendering `activities={[]}` under a zh `I18nProvider`: the card read
-`"Activity(0)No activity recorded"`, while its sibling `RecordActivityTimeline`
-rendered `"活动(0)全部动态暂无活动记录"` from the same packs.
-
-The call site now reads `detail.noActivity`, the key the sibling already uses.
-Reusing it rather than minting a second key is a measured decision: the `en`
-pack value for that key is `'No activity recorded'`, byte-identical to the
-literal it replaces, so both surfaces were already saying the same words in
-English and a new key would have forked one sentence across ten packs for no
-copy difference. No pack was edited — the key is already translated in all ten,
-verified by reading `detail.noActivity` out of each pack object.
-
-Both routes to the box are covered: an empty `activities` array, and a
-populated timeline filtered down to a type with no entries.
diff --git a/.changeset/7143-data-error-state-migration.md b/.changeset/7143-data-error-state-migration.md
deleted file mode 100644
index 1d490b3e2d..0000000000
--- a/.changeset/7143-data-error-state-migration.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-'@object-ui/components': minor
-'@object-ui/plugin-list': minor
----
-
-`DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s
-load-failure panel is now rendered by the error state instead of the empty state
-(objectui#7143; maintainer ruling 2026-09-01, director decision batch #27).
-
-`ListView` rendered its load FAILURE through `DataEmptyState` — the component named for
-the *empty* case — passing it a destructive icon, error copy and a retry action, while
-`DataErrorState`, in the same file and with the same layout, had no consumer anywhere in
-the repo. objectui#7132 closed the accessibility half of that collision (the panel now
-declares `role="alert"` over the empty state's `role="status"` default) and deliberately
-left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a
-props-surface question plus a visual change rather than a rename.
-
-**`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored
-from `DataEmptyState` in the same file rather than spelled a second way:
-
-- `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle`
- glyph the component has always drawn.
-- `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely.
-- `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than
- merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??`
- against its own default and this does the same, against
- `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive
- square `DataErrorState` already drew.
-
-Same names, same types, same default semantics as the empty state's; nothing existing on
-`DataErrorState` changed, and a call site that passes none of the three renders exactly
-what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the
-ruling pins three props, and this component's retry affordance is already spelled
-`onRetry` / `retryLabel` (plus `children` for a call site that needs its own control).
-
-One non-prop addition rides along, called out rather than folded in: the icon wrapper now
-carries `data-slot="data-error-state-icon"`, mirroring the empty state's
-`data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no
-name — untestable and unstylable — and migrating a call site off `DataEmptyState` would
-DROP that identifier rather than rename it.
-
-**`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call
-site passes the same custom icon through the new `icon` prop, the same
-`iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the
-error state's spelling of `description`); its retry `` moves from `action` to
-`children`, which renders at the identical position. `role="alert"`, the
-`data-testid="list-error-state"` hook and `data-error-kind` are untouched. The whole
-rendered delta is two attributes:
-
-- the panel root's `data-slot` becomes `data-error-state` (was `data-empty-state`);
-- the icon wrapper's becomes `data-error-state-icon` (was `data-empty-state-icon`).
-
-Both are renames, not removals. Nothing in this repo styles or selects on either — no CSS
-rule and no test read them — so a stylesheet in a host app targeting
-`[data-slot="data-empty-state"]` to reach *this* panel is the only way to notice, and it
-should be reading `data-error-state` now. Every class on every node, and the glyphs
-themselves, are byte-identical: this is a visual no-op, deliberately, so the review the
-ruling asks for has a small thing to look at rather than a redesign.
diff --git a/.changeset/7148-sankey-omitted-rows-note.md b/.changeset/7148-sankey-omitted-rows-note.md
deleted file mode 100644
index 48b8bf99cc..0000000000
--- a/.changeset/7148-sankey-omitted-rows-note.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-A sankey that drew only SOME of its rows now says how many (objectui#7148).
-
-The sankey arm keeps strictly positive measures
-(`data.filter((r) => (Number(r?.[dataKey]) || 0) > 0)`), so a mixed dataset
-drew a normal, healthy, confident chart of a fraction of itself and nothing
-anywhere recorded that the other rows existed. Measured in Chromium across 27
-tiles: `[{New business: 40}, {Refunds: -25}, {Chargebacks: -12}]` rendered
-`svg: 1`, `path: 3`, 18 descendants, no `role`, no text, and — against a live
-console control that did fire on the same instrument — zero console output.
-Its screenshot hashed byte-identical to five other datasets, one of which
-genuinely had a single row. Six datasets, one image: a reader had no bit of
-information separating a complete flow from a third of one.
-
-The discard itself stands — a flow has no negative width, so it is the only
-thing that arm can do with those rows. What is added is a footnote under the
-plot naming the ratio and the predicate the filter applies:
-
-> Showing 1 of 3 rows — 2 rows have no `amount` above zero, which a flow
-> cannot draw.
-
-It names the predicate rather than a cause because `Number(…) || 0` folds
-negatives, zeros, `null`, unparseable strings and a missing key into one
-discard, and all five were measured reaching this branch beside a survivor;
-naming any one of them is a sentence that is false for the other four.
-
-A complete flow is byte-for-byte unchanged and gains no wrapper element, and a
-drawable sankey is never replaced by prose: the `no-positive-flow` refusal
-still owns the case where NOTHING survives the filter, and the "one positive
-among zeros still draws" boundary still draws — that fixture is itself a
-thinned dataset, so it now draws *and* says so.
diff --git a/.changeset/7149-activity-timeline-remaining-literals.md b/.changeset/7149-activity-timeline-remaining-literals.md
deleted file mode 100644
index af65d500ad..0000000000
--- a/.changeset/7149-activity-timeline-remaining-literals.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/plugin-detail': patch
-'@object-ui/i18n': patch
----
-
-`ActivityTimeline` speaks the session locale — the other 18 literals
-(objectui#7149).
-
-objectui#7142 gave this component its first `t()` call (the empty-state title)
-and filed the sweep that found the rest. Until now a zh activity tab read
-`"Activity(0)暂无活动记录"`: one translated string in a component that was
-otherwise entirely English.
-
-All 18 now resolve from the ten packs, in three groups:
-
-- **Relative timestamps and the card title** (`just now`, `{{count}}m/h/d ago`,
- `Activity`) — these render on *every* activity tab. All five were a pure
- lookup swap: the `en` pack value was already byte-identical to the literal,
- and the sibling `RecordActivityTimeline` already used the same keys.
-- **The `formatFieldChange` sentences** — assembled in code, so they needed new
- keys *with* interpolation holes rather than a lookup. Same reachability as the
- timestamps: they render for any entry whose optional `description` is absent.
- The quotes live inside each pack's value, so every locale punctuates its own
- way (de `„…“`, zh `“…”`, ja `「…」`, fr/ru `«…»`).
-- **The six filter chips and the chip group's accessible name** — reachable only
- through the published export, since no host in this repo passes `filterable`.
-
-Ten new `detail.*` keys across all ten packs (no inline `defaultValue` —
-objectui#3517), mirrored byte-for-byte into `DETAIL_DEFAULT_TRANSLATIONS` so a
-provider-less host still reads English rather than a raw key.
-
-One deliberate English copy change: the chip group's `aria-label` was
-`"Activity type filter"` and now resolves `detail.filterActivity`
-(`"Filter activity"`) — the key `RecordActivityTimeline` already uses for the
-accessible name of its own activity filter, so one control does not carry two
-names across two components.
-
-Also drops the unused `Filter` import (a pre-existing eslint warning).
diff --git a/.changeset/7151-legacy-retired-provenance.md b/.changeset/7151-legacy-retired-provenance.md
deleted file mode 100644
index 6968ead813..0000000000
--- a/.changeset/7151-legacy-retired-provenance.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only change to `plugin-dashboard`'s legacy-retired suite; no published behaviour changes.
-
-The two annotated `it.each` rows in `DashboardGridLayout.legacyRetired.test.tsx` introduced their literals with provenance comments naming a schema-catalog fixture. Both were true when written and were invalidated 66 minutes later by the catalog migration off the retired shape (objectui#4600). The annotations now state the measured history and say plainly that the rows no longer carry the independent-corpus property, and the relationship they assert is derived at test time from the fixture instead of restated in prose.
diff --git a/.changeset/7154-lookup-picker-keys-editor-seam.md b/.changeset/7154-lookup-picker-keys-editor-seam.md
deleted file mode 100644
index cf3cf14d9a..0000000000
--- a/.changeset/7154-lookup-picker-keys-editor-seam.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
----
-
-Measurement-only change in `@object-ui/plugin-grid`: objectui#7154 reported that
-`ObjectGrid`'s inline lookup picker never receives `multiple`, `allowCreate`,
-`lookupPageSize` or `dependsOn` because they are not on the relational copy set.
-Rendered against the grid's own inline editor, all four already take effect —
-the copy set is not the route.
-
-`applyRelationalMeta` writes onto the `fieldMeta` handed to ``,
-the read-only cell. The inline editor is a different seam: `renderCellEditor`
-looks the field up in the object schema and spreads the whole def into the
-widget, so every key a def carries reaches `LookupField` whether or not it is
-copied. Both halves read `objectSchema.fields[name]`, so copying could never
-rescue an editor the schema read did not already serve.
-
-- New `__tests__/lookupPickerKeys-7154.test.tsx` renders each key against a
- control column differing only in that key: `multiple` accumulates two picks
- ("2 selected") where the control replaces; `allowCreate: false` removes the
- quick-create entry the control offers; `lookupPageSize: 3` scopes the picker
- dialog to 3 rows against a control of 10; `dependsOn` arrives and gates.
-- The four verdicts stay `deferred` — flipping them would write members onto a
- bag whose consumer does not read them, the shape objectui#6711 and
- objectui#6874 retired — with the measurement now in their notes.
-- Corrected the docblocks and the three `applyRelationalMeta` call-site comments
- that claimed the inline picker reads this bag; that claim is what the card was
- filed against.
-
-No behaviour change.
diff --git a/.changeset/7155-converge-lookup-dialect-camelcase.md b/.changeset/7155-converge-lookup-dialect-camelcase.md
deleted file mode 100644
index da9add6337..0000000000
--- a/.changeset/7155-converge-lookup-dialect-camelcase.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/fields': minor
-'@object-ui/plugin-grid': minor
-'@object-ui/app-shell': minor
-'@object-ui/plugin-detail': minor
----
-
-Converge the lookup/user widget metadata on the spec's camelCase — one concept, one
-spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19).
-
-**BREAKING, deliberately, with no deprecation window.**
-
-Two published contracts declared OPPOSITE dialects for the same four lookup keys, and
-`@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object
-contract *refuses* outranked the one it *declares*:
-
-| | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) |
-|---|---|---|
-| camelCase | **declared** | compile error (`TS2561`) |
-| snake_case | refused (`unrecognized_keys`) | **declared** |
-
-`LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the
-snake members are **removed**:
-
-| before (removed) | after |
-|---|---|
-| `display_field` | `displayField` |
-| `description_field` | `descriptionField` |
-| `lookup_filters` | `lookupFilters` |
-| `id_field` | `idField` |
-
-**Migration.** Rename those four keys wherever you author lookup or user field metadata
-— `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema`
-that returns them. The old spellings are no longer read: a def still carrying
-`display_field` falls back to the referenced record's generic name heuristic rather than
-the field you named.
-
-`idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name
-hydration — committing a record field other than the id as the lookup's stored value —
-which is picker behaviour with no `FieldSchema` twin, and none owed.
-
-**Not renamed** (outside this ruling's four keys, still snake on the widget bag):
-`reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`,
-`allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's
-`normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def.
-
-Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose
-snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the
-inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s
-`relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict
-class; its gate is restated to assert the class no longer exists rather than passing
-vacuously.
diff --git a/.changeset/7163-detail-timestamp-i18n.md b/.changeset/7163-detail-timestamp-i18n.md
deleted file mode 100644
index de3a9fdf88..0000000000
--- a/.changeset/7163-detail-timestamp-i18n.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/plugin-detail': patch
-'@object-ui/i18n': patch
----
-
-`RecordComments` and `PointInTimeRestore` resolve their copy from the locale
-packs instead of hardcoded English (objectui#7163).
-
-Both files carried their own `formatTimestamp` whose relative-time branches
-returned English literals, so a zh/ja/ar session read `5m ago` next to
-otherwise translated chrome — the defect objectui#7142/#7149 fixed one file
-over in `ActivityTimeline`.
-
-- **`RecordComments`** was already wired to the packs (11 `t('detail.…')`
- references), so this is a pure lookup swap onto `detail.justNow` /
- `minutesAgo` / `hoursAgo` / `daysAgo` — keys already present in all ten packs,
- each `en` value byte-identical to the literal it replaces. **No new key, no
- copy change.**
-- **`PointInTimeRestore`** used no translation hook at all, so it is swept
- WHOLE rather than having only its timestamps converted: card title, empty
- state, field-count line, preview panel, snapshot heading, restore
- confirmation and all three buttons. Wiring one string into an otherwise
- untranslated component is what shipped objectui#7142's visibly half-done zh
- card; that is not repeated here.
-
-Ten new `detail.*` keys land in all ten packs and in
-`DETAIL_DEFAULT_TRANSLATIONS`: `revisionHistory`, `noRevisions`,
-`revisionFieldsChanged`, `revisionFieldsChangedOne`, `revisionPreview`,
-`revisionSnapshot`, `restoreConfirm`, `restoring`, `confirmRestore`,
-`restoreToPoint`. `detail.cancel`, `detail.activityEmptyValue` and
-`detail.emptyValue` are **reused**, not forked.
-
-The restore confirmation becomes one key with a `{{when}}` hole rather than a
-sentence assembled around a JSX expression, and the field-count line uses the
-repo's two-key plural convention selected by a static ternary over two literal
-keys — never `t(KEYS[n])`, which objectui#7149 measured as invisible to the
-i18n scanners.
-
-One deliberate copy change: the snapshot panel's null placeholder was an EN
-DASH (`–`, U+2013) written inline and now resolves `detail.emptyValue`, which
-is an EM DASH (`—`, U+2014) in all ten packs — the glyph the rest of the detail
-package already uses for an empty value.
diff --git a/.changeset/7164-gantt-malformed-row-refusal.md b/.changeset/7164-gantt-malformed-row-refusal.md
deleted file mode 100644
index d18db69792..0000000000
--- a/.changeset/7164-gantt-malformed-row-refusal.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/plugin-timeline': patch
-'@object-ui/types': minor
-'@object-ui/i18n': patch
----
-
-A gantt timeline whose rows are malformed now refuses to draw, naming the row,
-instead of crashing the render (objectui#7164, maintainer ruling A+).
-
-`TimelineRenderer`'s gantt branch used to read the authored rows twice — once
-defensively in `findUnusableGanttDate`, once bare in `calculateDateRange` — and
-every input in the gap threw a `TypeError` mid-render from ordinary JSON:
-`items: [null]`, a row whose `items` is `5` / `true` / `{}` / an array-like
-object, or `items` itself not an array. The three readers (the date scan, the
-range computation and the render loop) now consume ONE verdict from
-`classifyGanttRows`, and a malformed shape renders the existing `role="alert"`
-refusal through a new diagnostic key,
-`timeline.gantt.unusableRange.malformedRow` — "items[0] is null, which is not
-a row shape" — never the `malformedDate` copy, which named the wrong fault.
-The key lands in `en` and the nine sibling locale packs.
-
-`@object-ui/types` (minor — the accept set narrows): `TimelineSchema.items` no
-longer declares `z.array(z.any())`. Every element must be an object, and a
-gantt row's own `items`, when present, must be an array, so `validate` refuses
-`items: [null]` and `items: [{ items: 5 }]` at authoring time — before they
-reach a renderer. Feed items (`vertical` / `horizontal`) carry no `items` key
-and parse exactly as before; every in-repo `type: 'timeline'` fixture parses
-green on both sides of the change. Rows with no bars (`items: []`, a row
-without `items`) stay the ordinary empty state and still draw.
diff --git a/.changeset/7165-grid-dependent-values.md b/.changeset/7165-grid-dependent-values.md
deleted file mode 100644
index f08912ac56..0000000000
--- a/.changeset/7165-grid-dependent-values.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-Fix: a `dependsOn` lookup column is no longer permanently uneditable in an
-editable `ObjectGrid`.
-
-`LookupField` resolves the record it gates on as
-`dependentValues ?? ctx.formValues ?? ctx.data ?? {}`, and the grid's inline
-cell editor supplied **none** of the three — `renderCellEditor` rendered
-`FieldEditWidget` with `field` / `value` / `onChange` only, `SchemaRendererContext`
-has no `formValues`, and the grid sets no `ctx.data` for a row. The resolved
-record was therefore `{}` for every row, so a column declaring `dependsOn`
-rendered a disabled trigger reading "Select region first" **even when the row
-carried the parent value**. The field could never be filled and nothing said
-why.
-
-PR #2216 closed #2215 in two halves: the form renderer injects its live watched
-record as `dependentValues`, and every picker takes the `dependsOn` chain as a
-hard `baseFilter`. The second half is host-independent and was already live on
-the grid path — which is why the gate fired at all. The first half is per-host
-and the grid never got it. `renderCellEditor` now passes
-`dependentValues={ctx.row}`, supplying that missing input; no cascade is
-re-implemented.
-
-⚠️ Interim, and deliberately labelled as such in the code (#7165): `ctx.row` is
-the **saved** record, so a parent edited but not yet saved in the same row does
-not re-scope the child — it stays scoped by the persisted value. Matching the
-form's live-record semantics needs a new member on `renderCellEditor`'s
-published context type and is tracked as #7188.
diff --git a/.changeset/7166-retire-inert-fieldmeta-copies.md b/.changeset/7166-retire-inert-fieldmeta-copies.md
deleted file mode 100644
index ff52564903..0000000000
--- a/.changeset/7166-retire-inert-fieldmeta-copies.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-`ObjectGrid` no longer copies `descriptionField`, `lookupColumns` or `lookupFilters` onto a
-relational column's `fieldMeta` (objectui#7166). No behaviour change — all three still reach
-the inline lookup picker, by a different seam.
-
-`applyRelationalMeta` writes the copy set onto the `fieldMeta` that `generateColumns` hands
-to `` as the `field` prop — six JSX passes across the three column-building
-paths, and nowhere else. For a relational column that resolves to `LookupCellRenderer`,
-which reads exactly `reference_to`, `reference`, `display_field`, `displayField`,
-`reference_field` and `options`; a `user` column resolves to `UserCellRenderer`, which
-destructures `{ value }` and reads no field meta at all. Measured by receiver rather than by
-count: `packages/fields/src/index.tsx`, the file holding **every** cell renderer, contains
-**zero** occurrences of the three retired keys, against a control of 22 occurrences of the
-`display_field` / `displayField` / `reference_to` spellings the cell does read.
-
-Their only readers off a field meta are `LookupField` and `UserField` — the two **editor**
-widgets — and the grid's inline editor does not receive this bag. `renderCellEditor` looks
-the field up in the object schema and spreads the whole def into the widget
-(`{ name: ctx.column.accessorKey, ...fieldDef }`), so every key a def carries reaches
-`LookupField` whether or not it is copied. The copies were dead writes: the objectui#6711
-(`reference_to_field`) and objectui#6874 (`titleFormat`) class, arriving from the opposite
-direction — those keys had no *declaration*, these have no *reader on this path*.
-
-Two of the three, `descriptionField` and `lookupColumns`, were **added** by objectui#6875 as
-its fix, classified from a read-set derivation alone. Its third key, `displayField`, is
-genuinely delivered and stays copied — and it is the one that arrived with a rendering test.
-⭐ The generalisable lesson, now recorded in both docblocks: **a derivation establishes that
-a consumer READS a key; it does not establish that a given BAG is how the consumer gets it.**
-
-- New `__tests__/relationalMetaCopySet-7166.test.tsx` renders both directions. The **cell**:
- four lookup columns over one referenced record, differing only in the key under test, all
- resolve the same text, while the `displayField` control column resolves something else —
- the control is what makes the three zeros readings rather than a fixture that never reached
- the lookup path. The **editor**: each retired key still takes effect in the inline picker
- with the copy set no longer carrying it — `descriptionField` drives the secondary line,
- `lookupColumns` shapes the picker columns, `lookupFilters` scopes the candidates — each
- against a sibling control column that declares nothing.
-- The three snake_case `legacy-alias` spellings — `description_field`, `lookup_filters`,
- `id_field` — have the same reader-side verdict and are **deliberately kept**. They are
- recorded as legacy aliases precisely because a host `DataSource` outside these repos may
- hand-feed them; that is a producer-side argument, untouched by this reader-side
- measurement. Their verdict is now recorded on the table with the open producer question
- stated, so the next pass inherits a measurement instead of a silence.
-- ⛔ The derivation gate cannot enforce this retirement, and now says so. Its read set is a
- union over three consumers, two of which are not fed this bag, so all three retired keys
- remain in it and every derived assertion passes whichever verdict they carry. Their absence
- is pinned by an explicit hand-written assertion plus the rendering test. Re-scoping the
- derivation around the cell alone is a design change to objectui#6875's mechanism and is
- filed, not made here.
-- The gate is strengthened, not weakened: every `deferred` verdict is now mechanically proved
- spec-declared against the installed `FieldSchema`, and the sibling pins
- (`relationalMetaCopySet-6711` / `-6874`) turn the retired `lookupFilters` into a live
- negative assertion on a fixture that still declares it.
diff --git a/.changeset/7173-ai-pending-actions-inbox-i18n.md b/.changeset/7173-ai-pending-actions-inbox-i18n.md
deleted file mode 100644
index 46b0e0f9a6..0000000000
--- a/.changeset/7173-ai-pending-actions-inbox-i18n.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
-'@object-ui/i18n': patch
----
-
-`AiPendingActionsInbox` speaks the session locale — every string in it, not only its timestamps (objectui#7173).
-
-The AI HITL approval inbox held its own relative-time helper returning hardcoded
-English (`'just now'`, `` `${min}m ago` ``), so a zh / ja / ar session read English
-relative times on every row. It is the fifth spelling of that helper in the repo,
-and the file had **no translation wiring at all** — the unwired-component shape,
-not the lookup-swap shape.
-
-It is therefore swept whole. objectui#7142 wired one string into an otherwise
-untranslated component and shipped something visibly half-done, and objectui#7149
-is what finishing that afterwards cost; the triage ruling on this card (2026-09-01)
-carried that forward as *sweep the file whole or leave it*. Everything the user can
-read now resolves from the locale packs: the card heading and description, the three
-tabs, the refresh button, all five status badges, the six column headings, the empty
-state, the row and drawer buttons, all nine drawer field labels, the outcome banner
-and the whole reject-reason dialog.
-
-**No new rows for the four relative-time branches.** `detail.justNow`,
-`detail.minutesAgo`, `detail.hoursAgo` and `detail.daysAgo` already existed,
-translated, in all ten packs, and cross-package key borrowing is this repo's settled
-convention rather than an open question — `ObjectGrid`, `ObjectKanban`, `ObjectTree`,
-`ListView`, `ObjectView`, `NavigationOverlay`, `RecordAttachmentsPanel`,
-`RecordDetailView` and `apps/console` all resolve `detail.*` from outside
-`plugin-detail`. One phrase on one kind of control should not get a second
-translation that can drift from the first.
-
-The rest of the sweep needed copy no pack had, so `@object-ui/i18n` gains an
-`aiApprovals` namespace: 38 keys, translated in all ten packs. It is deliberately
-separate from `approvalsInbox`, which is the human approval-**process** inbox — a
-different surface and a different feature, so no rows are shared with it. Four
-generic verbs are reused rather than forked (`common.refresh`, `common.cancel`,
-`common.loading`, `common.ok`).
-
-**⛔ The five relative-time helpers are not unified.** They differ in real behaviour
-— `Math.round` here against `Math.floor` in `plugin-detail`, thresholds 45s/30d
-against 60s/7d, different tails — so normalising them is a behaviour change wearing
-a refactor's clothes and needs its own card. This inbox's arithmetic is untouched,
-and three rows in the new suite exist only to pin it: 50s renders `1m ago` (a 60s
-threshold would still say "just now"), 90s renders `2m ago` (`Math.floor` gives
-`1m ago`), and 20d renders `20d ago` (a 7d threshold would already show a date).
-
-Two assembled English sentences became single interpolated keys — the outcome banner
-(`Approve for {{id}}: {{message}}`) and the drawer subtitle
-(`Tool {{tool}} on {{object}}`). Their word order differs per locale, which fragments
-around a `` element cannot express, so the two identifiers lose their monospace
-styling. That is the deliberate cost of making those sentences translatable.
-
-Evidence: an `en`-only assertion cannot discriminate here, because each key's `en`
-value is byte-identical to the literal it replaced. The suite asserts in **zh and
-ar**, and the provider-less path separately, in its own file (`createI18n` installs
-itself as react-i18next's module-level global, so a provider-less render in a file
-that has already mounted a provider silently reads that pack instead of the defaults
-map). No inline `defaultValue` anywhere (objectui#3517).
-
-Two consequences of the sweep, both landed here rather than left for CI to find:
-
-`packages/app-shell/src/console/ai/__tests__/ConversationsSidebar.test.tsx` froze its
-`vi.mock('@object-ui/i18n', ...)` factory to a hand-written object. Its import graph
-reaches `plugin-chatbot`, which now resolves `createSafeTranslation` at module scope, so
-the frozen surface made that read `undefined` and the file died during COLLECTION — the
-objectui#6849 shape, which does not look like a test failure. It now spreads
-`importOriginal()` and overrides only `useObjectTranslation`. Measured, not guessed: of
-the 41 frozen `@object-ui/i18n` factories in the repo, running every one of them showed
-this to be the only file whose graph reaches the package.
-
-The ten pack blocks are locale DATA, and locale data lands in the console's eager
-`framework` chunk, so `scripts/check-eager-closure-budget.mjs` raises that chunk's
-ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh
-measurement (502,405 to 514,863). Attributed by three console builds: the merge parent
-reads 510,192, this branch with the ten `aiApprovals` blocks cut reads 510,192 again, and
-this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing
-else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression
-the gate must catch) rather than widened; most of the overage was pre-existing drift, with
-the merge parent already at 510,192 of the 512,000 allowed.
diff --git a/.changeset/7175-downstream-consumer-census.md b/.changeset/7175-downstream-consumer-census.md
deleted file mode 100644
index 329ae19621..0000000000
--- a/.changeset/7175-downstream-consumer-census.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
----
-
-Docs-only: records the downstream-consumer census for
-`@object-ui/plugin-detail`'s `PointInTimeRestore` as
-`docs/audits/2026-09-plugin-detail-downstream-consumer-census.md` (objectui#7175).
-
-objectui#7163 measured that the component has zero *in-repo* consumers. That is not the
-same statement as zero consumers, because it is barrel-exported public API, so this audit
-reads the downstream populations instead: `hotcrm` measured at `a6be39a3d` (zero on every
-spelling, against a positive control of 104 `plugin-detail` node references in the same
-repo on the same instrument), and `cloud` reported as **NOT MEASURED** — refused on three
-independent channels, two of them beside a live control.
-
-No source, behaviour, or public-surface change; the audit records evidence so the
-ADR-0049 enforce-or-remove question can be decided later. It deliberately retires nothing.
diff --git a/.changeset/7177-studio-switcher-server-writable-verdict.md b/.changeset/7177-studio-switcher-server-writable-verdict.md
deleted file mode 100644
index 61bc87a692..0000000000
--- a/.changeset/7177-studio-switcher-server-writable-verdict.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio's package switcher reads the server's `writable` verdict instead of guessing
-from `manifest.scope` (objectui#7177, ADR-0130 Consequences row 6, server half in
-objectstack#14375).
-
-`GET /api/v1/packages` now stamps every row with `writable: boolean`, computed by
-`isWritablePackage` (ADR-0070 D2) — the same predicate the server's authoring and
-lifecycle gates enforce. `parsePackages` uses it when the row carries one, so the
-lock badge and the gate cannot disagree.
-
-The old `scope !== 'project'` expression stays as the fallback for servers that
-predate the field, and its output is pinned byte-identical. It is wrong for exactly
-one row, which is why the verdict had to move server-side: a `type: module`
-sub-package of a multi-package artifact (ADR-0130 D4) is served with no `scope` key
-at all — the schema default is applied at parse time, while the artifact load path
-hands the raw manifest body to `registerApp`. The heuristic reads that as a writable
-database base, while the server refuses every write to it. Nothing in the raw row
-separates it from a scope-less Studio-created base, which really is writable — only
-the server's `engine.manifests` does, so a client-side "missing scope means
-read-only" rule would have flipped every Studio base read-only instead.
-
-Kernel packages (`scope: system` / `cloud`) stay hidden whatever verdict they carry:
-that filter is about visibility, not writability.
diff --git a/.changeset/7178-format-measure-date.md b/.changeset/7178-format-measure-date.md
deleted file mode 100644
index e5426cc40a..0000000000
--- a/.changeset/7178-format-measure-date.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/fields': patch
----
-
-Render a dataset measure over a date field as a date (objectui#7178, maintainer
-ruling 2026-09-02, director summon #8 — option A).
-
-`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
-placed **before** `format` was ever read. So a `min` / `max` measure over a date
-or datetime field printed its stored value verbatim — a 24-character ISO string
-in the KPI tile's `text-2xl font-semibold`, wrapping to two lines — and the
-`format` that `DatasetMeasureSchema` accepts was unreachable for those values.
-A date-shaped value now routes to the date display path before that
-short-circuit, so all four dataset-bound surfaces are served at once: the metric
-tile, chart values, dataset table cells, and the metadata-admin dataset preview.
-
-`min` / `max` over a date stays a legal measure; nothing in `@objectstack/spec`
-narrows. `PivotTable` takes a `number` outright and is unchanged.
-
-**No second date formatter was written.** `formatDate`, `formatDateTime`,
-`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
-barrel down into `@object-ui/core` (`utils/date-display.ts`), which is the same
-remedy objectui#4576 applied to `formatDisplayNumber` and for the same reason:
-`core` is the React-free engine and could not import from a React package, so
-the alternative was a parallel date convention in `dataset-format.ts` — exactly
-the drift that once had a list cell rendering `1.234,5 %` beside a dashboard
-measure's `1.234,5%`. `@object-ui/fields` re-exports all four names unchanged,
-so no consumer's import path or behaviour changes, and a reference-identity test
-pins that the cell renderer and the measure formatter call the same function.
-
-**What `format` can say for a date measure, measured rather than assumed.** The
-shared date path takes a named STYLE, not a date pattern: `'short'` and
-`'relative'` are honoured — the same words `DateCellRenderer` honours from
-`field.format` — while a pattern such as `'YYYY-MM-DD'` renders the locale
-default. That limit is unchanged by this release (`plugin-dashboard`'s
-`recordFields` already routed a date-shaped `format` into the same style slot)
-and is now pinned by a test instead of being silent.
-
-**Numeric measures are byte-identical.** 33,696 argument forms
-(value × format × currency × percentScale × locale) were compared against a
-verbatim copy of the pre-fix function: the only values that moved were the four
-ISO-shaped, parseable ones. Numbers, numeric strings (`'1751612400000'`,
-`'2026'`), the nullish em dash, arbitrary prose and non-strings all render
-exactly as before.
diff --git a/.changeset/7179-grid-grouping-projection.md b/.changeset/7179-grid-grouping-projection.md
deleted file mode 100644
index b59fb5a296..0000000000
--- a/.changeset/7179-grid-grouping-projection.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/plugin-grid': patch
-'@object-ui/plugin-list': patch
----
-
-Fix: a grid grouped by a field it does not also show as a column no longer collapses
-every row into one `(empty)` group (objectui#7179).
-
-`$select` was built from the view's `columns` and nothing else, so a view declaring
-`grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns
-never asked the server for that field. It was `undefined` on every row by the time
-grouping ran, and the grouping label builder — correctly, for a genuinely empty value —
-answered `(empty)` for all of them. The result was one collapsible group holding every
-record, with no error, no warning and no empty state: a grid that looked like it grouped
-and did not, reading as "these records have no value for this field".
-
-The grouping fields are now unioned into the projection, at both places it is built —
-`ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the
-rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that
-fetches a bare foreign key without populating it buckets by raw id instead of by name,
-which is a different wrong answer rather than a fix.
-
-Authors do not need to mirror a grouping field in `columns` any more. That was never
-required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of
-it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their
-`groupByField` with no column needed. Refusing the configuration at author time was
-considered and rejected: it would make the grid the odd one out and reject working
-intent that the schema explicitly allows.
-
-The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]`
-entry carries a bare string that has never been through column validation, and some
-backends answer an unknown `$select` key with an empty result set rather than ignoring
-it. Unioned unguarded, a grouping field naming something the object does not declare
-would have turned this bug into a strictly worse one — no rows at all, equally silently.
-Grouping fields are therefore intersected with the object's declared fields and passed
-through the same field-level-security gate as columns and predicate operands before they
-reach the query.
diff --git a/.changeset/7182-declared-action-ids-one-rule.md b/.changeset/7182-declared-action-ids-one-rule.md
deleted file mode 100644
index 4ccde267f2..0000000000
--- a/.changeset/7182-declared-action-ids-one-rule.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': minor
-'@object-ui/plugin-detail': minor
----
-
-Mixed id/object action arrays are refused; use all ids or all objects
-(objectui#7182, maintainer ruling 2026-09-02, option C).
-
-An `actions` array on `page:header` or `record:quick_actions` (and the bar's
-spec-declared `actionNames`) is either **all action ids** or **all inline
-`ActionDef` objects**. A mixed `['convert', { … }]` array is now refused on both
-surfaces: none of its authored actions is rendered, and the console names the
-offending index (`… refused at index 1 — element 1 is an inline action object
-but element 0 is an action id …`). Before this change the two renderers
-disagreed on exactly that input — `page:header` normalised per element and drew
-both halves, `record:quick_actions` switched on the whole array and rendered
-nothing for the id — so one authored array meant two things depending on which
-surface drew it, and the mixed form is precisely what a half-migrated page under
-the objectstack#11592 ids ruling produces.
-
-**Breaking, deliberately, and narrowing.** `@objectstack/spec` has always
-declared `PageHeaderProps.actions` as `z.array(z.string())` — the spec already
-refuses an object element at validation, naming its index — and
-`RecordQuickActionsProps` declares `actionNames` (ids) only. What narrows is
-the renderers' undeclared tolerance: an all-object array still passes through
-(transition tolerance for the migration, retired on its own card once the last
-inline array is converted), a mixed one no longer does. **Migration:** convert
-each array whole — every element an id naming an action declared on the
-object — never one element at a time.
-
-New on `@object-ui/types`, beside `actionRendersAt`: the pure
-`resolveDeclaredActionIds(elements, registeredActions)`, with the
-`DeclaredActionsResolution` / `DeclaredActionsRefusal` result types (the shape
-classifier stays module-internal: called with no registry, the function already
-returns the registry-independent verdict a renderer needs before its lookup). Both
-renderers call it; the whole-array switch in `record-quick-actions.tsx` and the
-per-element normalisation in `containers.tsx` are gone. The rule is closed: a
-string is an id, a non-null non-array object is an inline definition, and any
-other element (`null`, a number, a nested array) is refused at its index too.
-An all-id array resolves by `name` in authored order, first registration
-winning on a duplicate name; ids that name nothing are reported back with their
-index for the caller to warn about once its lookup has settled.
-
-**Three further behaviour changes ride on the one rule, all on published
-packages:** on `page:header`, a padded id (`' convert '`) no longer resolves — it
-was previously trimmed before the lookup, and ids are now compared exactly as
-authored; on `page:header`, a blank `''` id is now reported by the unresolved-id
-warning instead of being silently skipped; on `record:quick_actions`, an all-id
-`actions` array with no object bound now renders nothing (the ordinary empty
-placeholder) instead of handing the bare strings to the action engine as action
-definitions.
diff --git a/.changeset/7183-dist-vitest-project.md b/.changeset/7183-dist-vitest-project.md
deleted file mode 100644
index 4d10ef7fd9..0000000000
--- a/.changeset/7183-dist-vitest-project.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
----
-
-Test and CI infrastructure only — nothing this change touches is published, so no package
-releases from it.
-
-Adds a fourth vitest project, `dist`, holding built-artifact pins: tests that import their
-package's BUILT bundle instead of its `src`. The root config aliases every workspace package
-to `src`, which is correct for the ~2000 tests that want fast source feedback and leaves
-"does the shipped bundle still do X" structurally unanswerable. Until now such a test could
-not be committed at all — turbo's `test` task is `dependsOn: ["^build"]`, the DEPENDENCIES'
-builds and not the package's own, so a `dist`-importing test landed in CI with no `dist` to
-import. That reads as NOT MEASURED rather than as a red pin, and the usual repair (delete it,
-or let it skip when `dist` is missing) leaves a green suite that measures nothing.
-
-The one file the changeset-presence gate flags,
-`packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx`, is under `src/` but
-ships nowhere: the package publishes `files: ["dist", …]`, and its `tsconfig.json` build
-excludes `src/__tests__` outright. The rest of the change is `turbo.json`, `vitest.config.mts`,
-the root and `@object-ui/components` `package.json` scripts, and one CI step — no runtime
-source, no published contract, no behaviour change for any consumer.
diff --git a/.changeset/7187-relational-copy-set-cell-scoped.md b/.changeset/7187-relational-copy-set-cell-scoped.md
deleted file mode 100644
index 12fb10de22..0000000000
--- a/.changeset/7187-relational-copy-set-cell-scoped.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
----
-
-Scope `ObjectGrid`'s relational copy-set derivation to the consumer the copied bag actually
-reaches (objectui#7187). No published behaviour changes: the copy set is byte-identical
-before and after — the same seven keys, in the same order.
-
-The gate in `packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts`
-re-extracted a read set from three consumers and collapsed it into a UNION. Only one of the
-three, `LookupCellRenderer`, is handed the bag `applyRelationalMeta` writes; the two inline
-editor widgets are fed `{ name, ...fieldDef }` straight off the object schema. So membership
-in that union meant "some consumer reads this key" and never "this bag is how that consumer
-gets it" — and a copy-set entry asserts the second. The union was read as the first claim's
-evidence for the second twice, and both verdicts shipped: objectui#6875 put `descriptionField`
-and `lookupColumns` on the copy set on it, and objectui#7166 had to take them back off.
-
-Three changes make that mistake unavailable:
-
-- The reader axis is now recorded PER CONSUMER on every entry (`readers`), and the gate checks
- each declared list against that consumer's own source in both directions — a hand-widened
- declaration is an orphan, a new spelling in a chain is unclassified, and both are red.
-- The copy set is derived from `CONSUMERS_FED_THIS_BAG` — the cell alone. The one deliberate
- exception, the three snake_case spellings kept on an unanswered producer-side question, has
- to name itself per key (`copiedWithoutCellReader`), and the gate confines that exit to keys
- `FieldSchema` does not declare, so no authorable key can take it.
-- The `deferred` verdict is gone. It meant "read only by an editor widget", which is now
- measured rather than declared; its seven keys are `spec` (which is what they are) and stay
- off the copy set because no consumer fed the bag reads them.
-
-Consequence worth stating: the hand-written non-membership pin that had been holding
-objectui#7166's retirement on its own is now redundant as a guard — re-adding any of the three
-retired keys turns the gate red on a derived assertion under every available verdict. The pin
-is kept so a regression is reported by name, not because it is the only hold.
diff --git a/.changeset/7188-components-data-table-pending-row.md b/.changeset/7188-components-data-table-pending-row.md
deleted file mode 100644
index 51b42be54d..0000000000
--- a/.changeset/7188-components-data-table-pending-row.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@object-ui/components': minor
----
-
-feat(data-table): pass `pendingRow` to the host cell editor — the row with its staged edits merged over it (#7188)
-
-`data-table` already computed each row's `pendingChanges` entry in the row loop that
-renders the editor; it now hands the injected editor that row merged with those staged
-values as `pendingRow`, next to the persisted `row`. Nothing about `row`, `value`,
-`stage`, `commit` or `cancel` changed. A host editor that scopes itself by a sibling
-field can read `pendingRow` and follow an edit the user has made but not yet saved.
diff --git a/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md b/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md
deleted file mode 100644
index 6390e33e1b..0000000000
--- a/.changeset/7188-plugin-grid-staged-parent-rescopes-child.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-Fix: a `dependsOn` lookup edited inline follows a parent edited in the same row **before**
-it is saved (#7188, finishing #7165).
-
-#7165 shipped an explicitly-labelled interim: the grid's inline editor scoped a
-`dependsOn` picker by `ctx.row`, the **saved** record. Edit the parent cell, do not save,
-open the child — and the child still listed candidates for the parent's persisted value
-(or stayed gated if that value was empty). The grid now scopes by `ctx.pendingRow ?? ctx.row`,
-where `pendingRow` is the row merged with its staged, unsaved edits, so picking a parent
-re-scopes the child immediately — the form's live-record semantics from #2216. The interim
-marker that named #7188 is gone with the interim.
diff --git a/.changeset/7188-types-render-cell-editor-pending-row.md b/.changeset/7188-types-render-cell-editor-pending-row.md
deleted file mode 100644
index af3a4be0b8..0000000000
--- a/.changeset/7188-types-render-cell-editor-pending-row.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/types': minor
----
-
-feat(types): `renderCellEditor`'s context gains `pendingRow` — the persisted row merged with its staged, unsaved edits (#7188)
-
-The inline cell editor's context on `DataTableSchema` (declared by #6882) carried the row
-once, as `row` — the **persisted** record. A widget that scopes itself by a sibling field
-(a `dependsOn` lookup) had no way to see a parent that was edited in the same row but not
-yet saved, so it kept listing candidates for the old parent.
-
-The context now carries the row twice, deliberately:
-
-- `row` — unchanged: the persisted record, what the data source last returned.
-- `pendingRow` — **new**: `row` shallow-merged with the row's staged, unsaved edits. The
- same object as `row` when nothing is staged.
-
-`row` was **not** redefined to mean the merged record — that would silently change an
-already-published member, and a host that needs the persisted value would have lost its
-only source. Both are addressable. The zod mirror's `renderCellEditor` is `z.function()`
-and encodes no parameter shape; its description records the delta and names the member
-on `DataTableSchema` as the authority. The #6882 exact-shape pin was extended (not
-weakened) in the same change, with a control proving the pin can tell the seventh member's
-presence from its absence.
diff --git a/.changeset/7189-grouped-grid-partial-disclosure.md b/.changeset/7189-grouped-grid-partial-disclosure.md
deleted file mode 100644
index 16e6a9461b..0000000000
--- a/.changeset/7189-grouped-grid-partial-disclosure.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-'@object-ui/plugin-grid': minor
-'@object-ui/i18n': minor
----
-
-A grouped grid now says, where the group counts are, that it grouped a **page**
-(objectui#7189).
-
-`useGroupedData` buckets the rows the browser already holds and computes every
-per-group aggregate from that same array, so both the set of groups and every
-number in a group header are properties of the fetched page, not of the query.
-That is a correct implementation of client-side grouping and is **unchanged**
-here — what was missing is any statement that client-side grouping is what you
-are looking at. Measured on a 186-record store distributed 86 / 61 / 31 / 7 / 1
-across five business units with a 100-row page: with contiguous rows the grid
-rendered **two** group headers (`86`, `14`) and three units were absent from
-the screen entirely; with interleaved rows all five resolved but every count
-was a page slice (31 / 31 / 30 / 1 / 7). Nothing on screen said either.
-
-The paging footer is not a statement about what was grouped, and it
-demonstrably did not prevent the wrong reading — a wrong number invites a
-second look, an absent row invites none. So the disclosure goes where the
-authoritative-looking number is:
-
-- a short `Partial` marker beside **every** group count, at every nesting
- depth, carrying the full sentence as its `title` and its accessible name;
-- one line directly above the group list, inside the grouped region rather
- than in the footer: *"Grouped over the first 100 of 186 records. Group counts
- are page-scoped, and a group whose records all fall beyond the loaded rows is
- missing here."*
-
-The trigger is the strongest thing the component can actually know, and the
-wording never outruns it. With a real match total to compare against
-(`resolvedTotalMatching` — the one derived value the pager and both bulk-bar
-sites already read, reached either from the grid's own fetch or from a host's
-`rowCount`) it states the fact with both numbers. With no total but a window
-that came back full it may only say *"more may match"* — the same inference
-`plugin-list`'s own footer draws when no total is known. Rows handed in inline
-are not a page and are never marked, and **a grouped grid whose result set fits
-in one page shows nothing at all**: the marker is conditional, which is what
-makes it worth reading.
-
-Server-side grouping — the durable fix — is deliberately NOT part of this. It
-is an API-surface decision still open on objectui#5560, and nothing here builds
-toward it or changes the fetch.
-
-`@object-ui/i18n` carries the three new `grid.grouping.*` strings across all
-ten locale packs; `GroupRow` gains two optional props (`partialLabel`,
-`partialTitle`) and is otherwise unchanged. No metadata schema key was added:
-the condition is derived from data the grid already has.
diff --git a/.changeset/7190-detail-dependson-lookup-probe.md b/.changeset/7190-detail-dependson-lookup-probe.md
deleted file mode 100644
index a7995766f5..0000000000
--- a/.changeset/7190-detail-dependson-lookup-probe.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
----
-
-Measurement-only change in `@object-ui/app-shell`: objectui#7190 asked whether a
-`dependsOn` lookup gates on the DETAIL page, and deliberately left it unanswered
-— a detail page renders ONE record, which is exactly the "record scope"
-`LookupField`'s `ctx.data` channel exists to carry, so a host that populates it
-would make the cascade resolve and there would be no defect. Measured in the
-real host, it gates.
-
-New `views/RecordDetailView.lookupDependsOn-7190.test.tsx` mounts the app-shell
-record page, loads a record carrying the parent value, enters inline edit by
-double-click, and reads the picker's own trigger. The declared lookup comes back
-`lookup-trigger-gated`, disabled, "Select region first" — while the `region`
-field it names is on screen in the same edit session carrying `emea`. The
-control lookup beside it (same reference, same record, no `dependsOn`) is
-asserted enabled, so the gated reading is a measurement and not a broken
-fixture. Both of `InlineFieldInput`'s call sites are covered — the details body
-and the highlights strip — and both gate.
-
-Pinned as the current behaviour, not fixed: which record the host should feed
-(the saved record, or the inline session's in-flight staged edits) is a design
-question this measurement does not answer, and the sibling fix for the grid is
-still in flight.
-
-No behaviour change.
diff --git a/.changeset/7194-scatter-one-measure.md b/.changeset/7194-scatter-one-measure.md
deleted file mode 100644
index 4f3725cf8b..0000000000
--- a/.changeset/7194-scatter-one-measure.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-charts': patch
-'@object-ui/i18n': patch
----
-
-A scatter handed more than one series now refuses instead of drawing a false picture.
-
-Scatter binds one measure: `series[0].dataKey` is the y axis, and every series was
-handed the same rows through that one axis. A second series therefore added a
-colour and a legend entry and nothing else — measured, two series over two rows
-painted four symbols at two positions, each drawn twice, and the second measure's
-values appeared nowhere on the plot. The data was valid and the picture was
-confidently wrong, which no existing refusal could see.
-
-A `chartType: 'scatter'` with two or more `series` now renders the renderer's
-refusal shell under `data-chart-error="scatter-multi-series"`, stating that a
-scatter plots one measure, naming the fix (keep exactly one series) and listing
-the series keys it was handed. A single-series scatter is unchanged.
-
-This refusal counts authored `series` only. `compareTo` on scatter is out of
-its scope: objectui#7402 ruled (b) that scatter joins pie / donut / funnel in
-excluding `compareTo` — `supportsCompareTo` and the dashboard widget path stop
-synthesising a comparison series for it, so no `…__comparison` overlay is ever
-built for a scatter and this guard is never reached by a compare-to document.
-That exclusion ships as a separate change; until it lands, a `compareTo`
-document still reaches the renderer as two series and refuses here today.
-
-No multi-measure projection is built (maintainer ruling, 2026-09-02): nothing
-in-repo authors a two-series scatter, so that capability waits for a real caller.
-The refusal copy is `chart.scatterOneMeasure` in all ten locale packs.
diff --git a/.changeset/7196-plugin-grid-held-key-census.md b/.changeset/7196-plugin-grid-held-key-census.md
deleted file mode 100644
index 9cb04a82ba..0000000000
--- a/.changeset/7196-plugin-grid-held-key-census.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
----
-
-Re-derives the held-key censuses in `packages/plugin-grid/src/ObjectGrid.tsx` and corrects
-the seven claims that no longer match the tree. Comments and one test docblock only — no
-runtime code, no exported type, no declared member moves, so nothing publishes.
-
-The card (objectui#7196) was filed on ONE measured entry: the schema-level census still
-listed `renderCellEditor` as an undeclared-but-live HELD key and called the `packages/types`
-ruling on it "pending", when objectui#6882 had declared it on 2026-08-30. The card
-deliberately did not claim the other entries were correct, only that nobody had checked, so
-all sixteen claims across both censuses in the file were re-derived against `origin/main`
-rather than read. Seven were defective:
-
-Stale — correct when written, drifted since:
-
-- `renderCellEditor` — declared by objectui#6882 on three surfaces (the member, the Zod
- mirror, an `Equal` exact-shape pin). The `(schema as any)` cast the census cites went
- with the declaration.
-- `cellClassName` — declared by the SAME ruling. The card did not name this one; the
- re-derivation did. The census called it a hold with a "pending ruling" too.
-- "leaves exactly TWO undeclared keys" — diffing the 46 flat-literal keys plus the 8
- group-literal keys against `DataTableSchema`'s declared members now leaves ZERO.
-- the consumer read set listed fourteen `col.` reads in `data-table.tsx` including
- `name`; objectui#6963 retired that alias on 2026-08-31, so it is thirteen.
-
-Wrong when written, not drift:
-
-- the schema-level `cellClassName` was described as folded "into every body cell's
- `className`". It reaches exactly three UTILITY cells (selection, row-number,
- row-actions) and never a data cell, which folds the per-column twin. The failure mode
- the census names is wrong in the same way. objectui#6882's declaration carries the
- correct version upstream; this makes the local copy agree with it.
-- "the 7-literal union `TableColumn` declares" — it has been eight since objectui#6370
- (2026-08-25), a day before the docblock was written; that commit's own subject says
- "8-literal".
-- the downstream read list omitted four keys the chrome passes read to re-express
- (`className`, `cellClassName`, `sortable`, `cell`). All three passes predate the list.
-
-Nine claims re-derived clean and are recorded as such, including every column-level
-verdict (`headerIcon`, `pinned`, `wrap`, `options`, `essential`, `name`) and the
-`FieldType` count of 49.
-
-The holds type is left in place, with its docblock rewritten to record that it is now
-redundant rather than load-bearing and what was measured toward removing it. Deleting a
-member of an exported type is a different kind of change and gets its own card, the way
-objectui#6615 was followed by objectui#6424 for `headerIcon`.
diff --git a/.changeset/7199-listview-description-relay.md b/.changeset/7199-listview-description-relay.md
deleted file mode 100644
index 2b080749cc..0000000000
--- a/.changeset/7199-listview-description-relay.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/plugin-list': patch
----
-
-fix(app-shell,plugin-list): a list view's own `description` now reaches the screen
-
-A `description` authored on a per-list-view entry (`listViews..description`)
-was validated, built and served correctly, then silently never rendered. Two
-independent cuts, both fixed here:
-
-- **app-shell** — `ObjectView`'s `renderListView` relay copied ~46 keys off the
- active view onto the schema it hands `ListView` (`label`, `sort`, `filter`,
- `hiddenFields`, `inlineEdit`, `color`, `allowExport`, …) but had no rung for
- `description`, so the renderer could only ever see the object-level list's
- description and a per-view one was unreachable. It is relayed now, with the
- same two-rung shape as `label`. This is *not* the object's own
- `objectDef.description`, which stays the page header's subtitle.
-- **plugin-list** — `ListView` rendered `typeof description === 'string' ? … : ''`,
- a type test rather than a resolution. `ListViewSchema.description` is
- `I18nLabel`, so an inline locale map (`{ en, 'zh-CN' }`) — metadata the spec
- entitles an author to write — rendered a blank strip in every locale. It now
- resolves through the same shared helper the sibling `label` uses, and the
- visibility guard reads the resolved text, so a map with no usable entry drops
- the strip instead of reserving empty space for it.
-
-`appearance.showDescription: false` still suppresses the description in both arms.
diff --git a/.changeset/7200-object-form-section-style-keys-undeclared.md b/.changeset/7200-object-form-section-style-keys-undeclared.md
deleted file mode 100644
index 5f3603b124..0000000000
--- a/.changeset/7200-object-form-section-style-keys-undeclared.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ObjectFormSection` no longer declares `className` / `gridClassName` (objectui#7200 —
-the declared-but-inert remainder of objectstack#13626).
-
-**Breaking, deliberately.** A TypeScript literal annotated `ObjectFormSection` (or an
-`ObjectFormSchema.sections` entry) that carries `className` or `gridClassName` is now a
-compile error at the authoring site. Before this change the two members were declared
-with doc comments promising a wrapper / grid class, while — since objectstack#13626
-retired the seven renderer reads (`@object-ui/plugin-form` 2026-09-01) — nothing
-delivered it: an author could write either key, have it type-check, and get nothing.
-
-The authored-metadata type now agrees with `@objectstack/spec`, whose `FormSectionSchema`
-is a strict object declaring neither key, and with the ruling's rationale (maintainer
-2026-09-01, verbatim): "retire the reads … Declaring the keys was weighed and not adopted:
-it would formally invite free Tailwind strings into authored metadata, the exact class
-the boundary exists to keep out." A `?: never` tombstone was not used: `ObjectFormSection`
-has no zod mirror (`ObjectFormSchema` in `zod/objectql.zod.ts` does not declare
-`sections`), so there is no parse door to refuse at, and a tombstone is still a
-declaration in completion and in the published `.d.ts`.
-
-**Not changed.** The five per-layout section config types in `@object-ui/plugin-form`
-(`ModalFormSectionConfig`, `SplitFormSectionConfig`, TabbedForm's `FormSectionConfig`,
-`WizardStepConfig`, `DrawerFormSectionConfig`) keep their `className` / `gridClassName`:
-their renderers read them for programmatic React mounts, which the authorable boundary
-does not govern. The form ROOT `className` (`ObjectFormSchema.className`) is a different
-key on a different node and is unaffected. Runtime behaviour is unchanged — JSON metadata
-carrying either key was already ignored.
-
-**Migration.** Remove the two keys from any `ObjectFormSection` literal; they did nothing.
-Style sections through the host application's own CSS or the form ROOT `className`.
-Section *layout* stays authorable through `columns`.
diff --git a/.changeset/7201-schema-slot-census-gate.md b/.changeset/7201-schema-slot-census-gate.md
deleted file mode 100644
index 947874b2a4..0000000000
--- a/.changeset/7201-schema-slot-census-gate.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
----
-
-Gate the `plugin-grid` schema-level held-key census on its ENTRY CONDITION
-(objectui#7201). The census's only pin asserted that the seam ACCEPTS
-`renderCellEditor` and `cellClassName` — equally true while the keys are held
-locally and once they arrive through `DataTableSchema` — so it stayed green
-across objectui#6882, the ruling that expired both holds. The suite now also
-carries the column-level twin's shape (`columnHoldsExpiry-6424.test.ts`):
-a compile-time verdict per member of `ObjectGridDataTableSchemaHolds` recording
-whether `DataTableSchema` declares that key, plus a pin on the member set so the
-gate is per HOLD rather than per key somebody remembered.
-
-No behaviour change and no published API change: compile-time assertions in
-`packages/plugin-grid/src/__tests__/`, plus the one census paragraph in
-`ObjectGrid.tsx` that this change falsifies — it said nothing above it was
-mechanically checked.
diff --git a/.changeset/7203-gantt-toolbar-period-label.md b/.changeset/7203-gantt-toolbar-period-label.md
deleted file mode 100644
index 1d6447a4f2..0000000000
--- a/.changeset/7203-gantt-toolbar-period-label.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/plugin-gantt': patch
----
-
-Gantt toolbar: the period label names the visible window, and the prev/next
-buttons step it (objectui#7203).
-
-The label formatted `timelineRange.start` — the memo spanning the whole dataset
-— so it named the first unit of the entire result set and could not change while
-the chart was scrolled, because it was not derived from scroll position at all.
-On a dataset running January to December it therefore read "January 2026" at
-every scroll position, four pixels above a band header correctly reading
-"Aug 2026". Measured on the demo fixture in Chromium at 1440x900: on first paint,
-after the chart auto-scrolls to Today, the label read `December 2025` over
-columns `28F 29S 30S 31M 1T 2W 3T` with the band beneath them reading `Aug 2026`.
-Two month labels four pixels apart, disagreeing — and the wrong one is the
-prominent one, so the chart reads as if the columns were mislabelled.
-
-The label now names the period at the left edge of the viewport, snapped to the
-same tier `headerGroups` bands the timeline by: a month under day and week view,
-a year under month and quarter view, a decade under year view, the shift-day
-under shift-segmented day view. The toolbar and the band header therefore agree
-by construction rather than by two derivations that can drift. Wording is
-unchanged for the month tier — the toolbar still spells the month out
-("August 2026" beside the header's "Aug 2026").
-
-The `‹` / `›` buttons rendered an `aria-label` and an icon and carried no
-`onClick`. They now scroll the visible window one period backwards/forwards at
-that same tier, clamped to the ends of the timeline (ADR-0049 enforce-or-remove:
-wiring is the branch the label change makes available). They step the label's
-tier rather than one column, so a click always changes what the label says.
-
-The band header is untouched. It was already correct; it is the reference here.
diff --git a/.changeset/7204-gantt-tasklist-width-and-date-predicate.md b/.changeset/7204-gantt-tasklist-width-and-date-predicate.md
deleted file mode 100644
index 516b083e92..0000000000
--- a/.changeset/7204-gantt-tasklist-width-and-date-predicate.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/plugin-gantt': minor
----
-
-Size the gantt task list from its container, and give a row's dates ONE predicate
-(objectui#7204, objectui#7224; maintainer ruling 2026-09-02, option Y).
-
-**A row could show no dates at all.** Two gates decided whether a row's dates were
-painted, and they read two different widths. The Start/End columns were gated on the
-container-derived task-list width; the `8/26 → 9/2` sublabel under the title was gated
-by the component's own `@media (min-width: 640px) { .gantt-sm-hidden { display: none } }`
-rule, which reads the viewport. Between a 640px and a 1023px container both were shut,
-so the row's dates existed in the DOM twice and were painted zero times. The same hole
-opened at any width once the splitter was dragged under the threshold.
-
-The sublabel now renders on exactly the complement of the Start/End columns, both from
-the same container-derived width, and the media rule is gone. A row always carries its
-dates one way or the other.
-
-**And the task list no longer caps at 320px.** From a 1024px container up, the pane
-takes 3/8 of the container clamped to `[320, 560]` instead of a flat 320. At 1440 that
-is 540px, which leaves the title 287px with the Start/End columns still painted — a
-40-character title measures 262px in the row's font, so real-world task names stop
-truncating to about seven characters while several hundred pixels of chart sit empty.
-Measured in Chromium at a 1440px container: title 53px before, 287px after.
-
-The Start/End threshold moves from an estimated 280 to a derived **412** — 32 row
-padding + 160 for the two columns + 28 for the open-details slot + 32 of title
-furniture + 160 minimum title, each term traced to the markup that spends it. Below it
-the sublabel carries the dates. One consequence worth stating: between a 1024px and a
-1097px container the columns are now off and the dates ride the sublabel, which trades
-two 80px date cells for a title that grows from 67px to 291px.
-
-Not fixed here, and unchanged: a row's `depth * 14` indent is unbounded, so no single
-default keeps a deeply nested row legible.
diff --git a/.changeset/7206-dead-context-tail-comments.md b/.changeset/7206-dead-context-tail-comments.md
deleted file mode 100644
index ac1ddad187..0000000000
--- a/.changeset/7206-dead-context-tail-comments.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
----
-
-Comment-only fix, no release: thirteen comments across ten files described the
-tail of `dependentValues ?? ctx.formValues ?? ctx.data ?? {}` as a live channel
-a host could supply. It is not one, and never has been.
-
-`SchemaRendererContextType` (`packages/react/src/context/SchemaRendererContext.tsx`)
-declares exactly `dataSource`, `debug`, `debugFlags` and `apiFetch`, and
-`SchemaRendererProvider` accepts no other prop. There is no `data` member and no
-`formValues` member, so that tail is **unconditionally empty in production** —
-unsettable rather than merely unset, because no host can populate a member the
-type does not declare. `dependentValues` is today the only channel that can
-carry a record.
-
-The corrected comments now state what is measured. The strongest of them claimed
-the fall-through reached "the OUTER page's record"; another credited
-`dependentValues` with a context fallback that only `dataSource` actually has;
-a third framed the chain as three suppliable links when only the first has ever
-been suppliable. Two of the thirteen were written the same day this was
-measured, so they were wrong when written rather than gone stale.
-
-No runtime, type or test behaviour changes: the reads themselves are left
-exactly as they are, and the diff contains zero non-comment lines. Whether the
-channel should be made real or retired is the open question on objectui#7206 and
-is deliberately not answered here.
diff --git a/.changeset/7210-gantt-paging-chrome.md b/.changeset/7210-gantt-paging-chrome.md
deleted file mode 100644
index 7f8ff53d67..0000000000
--- a/.changeset/7210-gantt-paging-chrome.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-A gantt list view no longer shows the record-count bar, because the bar describes a
-request that view does not draw (objectui#7210, half 1).
-
-`ListView` renders one record-count bar for every `viewType`: the row count, the
-"Showing first N records. More data may be available." warning that goes with its own
-`$top: pageSize` query, and a rows-per-page selector that re-issues that query. On grid,
-kanban, calendar, gallery, timeline and map the bar is accurate — those renderers draw
-the `data` `ListView` hands down.
-
-On `gantt` it is not. Measured, not inferred: the registered `object-gantt` renderer
-forwards no prop but `schema`, so the `data` prop never reaches the chart and
-`ObjectGantt` issues its own query — one that carries no `$top` at all. In a harness of
-18 rows with `pagination.pageSize: 6`, the chart drew all 18 while the bar under it read
-"6 records · Showing first 6 records. More data may be available." Because it is the
-only paging disclosure on the screen, a reader takes it as describing the chart; that
-reading already produced a wrong finding in an application repo, which cost a browser
-session to disprove. Authoring `pagination.pageSize` could not have fixed it either —
-the chart's request never carried a page size to begin with.
-
-Scoped to `gantt` alone. Every other surface keeps its bar unchanged, warning included;
-a kanban over the same result set still reports its page honestly.
-
-Not changed here, deliberately: the gantt's query still has no ceiling, and `ListView`
-still issues its own paged query for that view (its rows still drive the loading
-skeleton and load-error panel, `UserFilters`' option counts, and the client-side CSV /
-JSON export). Capping a non-grid view's fetch is an open maintainer decision — adding
-one would turn a complete schedule into a quietly truncated one — and this correction is
-right whichever way that lands.
diff --git a/.changeset/7210-non-grid-row-ceiling.md b/.changeset/7210-non-grid-row-ceiling.md
deleted file mode 100644
index 7adf0e20b2..0000000000
--- a/.changeset/7210-non-grid-row-ceiling.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-'@object-ui/react': minor
-'@object-ui/i18n': patch
-'@object-ui/plugin-gantt': minor
-'@object-ui/plugin-calendar': minor
-'@object-ui/plugin-map': minor
-'@object-ui/plugin-tree': minor
----
-
-A non-grid view's fetch now carries a platform row ceiling, and crossing it is
-never silent (objectui#7210, maintainer ruling a′, 2026-09-02).
-
-Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
-issued a `find` with **no `$top` at all**, so the request returned the entire
-filtered result set. At the 186 rows the card was filed from that is invisible;
-on an object with 100k scheduled rows it is the whole table into the browser,
-and nothing an author could write — `pagination.pageSize` included — could
-bound a request that never carried a cap to begin with.
-
-**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
-rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
-larger they render a footnote naming both numbers, verbatim as it renders:
-*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
-ceiling nothing changes: the full set draws and no footnote appears.
-
-The four view packages take a **minor**, not a patch: a result set above the
-ceiling is no longer drawn in full, which is a behaviour break whatever the
-fixed group does to the released version number.
-
-**The ceiling is a platform constant, not an authorable key** — `2000`, exported
-from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
-`dataSource: { limit }` still does not reach these queries, by the same ruling;
-three alternatives were rejected with it (a documentation note only — still the
-whole table; truncating at `pageSize` — silent, and a complete schedule capped
-at one page; an authorable `maxRows` — a new permanent key every author sets).
-
-**Why 2,000.** One constant for all four, so the binding view sets it. Measured
-in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
-grow (virtualised task list; four events per day cell; auto-clustering above
-100 markers), while `ObjectTree` flattens every expanded node into the document
-at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
-is where the worst of the four lands at ~10,400 elements — an order of
-magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
-real application result set this card came from.
-
-New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
-`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
-and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
-footnote copy in all ten packs.
diff --git a/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md b/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md
deleted file mode 100644
index b3c6f63412..0000000000
--- a/.changeset/7212-published-tsconfig-tooling-exclude-ratchet.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
----
-
-Repo tooling and build config only — no published package changes, so no version bump.
-
-Published build tsconfigs must now exclude tooling DIRECTORIES, not just tooling FILE NAMES,
-and a new per-PR gate enforces it: `pnpm check:published-tsconfig-exclude`
-(`scripts/check-published-tsconfig-tooling-exclude.mjs`, wired into `ci.yml`'s Type Check job).
-
-The same defect had been repaired three times and gated never — objectui#4006 (73 `*.test.d.ts`
-published from two packages), objectui#4836 (9 more, one of them an emitted module whose first
-statement imports `vitest`) and objectui#6943 (the same package as the first, because that fix
-wrote the name form and the directory form was never generalised). objectui#7212 measured the
-standing exposure instead of another instance: 29 published packages carried the name form with
-ZERO offending files, green because nobody had yet added a shared helper to a `__tests__/`
-directory, not because the config would stop one. The maintainer ruled for the ratchet on
-2026-09-02.
-
-The gate reads `exclude` arrays and nothing else — no build, no artifact, no emit model. That
-narrowness is the point: objectui#4846 measured the cheap static gate "no build tsconfig program
-may contain a tooling file" and rejected it, because deciding whether a tooling file in a program
-is a defect means reimplementing three third-party emit semantics. Asking only DOES THE CONFIG
-NAME THE DIRECTORY needs none of that. The artifact-level `pnpm check:published-dist` is
-untouched and stays the second line of defence — it is the only criterion that cannot be wrong
-about what actually ships.
-
-28 build tsconfigs were converted in the same commit so `main` is green on merge. The conversion
-is emit-neutral, and that was measured rather than assumed: each package's build program file
-list was resolved with TypeScript's own config resolver before and after, and all 33 enforced
-packages came back byte-identical (0 files added, 0 removed). Identical program input means
-identical emit, so no published artifact and no bundle moves.
-
-Six published packages are named carve-outs rather than converted, because their emitter never
-builds its program from this file list, and each re-proves its own reason on every run instead of
-being trusted: `@object-ui/cli`, `@object-ui/create-plugin` and `@object-ui/data-objectstack` emit
-from a `tsup` entry graph; `@object-ui/plugin-charts` keeps its tooling exclude in the `dts()`
-options and carries no `exclude` key in its tsconfig; `@object-ui/console` and `@object-ui/runner`
-are Vite applications with `noEmit: true` and no `dts()` plugin, so they write no declarations at
-all. Two of those six — `create-plugin` and `runner` — were found at authoring time and were
-inside the ruling's initial red set; requiring a shape in a config their emitter never consults
-would have been a check that is green about nothing.
diff --git a/.changeset/7215-expand-fls-gate.md b/.changeset/7215-expand-fls-gate.md
deleted file mode 100644
index d962648ae3..0000000000
--- a/.changeset/7215-expand-fls-gate.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/plugin-grid': patch
-'@object-ui/plugin-list': patch
----
-
-FLS-gate the `$expand` projection at both build sites (objectui#7215).
-
-objectui#6898 closed field-level security on `$select`. `$expand` was left ungated at
-both projection sites — `ObjectGrid`'s own fetch and `ListView`'s `expandFields` memo —
-so a `lookup` / `master_detail` / `user` / `tree` field the current principal cannot
-read was still handed to the server for expansion. `$select` on a denied lookup asks for
-its bare foreign key; `$expand` on the same field asks the server to resolve it and
-return the related record, so the larger of the two disclosures was the ungated one.
-
-**Reproduced before it was fixed**, as failing tests at both sites, and the same leak
-reaches further on the `ListView` path: that builder's `$select` gate drops the denied
-column and then adds the expand roots back unconditionally, so the denied field walked
-back into `$select` as well. Gating the expansion closes both halves.
-
-**Grading, measured rather than assumed.** Against ObjectStack's own server this is
-defence-in-depth, exactly as objectui#6898 is: `plugin-security`'s
-`FieldMasker.maskRecord` deletes every unreadable key from each returned row, and
-objectql's expand path writes the resolved record back under that same key, so one
-statement removes the expanded object and the bare id alike; the expansion sub-read is
-itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS
-treatment). It is load-bearing for any backend that does not strip.
-
-**Nothing a permitted view did stops working.** The gate judges the OUTPUT of
-`buildExpandFields`, which is already a subset of the object's declared
-reference-bearing fields, so the "`checkField` answers false for an undeclared key"
-trap cannot be reached and derived / host-joined columns are untouched. An unanswered
-permission policy filters nothing. `buildExpandFields` itself is unchanged.
diff --git a/.changeset/7216-listview-speculative-fls.md b/.changeset/7216-listview-speculative-fls.md
deleted file mode 100644
index 29ba6a188f..0000000000
--- a/.changeset/7216-listview-speculative-fls.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-FLS-gate the speculative half of `ListView`'s `$select` projection (objectui#7216).
-
-`ListView` builds `$select` from two populations and, until now, asked them different
-questions. The user-declared `columns` were passed through `perms.checkField(...)` by
-objectui#6898. Everything the builder adds ON TOP — the kanban / gantt / timeline /
-calendar / gallery field bindings, the timeline's auto-added `status` / `priority`
-badge fields, and the operands harvested from row-action and conditional-formatting
-predicates (objectui#3501) — went through `addSpeculative`, which intersected them
-against the object's declared fields and then added them unconditionally.
-
-The two gates it needed answer unrelated questions and neither substitutes for the
-other. The known-field gate keeps an **unknown** key out, because some backends answer
-an unknown `$select` key with an empty result set rather than ignoring it. The FLS gate
-keeps a **known but denied** key out, because sending it leaks the value at the server
-boundary even though the UI hides it. A field can be perfectly well-declared and still
-denied, and that was the case this path did not handle: a kanban grouped by a denied
-field, or a gantt bound to a denied date, still named it in the request.
-
-**Reproduced before it was fixed.** Eight of fourteen new pins fail on the unmodified
-tree, each reaching the denied field only through a view binding — a denied *column* has
-been dropped since objectui#6898, so a pin naming one would have proved nothing.
-
-The gate goes inside `addSpeculative` rather than at the five call sites, so every route
-into the speculative union is covered at once; gating call sites one at a time is how
-the asymmetry arose. It runs **after** the known-field intersection, the ordering
-objectui#7179 established: `checkField` answers false for an undeclared key, so asking
-it first would drop derived and computed bindings — and would be the reason they were
-dropped. The platform record columns (`created_at`, `owner_id`, the audit FKs) are
-carved out for the reason they already are elsewhere in this builder: every object
-carries them and none declares them, so no field policy mentions them and an FLS answer
-about them is always false. Without the carve-out a calendar bound to `created_at` would
-go blank for everybody.
-
-**Nothing a permitted view did stops working.** A permitted binding is still projected,
-an unanswered permission policy filters nothing, and the projection is rebuilt when the
-policy answers — pinned, because a gate that only runs before `/me/permissions` resolves
-is a dead gate that passes almost every test written for it.
-
-objectui#7179's `addGroupingField` wrapper is removed: its predicate was identical to
-the one now inside `addSpeculative`, and two spellings of one gate is the shape that
-lets them drift. That path keeps its behaviour and gains the FLS pin it never had.
diff --git a/.changeset/7217-grouping-null-entry-guard.md b/.changeset/7217-grouping-null-entry-guard.md
deleted file mode 100644
index ba37e2c0bd..0000000000
--- a/.changeset/7217-grouping-null-entry-guard.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-Grid: a malformed entry in `grouping.fields[]` no longer crashes the whole grid.
-
-A `null` (or `undefined`) hole in the array was dereferenced with no guard at
-two places — `ObjectGrid`'s `groupValueFormatter` memo and `useGroupedData`'s
-`buildLevel` — throwing `TypeError: Cannot read properties of null (reading
-'field')` during render, before any projection was built. Both sites now read
-one normalized entry list, admitting exactly the entries `collectGroupingFieldRefs`
-harvests into the query projection, so the usable grouping levels still group
-and an unusable entry is simply dropped rather than taking the view down.
diff --git a/.changeset/7218-rowcolor-host-relay.md b/.changeset/7218-rowcolor-host-relay.md
deleted file mode 100644
index a82511ee66..0000000000
--- a/.changeset/7218-rowcolor-host-relay.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/plugin-view': patch
----
-
-Relay a per-view `rowColor` through the two object-view hosts (objectui#7218).
-
-`rowColor` is a declared member of `ListViewSchema` — imported by reference from
-`@objectstack/spec`, shape `{ field, colors? }` — and `ListView` reads it to
-seed its `rowColorConfig` state, which colours whole rows from the named field's
-value. Neither object-view host relayed it: `app-shell`'s
-`ObjectView.renderListView` builds its list schema by spreading the host's and
-then relaying 47 named keys off the active view, and `plugin-view`'s
-`ObjectView` assembles 46 inside its `object-view HOST-COMPOSITION SURFACE`
-fence. `rowColor` had a rung in neither.
-
-So an authored per-view row colour was unreachable on the object route:
-authored, validated, built and served correctly, then dropped at the relay.
-Nothing errored and every authoring gate passed — the only symptom was that the
-rows were not coloured, which an author cannot notice short of diffing the DOM.
-Same "declared and inert" shape objectui#7199 fixed for `description`.
-
-**This is a relay, not a new surface.** The interface route
-(`InterfaceListPage.tsx`) has shipped `rowColor: view.rowColor` next to
-`grouping` and `pagination` since ADR-0047, into a schema typed
-`ListViewSchema`, with no fence of any kind — so the key was already
-author-reachable and already had a delivery path; two of three hosts simply did
-not use it. The legacy shorthand for the same feature (bare `color`) already had
-a rung in both literals; only the spec-canonical spelling was missing.
-
-**No published surface moves.** Both rungs are view-sourced only, and neither
-adds a cast read off the object-view node — that would have added a 28th name to
-the objectui#5097 HOST-COMPOSITION exemption whose count the 2026-08-18 ruling
-fixed at 27, which is a ruling and not a refactor. `grouping` is the in-fence
-precedent for a view-only rung.
-
-⚠️ Not `userActions.rowColor`, a boolean permission toggle sharing this name at
-a different nesting level ("may the user open the colour panel" versus "what the
-colours are"). That key is untouched, and the new pins hold the two apart.
diff --git a/.changeset/7219-retire-viewdescription-catalog-key.md b/.changeset/7219-retire-viewdescription-catalog-key.md
deleted file mode 100644
index a8c7b42d84..0000000000
--- a/.changeset/7219-retire-viewdescription-catalog-key.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/i18n': minor
----
-
-Retire `useObjectLabel().viewDescription()` and the `_views..description`
-catalog convention it resolved (objectui#7219, maintainer ruling 2026-09-02,
-option B — enforce-or-remove).
-
-**Breaking for translation bundles, deliberately — and this text is the notice.**
-Out-of-repo translation bundles that authored
-`.objects.._views..description` cannot be seen from
-this repo, so there is no census to point at and no migration script to run:
-that key now resolves nowhere, and an entry left under it is simply ignored.
-Nothing throws, and nothing else on that node changes.
-
-**What replaces it.** A list view's description has exactly ONE channel: the
-`I18nLabel` value authored on the view entry itself — a string, or an inline
-locale map:
-
-```ts
-listViews: {
- by_unit: {
- label: 'By business unit',
- description: { en: 'Open work only.', 'zh-CN': '仅未完成的工作。' },
- },
-}
-```
-
-`ObjectView` relays that value to the renderer and `plugin-list`'s `ListView`
-resolves it against the display locale (objectui#7199, shipped before this
-change), so the authored channel already works end to end. **Migration:** move
-the sentence out of the translation bundle and onto the view entry as a locale
-map.
-
-**Why removed rather than wired in.** The member was declared and resolved but
-had zero callers and zero in-repo bundle usage — an entry authored under the
-catalog key reached no screen. Wiring it in would have put two vocabularies on
-one concept (`I18nLabel` on the entry, and the catalog key) and required a
-precedence rule between them, which is the ambiguity rather than the fix.
-
-The two sibling members on the same node are **unaffected**: `viewLabel` and
-`viewEmptyState` still resolve `_views..label` and
-`_views..emptyState.{title,message}`, and the shared `viewSuffixes` key
-builder they use is unchanged — only the `'description'` tail is gone. Pin tests
-in `@object-ui/i18n` and `@object-ui/app-shell` were retargeted onto those two
-survivors plus a case that authors the catalog `description` and asserts the
-authored value is what a consumer resolves.
diff --git a/.changeset/7221-filter-dialect-equivalence.md b/.changeset/7221-filter-dialect-equivalence.md
deleted file mode 100644
index fa22884616..0000000000
--- a/.changeset/7221-filter-dialect-equivalence.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only: pin the measured behaviour of the two filter dialects one authored view filter reaches the wire in (objectui#7221). Records the lowering `mergeFilterNodes` produces, the row sets each dialect selects through `ValueDataSource`, and the wire strings and server-side reading of each through the ObjectStack adapter. No published behaviour changes.
diff --git a/.changeset/7225-settled-schema-convergence.md b/.changeset/7225-settled-schema-convergence.md
deleted file mode 100644
index c8b9c16de3..0000000000
--- a/.changeset/7225-settled-schema-convergence.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/plugin-kanban': patch
-'@object-ui/plugin-view': patch
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-gantt': patch
----
-
-The settled-schema convergence, and the gantt's duplicate query gated
-(objectui#7225, maintainer ruling B, 2026-09-02).
-
-`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
-non-test adopter (`ObjectTree`, the component that had an actual defect —
-objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
-`ObjectCalendar` kept their own hand copies of the same shape, so a published
-export was owed compatibility forever **and** the duplication it was named for
-stayed. All three now call the hook.
-
-The migration is a pure deduplication with no behaviour delta — the hook was
-extracted *from* these three shapes, so each becomes a one-line call.
-`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
-recipe the hook's own doc comment prescribes for it by name: pass the data
-source as `undefined` for a render that must not read metadata
-(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
-expressed as "there is no source to read from" rather than as a second enable
-flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
-what made the calendar's obstacle a non-obstacle: it was about the gate half.
-
-**One observable change:** `ObjectKanban`'s rejected definition read now logs
-on `console.error` with a `[useSettledSchema]` prefix instead of
-`console.warn`. Its test spy moves with it, and now asserts on the channel
-rather than merely silencing it.
-
-**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
-undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
-dependency list, so every load issued two unbounded queries — the first with no
-`$expand` at all. Measured on this component across three latency profiles, the
-cost is not the mild "round trip bought and thrown away": when the metadata
-read is the slower of the two, which is the common case on a cold
-`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
-back to the loading placeholder, then the expanded rows. It now issues one
-query, already expanded.
-
-Gating the gantt required its schema resolution to settle on EVERY exit
-(objectui#7232): the hand-rolled effect returned without settling on
-`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
-nothing waited on it, and a chart that never loads once something does.
-`useSettledSchema` settles on all three by construction, which is what makes
-the gate safe; both exits are pinned.
-
-⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
-separate ruling, in its own commit on the same branch.
diff --git a/.changeset/7228-gantt-column-width-flat-floor.md b/.changeset/7228-gantt-column-width-flat-floor.md
deleted file mode 100644
index 6a34e9ce14..0000000000
--- a/.changeset/7228-gantt-column-width-flat-floor.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
----
-
-The gantt's timeline column width says what it does: a flat 110px floor, not a
-container-width breakpoint table (objectui#7228). **No behaviour changes** — this is
-why the changeset carries no package bump.
-
-`columnWidthForContainer(width)` branched on the container width three ways and returned
-110 from all three arms, so its parameter was read only to be compared against thresholds
-nothing acted on. That shape is not neutral: the two siblings directly beneath it,
-`taskListWidthForContainer` and `showStartEndColumns`, branch on the *same* 640/1024
-breakpoints and really do vary, so the dead table read as a live responsive policy. A
-reader — human or agent — reasonably concluded the gantt narrows its columns in small
-embeds. It does not, and has not since objectui#1870.
-
-The history settles what the arms were for. The function was born a real curve
-(`35/50/60` off `window.innerWidth`), kept it through the rename to a container-derived
-width, and was bumped to `44/64/80` inside objectui#1870 — then, in a later commit of that
-same PR titled *"floor timeline columns at 110px so day/week/month stay readable"*, the
-whole curve was deliberately replaced by a single floor. The comment added in that commit
-names the value's provenance ("user-specified minimum") and why a flat floor costs nothing
-at the wide end (the fit-stretch and manual zoom both move the width upward). The
-branching was leftover shape, not an unfinished table.
-
-So the arms are gone and the value all three returned is now a module constant,
-`BASE_COLUMN_W`, documented as the floor it is — matching how objectui#7420 hoisted the
-task-list geometry in this file: fixed px values become named module constants, helpers
-stay functions only where they genuinely vary with the container.
-
-Nine pins now hold the behaviour on both sides of each retired breakpoint (320 / 500 /
-639 / 640 / 800 / 1023 / 1024 / 1280 / 1920), plus one that requires a single distinct
-value across all of them. Nothing pinned this before: every sibling suite that reads a
-column width first forces the container to 1280 "so columnWidth=110 (deterministic)",
-pinning the widest arm alone — so neither re-curving the width nor moving the floor would
-have been caught.
diff --git a/.changeset/7230-expand-fls-gate-five-sites.md b/.changeset/7230-expand-fls-gate-five-sites.md
deleted file mode 100644
index 17fe56f95b..0000000000
--- a/.changeset/7230-expand-fls-gate-five-sites.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-gantt': patch
-'@object-ui/plugin-detail': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/app-shell': patch
----
-
-FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230).
-
-objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope
-(`ObjectGrid`, `ListView`). The helper is reached from more places than that. This
-closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`,
-`DetailView`, and `ObjectDataTable` (which builds its own whitelist in
-`computeLookupExpand` rather than calling `buildExpandFields`).
-
-**Three of them pass no column list at all**, which makes them the sharp ones:
-`buildExpandFields` reads an absent column list as "no column restriction" and falls
-back to **every declared relation on the object**, denied ones included. So a standalone
-calendar, a gantt, and every record page in the console asked the server to resolve the
-object's full relation set by default rather than by configuration.
-
-**`DetailView` was input-gated, and that is the defect rather than the fix.** Its column
-list is already FLS-filtered field by field, which is exactly the route PR #7229 measured
-as unsound: an emptied column list reads as "no column restriction", so a detail view
-whose authored fields are all denied had its `$expand` **widened** from the relations it
-asked for to every relation the object declares. The principal who may read least was
-asking for the most.
-
-**Reproduced before it was fixed**, as a failing test per site.
-
-**Grading, measured rather than assumed.** Against ObjectStack's own server this is
-defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s
-`FieldMasker.maskRecord` deletes every unreadable key from each returned row and
-objectql's expand path writes the resolved record back under that same key, so one
-statement removes the expanded object and the bare id alike; the expansion sub-read is
-itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS
-treatment). It is load-bearing for any backend that does not strip, and the
-client-request side is real regardless.
-
-**Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT,
-which contains only the object's declared reference-bearing fields, so the "`checkField`
-answers false for an undeclared key" trap cannot be reached and derived / host-joined
-columns are untouched. An unanswered permission policy filters nothing. Neither
-`buildExpandFields` nor `computeLookupExpand` is changed.
diff --git a/.changeset/7231-gantt-stale-reload-finally.md b/.changeset/7231-gantt-stale-reload-finally.md
deleted file mode 100644
index e1221f261d..0000000000
--- a/.changeset/7231-gantt-stale-reload-finally.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/plugin-gantt': patch
----
-
-`ObjectGantt` no longer blanks the chart when one reload supersedes another
-
-`reload()` already sequenced concurrent runs with `reloadSeqRef` and guarded every
-result write with `isCurrent()`, but its `finally` was unguarded — so a **superseded**
-reload still flipped `loading` / `refreshing` off. The stale run only had to finish
-first, which is the ordinary case whenever a second reload is issued while the first
-is still in flight: the placeholder was released, no rows had arrived, and the user
-saw an empty chart until the fresh response landed.
-
-The `finally` now clears the flags only when the run reaching it is still the current
-one. It clears **both** flags rather than only the one its own `silent` mode set:
-being current at that point means nothing is in flight any more, so clearing only its
-own mode would strand the other flag whenever the superseded run used the other mode
-— a silent toolbar refresh overtaken by a filter-change reload would have left the
-refresh button busy for the life of the component.
-
-This is the reload guard alone. Nothing about which queries are issued, how they are
-projected or how they page changes.
diff --git a/.changeset/7238-disabled-one-carrier.md b/.changeset/7238-disabled-one-carrier.md
deleted file mode 100644
index 2961511fc5..0000000000
--- a/.changeset/7238-disabled-one-carrier.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-"@object-ui/components": patch
----
-
-fix(components): eight widgets now consume `SchemaRenderer`'s evaluated `disabled` verdict instead of re-reading the raw authored key
-
-`disabled` on a schema node is `boolean | string` — the string being a predicate. `SchemaRenderer` evaluates `disabled` / `disabledOn`, strips the raw key from the props it spreads, and forwards the answer as a real `disabled` prop. `ui:form`, `ui:button`, `ui:input`, `ui:textarea`, `ui:checkbox`, `ui:select`, `ui:combobox` and `ui:collapsible` re-read the raw key beside that verdict, and an expression string is truthy however it evaluates.
-
-Two user-visible defects go away:
-
-- `ui:form` — a form declaring `disabled: "${...}"` greyed out every field, the submit button and the cancel button even when the predicate was FALSE.
-- `ui:button` — `loading: true` did not disable the button when it was rendered through `SchemaRenderer`: the computed state was overwritten by the forwarded verdict arriving through the DOM pass-through spread, so the spinner ran on a live control.
-
-The six DOM pass-throughs kept their behaviour through `SchemaRenderer` and now read the verdict by name rather than depending on spread order.
diff --git a/.changeset/7243-colorfield-shared-ladder.md b/.changeset/7243-colorfield-shared-ladder.md
deleted file mode 100644
index f2fdbb5e31..0000000000
--- a/.changeset/7243-colorfield-shared-ladder.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/plugin-gantt': patch
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-timeline': patch
----
-
-`colorField` now means the same thing in the gantt, the calendar and the timeline
-(objectui#7243).
-
-**The inversion this fixes.** `gantt.colorField` is documented as "field that drives the
-bar color", and the renderer passed the stored value straight into the bar's
-`backgroundColor`. Pointing the key at a select field therefore emitted
-`backgroundColor: "open"` — not a colour, so the browser dropped the declaration and
-every bar rendered identically. OMITTING the key was strictly better: the absent-key
-branch derived a real colour per status. Declaring the documented key was worse than not
-declaring it, with no error, warning or console message either way.
-
-The same key also meant three different things across the three lenses: the timeline
-resolved the field's authored option `color`, the calendar hashed the raw value onto a
-fixed palette, and the gantt emitted the raw value. An author colouring three views by
-one field got three unrelated results, one of which was no colour at all.
-
-**The ladder.** `@object-ui/core` gains `createFieldColorResolver` — the timeline's
-resolver, lifted so all three call it:
-
-1. the field's own option `color` for the record's value;
-2. else the value itself when it already IS a colour literal (`#rgb`, `#rrggbb`,
- `#rrggbbaa`, `rgb(...)`, `hsl(...)`);
-3. else each renderer's own last rung, which is deliberately NOT shared — the gantt
- derives a semantic-token hex (a bar must be painted), the calendar keeps its
- theme-aware 8-stop hash (a soft tint, not a solid fill), the timeline draws its
- default marker.
-
-**What changes for authors.** A gantt or calendar whose `colorField` points at a select
-field with authored option colours now paints those colours. A gantt value that is
-neither an option colour nor a colour literal now derives a colour instead of emitting
-an invalid CSS value — including a palette NAME (`red`), which now resolves to that
-palette's hex, the behaviour the key's own contract has always promised ("hex or
-semantic name") and the one `borderColorField` already had. `gantt.borderColorField`
-takes rung 1 as well, so an authored option colour reaches the alert stroke; it keeps
-today's behaviour otherwise and deliberately gains no derivation rung, since the stroke
-is opt-in and deriving one for every record would draw an alert on records that have
-none.
-
-Calendars whose `colorField` points at a plain categorical field are unchanged: that
-value still reaches `CalendarView`'s deterministic hash exactly as before. The timeline
-is unchanged apart from accepting the 8-digit `#rrggbbaa` hex spelling the calendar
-already accepted.
diff --git a/.changeset/7245-default-view-namefield-lead.md b/.changeset/7245-default-view-namefield-lead.md
deleted file mode 100644
index 01458fa56d..0000000000
--- a/.changeset/7245-default-view-namefield-lead.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/app-shell': patch
-'@object-ui/plugin-grid': patch
----
-
-A synthesized default list view now always leads with the object's name field
-(objectui#7245).
-
-**The defect.** An object that declares no list view gets its default grid columns
-synthesized from `highlightFields`, taken verbatim. But `highlightFields` is ADR-0085's
-*"most important fields"* role, not a column list — and its first consumer, the
-detail-page highlight strip, **deliberately removes the title field**, because the page
-H1 directly above it already shows one. So metadata that is entirely correct routinely
-omits the record's name from `highlightFields`. The showcase `showcase_account` declares
-`nameField: "name"` and `highlightFields: ["status", "industry", "annual_revenue"]`, and
-its default `所有记录` grid rendered 14 rows whose columns were `#` / Lifecycle / Industry
-/ Annual Revenue / actions — no name column, and no way to tell one account from another.
-
-A list has no H1 to lean on, so the same declaration needs the opposite treatment here.
-This is not a new convention: `deriveLookupColumns` in `@object-ui/fields` already leads
-its record-picker columns with the display field and filters it out of the declared list.
-The list faces now agree with it.
-
-**What changed.** `@object-ui/core` gains two exports on the ADR-0079 title ladder:
-
-- `resolveNameField(objectDef)` — *which field* titles an object: the declared
- `nameField` (then its deprecated `displayNameField` / `NAME_FIELD_KEY` aliases), else
- the type-aware derivation. The name-space twin of `getRecordDisplayName`, which answers
- what that field *says* on one record. Both now read one spelling of the declared
- pointer, so they cannot drift into naming different fields.
-- `leadWithNameField(objectDef, columns)` — moves that field to the front of a
- **synthesized** column list.
-
-All three faces that synthesize default list columns call it: `ObjectView`
-(`defaultListColumnsFromObject`), `InterfaceListPage` (`defaultColumnsFromObject`) and
-`ObjectGrid`'s own derivation. The name field is **moved**, not merely appended, so an
-author who lists it third still gets it first — "the column that identifies the row"
-means first. On the two capped faces the lead is applied *before* the 5 / 6-column slice,
-so an object declaring its name field late no longer loses it off the end.
-
-**Scope, deliberately narrow.** Author-declared column lists are untouched — a view or
-grid that declares `columns` / `fields` said what it wants, and reordering it would be
-renderer-side second-guessing of metadata. Three cases also decline to lead: a name field
-the object carries no field def for (never fabricate a column), one marked
-`hidden: true` (the author said don't show it), and a *derived* pick that lands on a
-system-managed column — `deriveTitleField` filters by type only, and leading a default
-list with a raw id is the regression objectui#2702 / #2777 fixed. A *declared*
-`nameField` pointing at a system field still leads: `sys_migration` really does point at
-`id`, and an explicit designation is not a heuristic misfire.
diff --git a/.changeset/7248-scatter-legend-names-its-series.md b/.changeset/7248-scatter-legend-names-its-series.md
deleted file mode 100644
index fa76eda861..0000000000
--- a/.changeset/7248-scatter-legend-names-its-series.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-Name the scatter legend's series, so its swatch stops reading as a stray data point
-(objectui#7248).
-
-The Chart Gallery scatter ("Estimate vs Progress") appeared to draw a seventh point
-below the x-axis, outside the plot area. It was not a point. `ChartLegendContent`
-resolves a label as `config[nameKey || item.dataKey || 'value']`, and a ``
-carries **no `dataKey`** — scatter's keys live on the XAxis/YAxis, not on the mark — so
-the key collapsed to the literal string `'value'`, missed a config keyed by measure
-name, and the legend entry rendered its colour swatch with no text beside it. An 8x8
-square in `--chart-1`, the same colour as the marks, sitting under the x-axis.
-
-Measured on the running showcase in real Chromium: the swatch sat at cy 341 against a
-plot area ending at cy 295, on a y scale of 4.835 px per unit — y = -9.5, at x ≈ 45.
-That is the "x≈40, y≈-10" the report described, to the pixel, and all six real marks
-were inside the plot area at every viewport width swept from 1440 down to 480.
-
-**The y domain was not the defect and is unchanged.** Clamping it — the fix the report
-asked for — would have created the bug it described: mixed-sign and all-negative
-fixtures are pinned here drawing every mark, because recharts already extends the
-domain to cover negative values.
-
-Two changes. The scatter now passes `nameKey` so its legend resolves the measure's
-label, and `ChartLegendContent` falls back to the series `name` recharts itself put on
-the legend item when the config lookup misses. The second closes the class rather than
-this one instance: the swatch renders unconditionally, so a config miss must never
-leave it anonymous. Charts whose config already resolves are unaffected — only a
-currently-empty label changes.
diff --git a/.changeset/7252-form-outcome-toast-supersede.md b/.changeset/7252-form-outcome-toast-supersede.md
deleted file mode 100644
index 07abcf2c6c..0000000000
--- a/.changeset/7252-form-outcome-toast-supersede.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/components': patch
-'@object-ui/console': patch
----
-
-A form no longer ends on a screen asserting both a failure and a success (objectui#7252).
-
-A refused submit raised an error toast that nothing ever retired, so when the user
-fixed the input and submitted again the confirmation of that second attempt appeared
-*beside* the refusal of the first — a wizard's last step showing "Invalid project
-status transition." and "Your new project is ready…" at the same time.
-
-Every outcome toast a form raises now travels under one stable per-form id, so the
-later outcome supersedes the earlier one instead of stacking beside it:
-
-- `@object-ui/components`' form renderer publishes its three outcome toasts (the
- field-level rejection, an `onAction` error, and a rejected write) under that id, and
- retires the previous attempt's toast in the same place it already cleared the
- previous attempt's in-form banner. This is what fixes the reported wizard flow: the
- refusal comes from this renderer while the success toast is raised by the host
- (`WizardForm` / `ObjectForm`), so no single raiser could supersede the other before.
-- the console's own `FormPage` publishes its confirmation and its submit failure under
- one id, for the same reason.
-
-Toast durations are unchanged — this is about supersession, not lifetime. The
-objectui#4190 arm is deliberately excluded: a refused redirect *destination* still gets
-its own toast, because the write succeeded and that refusal has to stay readable beside
-the confirmation it qualifies.
diff --git a/.changeset/7256-chrome-home-follows-declared-landing.md b/.changeset/7256-chrome-home-follows-declared-landing.md
deleted file mode 100644
index 60ca128b76..0000000000
--- a/.changeset/7256-chrome-home-follows-declared-landing.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-The console chrome's Home affordances follow the product's declared landing (objectui#7256).
-
-`/` has honored `app.isDefault` since the hardcoded `PREFERRED_APPS = ['cloud_control']`
-redirect was retired; the chrome had not. The top-bar logo, the sidebar's Home row, the
-mobile sheet's Home row and the app-switcher's Home entry each named `/home` literally —
-the ENVIRONMENT layer's launcher (ADR-0075). So a deployment that declares a landing
-offered the customer two homes in two voices, and one click on the logo left the declared
-one.
-
-On cloud's control plane that second home is actively wrong: its "Build an app" / "Start
-from a template" cards are environment-side actions that cannot work from the control
-plane, and its "Your apps" tiles are the control plane's own internal management apps.
-
-- `@object-ui/app-shell` adds `resolveDeclaredHomePath()` — the one reader of the
- declaration — plus `useHomePath()`, which the four chrome sites now consume. The signal
- is the App metadata the server already sends: no hostname sniff, and no product name
- baked into the shared bundle.
-- `@object-ui/console`'s `/` resolver is unchanged, and a behavioural matrix now pins its
- answer equal to the chrome's for every declared app list, so the post-login landing and
- the logo cannot drift apart.
-
-Deployments that declare no landing are unaffected: every Home affordance still resolves
-to `/home`. The "this app is gone" recovery redirects in `AppContent` / `ConsoleShell` are
-deliberately untouched — they are error paths, not Home affordances.
diff --git a/.changeset/7257-metadata-hmr-give-up-after-404.md b/.changeset/7257-metadata-hmr-give-up-after-404.md
deleted file mode 100644
index 94039eedd7..0000000000
--- a/.changeset/7257-metadata-hmr-give-up-after-404.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/console': patch
----
-
-`MetadataHmrReloader` stops flooding a production-posture deployment with
-`GET /api/v1/dev/metadata-events` 404s (objectui#7257).
-
-The dev-only HMR component subscribes via `EventSource`, gated on
-`import.meta.env.DEV`. That gate is not airtight against every rig: a
-"prod-like" build/serve setup that forces `NODE_ENV=development` for the
-*build tooling* while running the server itself in production posture can
-bake `DEV === true` into the shipped bundle even though the server never
-mounts the dev route there — and the old reconnect loop treated every closed
-connection as transient, retrying on a fixed `reconnectDelayMs` (2s)
-forever. On an env host that is ~30 requests/minute of 404s per open
-record/list page, drowning out the legitimate `sys_inbox_message` /
-`sys_notification_receipt` polling in the same console.
-
-The first `connect()` attempt now doubles as the real capability probe: if
-the stream closes before it ever reaches `open`, the component gives up for
-good instead of retrying (and specifically not a longer interval either —
-that would still spam 404s, just slower). A stream that DID open at least
-once and later drops — a real dev-server restart or network blip — keeps
-reconnecting exactly as before.
-
-No production-side replacement is introduced here: this component's only
-job is turning "a metadata file changed on disk" into a full reload, and
-production deployments have no such file-system event to watch. The
-separate Studio-left-nav-doesn't-refresh caching issue does not go through
-this SSE stream and needs its own fix.
diff --git a/.changeset/7258-chart-builtin-aggregate-label.md b/.changeset/7258-chart-builtin-aggregate-label.md
deleted file mode 100644
index 1d200a3fb6..0000000000
--- a/.changeset/7258-chart-builtin-aggregate-label.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/i18n': minor
-'@object-ui/plugin-charts': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-report': patch
----
-
-Localize the server's built-in aggregate measure titles on dataset charts
-(objectui#7258 — consumer half of the objectstack#14492 contract; maintainer
-ruling B, 2026-09-02).
-
-A dataset-bound chart's aggregate axis / legend title read the analytics
-service's hard-coded English `Count` on a zh console whose category labels were
-already Chinese. The renderer was passing `fields[].label` through verbatim —
-correctly, for an author-declared measure (objectui#4106) — and had no way to
-tell the server's built-in default apart from an author's label.
-
-The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural
-discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' |
-'count_distinct'`, populated only on the server-side built-in defaults
-(objectstack#14492). This change is the consumer side of that contract:
-
-- `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` —
- `ChartResultField` plus the optional `builtinAggregate` carrier
- (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than
- on it because the spec this release is built against does not carry the key
- yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` /
- `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries
- the locale strings in (core stays React-free and i18n-free — the same
- division as `nullCategoryLabel`). A field carrying a recognised
- discriminator resolves through that map; every other field keeps its wire
- `label` verbatim — never by matching the label's text or the field's name
- (the rejected option A).
-- `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings
- through the existing `report.aggregate.*` keys (zh already carried 计数 /
- 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary).
-- `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`),
- `plugin-report` (`DatasetReportRenderer`): pass the resolved map to
- `buildChartSeries`.
-
-Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same
-chart titled `计数`; an `en` session still reads `Count`; an author-labelled
-measure (`Tasks`) and a measure literally named `count` without the
-discriminator are byte-for-byte unchanged. Until the upstream field is
-populated the wire carries no discriminator and every chart renders exactly as
-before.
diff --git a/.changeset/7271-localstorage-probe-node26.md b/.changeset/7271-localstorage-probe-node26.md
deleted file mode 100644
index 103d6f8573..0000000000
--- a/.changeset/7271-localstorage-probe-node26.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
----
-
-Repair the test-environment storage globals so a `Storage.prototype` instrument
-cannot go silently blind on a newer Node (objectui#7271). Test only; no package
-is released by this change.
-
-From Node 26 the experimental Web Storage globals are on by default, and
-Vitest's `populateGlobal` copies happy-dom's `Storage` class but not its
-`localStorage`/`sessionStorage` — so the class and the stores came from two
-different implementations, and instrumentation of `Storage.prototype` observed
-nothing. `vitest.setup.base.ts` now states the rule positively (in a DOM
-environment every storage global is that environment's own `Storage`) and
-verifies it, and `anonSeedScope-5746.enumeration.test.tsx` names the failure
-mode instead of reporting seven anonymous zeros.
diff --git a/.changeset/7279-object-view-split-route-pin.md b/.changeset/7279-object-view-split-route-pin.md
deleted file mode 100644
index b94765a695..0000000000
--- a/.changeset/7279-object-view-split-route-pin.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Re-derive `ObjectViewSchema`'s side of the objectui#6058 SPEC-DERIVED / LOCAL split in
-`zod-mirror-parity.test.ts` (it is LOCAL: the mirror takes no shape from the spec), correct
-the split's counts, and pin them against the ledger and `SPEC_DERIVED_PAIRS` (objectui#7279).
-Test only; no package is released by this change.
diff --git a/.changeset/7282-console-docs-fragment-markers.md b/.changeset/7282-console-docs-fragment-markers.md
deleted file mode 100644
index f17a41a24e..0000000000
--- a/.changeset/7282-console-docs-fragment-markers.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-Console documentation only (`apps/console/docs/**`) — nothing published moves, so no version bump.
-
-`UI_IMPROVEMENT_PROPOSAL.md` section 3.1 taught ``.
-The exported component takes `orientation`, not `direction`
-(`packages/components/src/ui/resizable.tsx:44`, pinned by
-`packages/components/src/__tests__/resizable-orientation.test.tsx` and used in-tree at
-`packages/components/src/custom/navigation-overlay.tsx:519`), so a reader who copied the guide's
-first prop got a type error. The prop is corrected in place.
-
-The other ten red `ts` / `tsx` blocks across that tree are excerpt fragments rather than defects,
-and each now carries `check-doc-snippet-types.mjs`'s per-block fragment declaration with a written
-reason naming the identifiers the excerpt leaves to its host. No `UNGATED_DOCS` entry was added:
-the 2026-09-02 ruling on objectui#6600 rejects whole-file exemptions, and this card is that
-ruling's step 1 — content first, then the gate roots move (objectui#6600 step 2).
-
-Why the empty frontmatter rather than a bump: `apps/console/docs/**` is not in `@object-ui/console`'s
-`files` (`dist`, `plugin.ts`, `plugin.js`, `plugin.d.ts`, `README.md`), so no published artifact
-changes. `scripts/check-changeset-presence.mjs` agrees — it reports no changeset owed for this
-range. Note for the record that `@object-ui/console` is **not** `private: true`: it is a published
-member of the 40-package `fixed` group at 17.6.0, so the "no release" declaration rests on the
-unshipped path, not on privacy.
diff --git a/.changeset/7287-resolve-title-field-shared-ladder.md b/.changeset/7287-resolve-title-field-shared-ladder.md
deleted file mode 100644
index 6bf99f0bf0..0000000000
--- a/.changeset/7287-resolve-title-field-shared-ladder.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-'@object-ui/plugin-detail': minor
----
-
-`resolveTitleField` delegates to the shared ADR-0079 ladder (objectui#7287).
-
-ADR-0079 collapsed ~6 divergent record-title resolvers onto one — `@object-ui/core`'s
-`record-title.ts`, whose header tells the "Untitled everywhere" story that produced it.
-`plugin-detail` grew one back. `resolveTitleField` now calls core's `resolveNameField`
-and nothing else, so the detail page, the list column and the lookup chip cannot
-disagree about which field titles an object.
-
-**Two rungs are gone.**
-
-`def.primaryField` — a `DetailViewSchema` key (`@object-ui/types` `views.ts`), read off
-an OBJECT def and ranked ABOVE the canonical `nameField` ADR-0079 Phase 2 made the
-pointer (AGENTS.md Commandment #0.1). No producer can put it there: `@objectstack/spec`'s
-object schema is a `strictObject` that answers `unrecognized_keys: ['primaryField']`,
-and `ObjectSchema.create()` throws — which is why objectstack#6326 deleted the identical
-read from two lint rules. A census across both repos found **zero** object payloads
-carrying it (the only writers are three test fixtures), and `primaryField` appears in
-**zero** files of the shipped `@objectstack/spec@17.2.0` dist against 68 for `nameField`.
-Same shape as the undeclared `objectDef.titleField` read objectui#6531 measured and
-#6557 removed. `DetailViewSchema.primaryField` is untouched and still honoured by
-`DetailView`'s own header — it is a view key, and on a view it is legitimate.
-
-The literal `['name','full_name','title','subject','display_name']` walk — a NAME match
-with no type check.
-
-**User-visible, deliberately.** Two shapes retitle:
-
-- An object with no conventional name field — `{ due_at: datetime, headline: text }` —
- resolved to `null` here while every other surface titled the record `headline`. The
- detail page now agrees, and the highlight strip stops repeating the H1 as its first
- chip (the objectui#2548 duplication, which until now only DECLARED titles were spared).
-- A field named `title` of a non-title-eligible type (e.g. a `select`) was chosen by
- name alone and is now correctly skipped, so the strip no longer hides a chip on
- account of a field the H1 never shows.
-
-**A third user-visible change, in the highlight strip's skip set.** It previously read
-`primaryField`, `nameField` and `displayNameField` separately and skipped EVERY declared
-one; it now skips the single field the shared ladder resolves. These differ on one input:
-an object declaring `nameField` and the deprecated `displayNameField` alias to DIFFERENT
-fields. The alias's field is no longer hidden from the strip — deliberately, because
-`getRecordDisplayName` reads the same ladder and renders the WINNER's value, so the loser
-is an ordinary field the H1 never shows and hiding it spent a strip slot for nothing.
-This also makes the heuristic branch agree with the declared branch, which has only ever
-filtered a single title field.
-
-`deriveHighlightFields` asks the retirement gate (objectui#4914) about the title field
-**before** skipping it: an object whose only retired-typed field is the one that titles
-it must still get its report, which skipping-first would have silenced.
-
-`ObjectDefLike.primaryField` stays DECLARED but is never read. The interface is
-re-exported from this package's index and this package is published, so removing the
-member would narrow a shipped `.d.ts` — a contract change owed its own card, not a rider
-on a behaviour fix.
diff --git a/.changeset/7292-turbo-owns-build-order.md b/.changeset/7292-turbo-owns-build-order.md
deleted file mode 100644
index 7f2d617f5c..0000000000
--- a/.changeset/7292-turbo-owns-build-order.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
----
-
-Build orchestration only; no published behaviour changes.
-
-`@object-ui/components` and `@object-ui/site` each carried a hand-written
-`prebuild` (and, for components, a `pretest` pointing at it) that listed the
-workspace packages their build needed. That list is a second, unchecked copy of
-the package's own `dependencies`, and the components one had drifted: it named
-three packages while the closure turbo derives is seven, so
-`pnpm --filter @object-ui/components build` failed inside `@object-ui/react` on
-any tree where `@object-ui/i18n` and `@object-ui/data-objectstack` were not
-already built. Both hooks are deleted; turbo's `build.dependsOn: ["^build"]` is
-now the single source of build order, as it already was for every path CI takes.
-
-Trade-off, recorded here rather than left implicit: the bare per-package forms
-`pnpm --filter @object-ui/components build` and `pnpm --filter @object-ui/components test`
-no longer build their upstream packages first. They were never a supported entry
-point on a clean tree — that is the defect this removes, not a capability it
-takes away — but on a warm tree they used to be self-sufficient and now are not.
-The supported forms are `turbo run build --filter=@object-ui/components` and
-`turbo run test --filter=@object-ui/components` (turbo's `test` task also
-declares `dependsOn: ["^build"]`), plus the repo-root `pnpm test`. Every
-in-repo document and hint string that taught the direct form now teaches the
-turbo form, and the root `site:build` script goes through turbo, matching what
-CI already ran.
diff --git a/.changeset/7295-chat-message-avatar-keys.md b/.changeset/7295-chat-message-avatar-keys.md
deleted file mode 100644
index 96c162b710..0000000000
--- a/.changeset/7295-chat-message-avatar-keys.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Declare `avatar` and `avatarFallback` on `ChatMessage`, on both faces
-(objectui#7295 — the residue of objectui#4424, whose `RuntimeOnlyMessageKeys`
-named only the three keys API mode lifts out of the stream, never the two a
-human author writes by hand).
-
-`packages/plugin-chatbot/src/index.tsx:173–178` reads
-`message.avatar || userAvatarUrl` and
-`message.avatarFallback || userAvatarFallback` (and the assistant twins), the
-authoring-to-runtime seam spreads every unlisted key through
-(`chatMessageAdapter.ts`, `...passthrough`), and the SDUI renderer feeds the
-authored `messages[]` straight in — a per-message avatar override renders, is
-documented, and no authoring-facing type declared it. `ChatMessage` in
-`packages/types/src/complex.ts` has no index signature (objectui#5155,
-deliberately — none is added here), so an author annotating
-`ChatbotSchema.messages` was told a value that renders is an error (TS2353); the
-zod mirror `ChatMessageSchema` is a plain strip-mode `z.object`, so the value
-parsed green and was silently DROPPED from the parsed output.
-
-**patch, not minor: the accept set only widens toward what already renders.**
-Both keys are optional; no document that validated before stops validating, and
-no TypeScript value that compiled before stops compiling. Two verdicts move,
-both measured on `446d93d` before the change and both toward the renderer's
-behaviour:
-
-- The mirror now KEEPS an authored `avatar` / `avatarFallback` through
- `ChatMessageSchema`, `ChatbotSchema` and `safeValidateSchema` — it stripped
- them before. A consumer that renders the PARSED document (none in this
- repository does; the renderer receives the authored one) sees the override for
- the first time.
-- The mirror now REFUSES a non-string value at the key (`avatar: 42` was
- admitted-and-stripped before) — enforcement of the declared type, not a new
- capability.
-
-Same precedent as `CheckboxSchema.wrapperClass` (objectui#6938) and the
-objectui#6150 batch. `RuntimeOnlyMessageKeys` in `plugin-chatbot` is untouched;
-`SeamChatMessage` inherits the two keys through its `ChatMessage` half. The
-three example blocks on `content/docs/plugins/plugin-chatbot.mdx` that PR #7294
-left unannotated (`supportChat`, `salesBot`, `multiAgentChat`) are annotated
-`ChatbotSchema` again.
diff --git a/.changeset/7313-object-calendar-record-source.md b/.changeset/7313-object-calendar-record-source.md
deleted file mode 100644
index 50d5f4eae2..0000000000
--- a/.changeset/7313-object-calendar-record-source.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ObjectCalendarSchema` declares the record-source ladder its renderer already
-reads — `data`, `staticData`, `objectName` — on both faces, in the shape
-objectui#6939 landed on `object-map` and `object-gantt` (objectui#7313).
-
-`ObjectCalendar` resolves its records through the shared ladder
-(`resolveRecordSourceConfig` in `@object-ui/core`, called from
-`plugin-calendar/src/ObjectCalendar.tsx`): `data` first, then `staticData`,
-then `objectName`. The published TypeScript interface REQUIRED `objectName` and
-declared neither `data` nor `staticData`; the published Zod mirror did the same.
-So an `object-calendar` node authored on `staticData` — the route the plugin
-page documents twice — rendered correctly and was refused by
-`safeValidateSchema`, and could not be annotated with its own type
-(`TS2741: Property 'objectName' is missing`).
-
-- `objectName` becomes optional on the TypeScript interface and on the mirror
- in the same stroke; it stays the object-provider key.
-- `data` (`ViewData` / `ViewDataSchema`) and `staticData` (`any[]`) are
- declared on both faces, spelled exactly as `ObjectGanttSchema` spells them.
-- The member ends in `requireRecordSource('object-calendar')`: a node
- authoring NONE of the three is refused by name — one root-level issue,
- `params.code = 'RECORD_SOURCE_REQUIRED'`, the map/gantt message naming
- `data`, `staticData` and `objectName` — instead of by a missing `objectName`.
-
-**A widening.** A node authoring `staticData` or `data` without `objectName`
-now validates (it always rendered — the read is
-`resolveRecordSourceConfig(schema)`, keyed on the three). Every document that
-validated before still validates: `objectName` alone still parses, an empty
-one included, because presence is `!== undefined`. The one shape the
-refinement refuses (none of the three) was refused before too, at
-`objectName`. The two static-data examples in
-`content/docs/plugins/plugin-calendar.mdx` are now annotated
-`ObjectCalendarSchema` and compile under the doc-snippet gate.
diff --git a/.changeset/7319-dead-schemarenderer-retired.md b/.changeset/7319-dead-schemarenderer-retired.md
deleted file mode 100644
index 225cde7350..0000000000
--- a/.changeset/7319-dead-schemarenderer-retired.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/components': patch
----
-
-Delete the second, dead `SchemaRenderer` in `packages/components/src` (objectui#7319).
-
-`packages/components/src/SchemaRenderer.tsx` was a 28-line component carrying the same
-export name as the real renderer in `packages/react/src/SchemaRenderer.tsx`. Nothing
-reached it: it is absent from the package barrel, no file in the repo imports it by any
-form, and the package's `exports` map has no subpath that resolves to it.
-
-**No behaviour changes.** The file was never in the runtime bundle — two markers unique to
-it appear in zero `dist/` files, while controls for barrel-exported symbols appear in four
-each. Its only shipped footprint was a stray types-only `dist/SchemaRenderer.d.ts` with no
-runtime module behind it, reachable through no specifier; the published tarball loses that
-file, and no importable surface changes in either direction. Hence a patch, not a minor.
-
-**Why deleting beat keeping.** The copy is a trap, which is what the card's triage asked
-whoever took it to settle. It evaluates no predicate at all: of the real renderer's six
-visibility legs (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` /
-`hiddenOn`) it consults exactly one, `hidden`, and by bare truthiness rather than
-evaluation — so a node declaring `hiddenOn` is never hidden, and the two enablement legs
-(`disabled` / `disabledOn`) are not read at all. It then spreads `{...schema}` raw, so
-`disabled` would reach the widget as an unevaluated value, which is the precise inverse of
-the real renderer's contract: evaluate the predicate, strip the raw key, forward only the
-verdict.
diff --git a/.changeset/7322-object-kanban-component-props.md b/.changeset/7322-object-kanban-component-props.md
deleted file mode 100644
index 9cc7ff4177..0000000000
--- a/.changeset/7322-object-kanban-component-props.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-'@object-ui/plugin-kanban': minor
----
-
-`ObjectKanbanComponentProps.schema` names both node types the component is registered for
-(objectui#7322 item ②, following the objectui#5903 / #5018 land shape).
-
-`ObjectKanbanRenderer` is registered under two keys — `'object-kanban'` and `'kanban'` —
-and the two keys have different declared node types: `ObjectKanbanSchema` (`type:
-'object-kanban'`, `objectName` and `groupBy` required) and `KanbanSchema` (`type:
-'kanban'`, both optional). The prop named `KanbanSchema` alone, so **no `object-kanban`
-node was assignable to the component that renders it**, and the discriminants are disjoint
-string literals, so no cast-free annotation existed for half the boards this component
-serves. It is now the union of the two.
-
-## What settled it: the read set
-
-`ObjectKanban` reads thirteen keys off `schema`. Neither declaration covers them; the two
-TOGETHER cover twelve, and each arm is load-bearing:
-
-- `objectName`, `groupBy`, `limit`, `cardFields` — declared on both;
-- `columns`, `cardTitle`, `swimlaneField`, `grouping` — `KanbanSchema` only;
-- `titleField` — `ObjectKanbanSchema` only (which is why that read was spelled
- `(schema as any).titleField`);
-- `data`, `bind`, `className` — `BaseSchema`;
-- `filter` — declared by **neither** face, still riding `BaseSchema`'s index signature.
- Measured and reported, **not** changed here: this card moves the prop, not the two
- published schema faces.
-
-So naming `ObjectKanbanSchema` alone — the remedy the original card implied — would have
-been wrong in the other direction: it drops four declared reads and the `'kanban'`
-registration.
-
-## Not affected
-
-Widening a member of an exported prop type is additive: every caller that passed a
-`KanbanSchema` still compiles, and the union claims exactly the accept set the registry
-dispatches to this component — a third node type is still turned away. The runtime is
-untouched; `ObjectKanbanRenderer` still takes `schema: any`, so no shape is turned away
-there either (the objectui#5903 disposition, restated). The view-level `kanban.groupField`
-alias, `BaseSchema`'s index signature, and `@object-ui/types` are all untouched.
-
-## Casts this removes
-
-Inside `ObjectKanban.tsx`, three schema-key reads drop their `as any`: `titleField` (two
-sites, now honest because the `object-kanban` arm declares it) and `cardFields` /
-`cardTitle` (already declared; the casts were redundant). `(schema as any).navigation`
-**stays** — `navigation` is declared on neither face, so removing the cast would change
-nothing but the spelling of an index-signature read.
-
-Four of the six in-package fixtures that mount an `object-kanban` board drop their
-`as never` escape for a real `satisfies ObjectKanbanSchema`. The other two are static
-boards (`columns` + inline `data`, no fetch) that author no `objectName`, which
-`ObjectKanbanSchema` declares required — objectui#7780's subject; their casts stay, now
-carrying the reason and the card number.
diff --git a/.changeset/7322-object-kanban-group-by-limit.md b/.changeset/7322-object-kanban-group-by-limit.md
deleted file mode 100644
index c386047d0c..0000000000
--- a/.changeset/7322-object-kanban-group-by-limit.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Breaking for authored metadata:** `ObjectKanbanSchema.groupField` is RETIRED
-(objectui#7322, ADR-0049 enforce-or-remove), and the two keys the `object-kanban`
-renderer actually reads — `groupBy` and `limit` — are now DECLARED and validated
-on both published faces: the TypeScript interface in `objectql.ts` and the Zod
-mirror in `zod/objectql.zod.ts`.
-
-**What was measured, on this branch's base (`53ded82b`).**
-`packages/plugin-kanban/src/ObjectKanban.tsx` reads `schema.groupBy` at thirteen
-sites (lane materialisation at `:601` / `:625` / `:640`, card moves at `:747` /
-`:865`, and their effect deps) and `schema.limit` at two (`:264`,
-`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`, and the effect deps at `:291`).
-`groupField` has ZERO read sites anywhere under `packages/plugin-kanban/` —
-against a control of those thirteen `groupBy` reads in the same query, so the
-zero is a reading, not a blind grep. Yet the declaration REQUIRED `groupField`
-and declared neither `groupBy` nor `limit`. Measured from source: the
-documented, tested, working shape — `{ type: 'object-kanban', objectName,
-groupBy, limit }` — failed `ObjectKanbanSchema.safeParse` and
-`safeValidateSchema` on the missing `groupField`, and only ever reached the
-renderer through `BaseSchema`'s `[key: string]: any` and `.passthrough()`,
-admitted unexamined. An author who followed the declaration wrote `groupField`
-and got a board that grouped nothing, with no diagnostic on either face.
-
-**Who is affected — an `object-kanban` node authoring `groupField`:**
-
-```json
-{ "type": "object-kanban",
- "objectName": "task",
- "groupField": "status" } // ← validated, compiled, grouped nothing
-```
-
-now fails validation at `groupField` with:
-
-> RETIRED (objectui#7322) — `groupField` is not read by the object-kanban
-> renderer; author `groupBy`. (The view-level `kanban.groupField` alias is
-> unaffected.)
-
-and is refused at compile time: `groupField?: never`. The tombstone is
-load-bearing rather than decorative — `BaseSchema` carries `[key: string]: any`,
-so DELETING the member would let the retired spelling type-check green and go on
-doing nothing. **Migration:** rename the key to `groupBy`; the value — the field
-whose value places a record in a lane — is unchanged.
-
-**What now validates that did not before:** the documented shape. `groupBy` is
-REQUIRED (the retired contract required a lane field too; the renderer's
-`if (!schema.groupBy)` branches are defensive early-returns, not a lane-less
-mode, and every documented and tested `object-kanban` node authors the key) and
-must be a string; `limit` is an optional positive integer. A wrong-typed
-`groupBy: 42` or `limit: "twenty"` — previously admitted unexamined — is now
-refused at its own path.
-
-**Who is NOT affected.** The VIEW-LEVEL kanban config is untouched:
-`kanban.groupField` there is a live legacy alias of the spec's `groupByField`
-(`packages/core/src/utils/normalize-list-view.ts` maps it; `plugin-list`'s
-`ListView` and `plugin-view`'s `ObjectView` still read it). `groupField` is dead
-only on the `object-kanban` NODE. The declarative `kanban` node (`KanbanSchema`)
-is untouched, `BaseSchema`'s unknown-key policy is byte-identical (an undeclared
-key still passes through), and the renderer is unchanged — boards authored the
-documented way rendered before and render now. One in-repo fixture authored
-`groupField` on this node (`packages/types/src/__tests__/kanban-conditional-formatting.test.ts`);
-it now authors `groupBy`. No doc snippet, catalog entry, skill or app in this
-repository authored `groupField` on an `object-kanban` node.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the retired key. It is not `major` per this
-repo's fixed-group convention (objectui's own breaking changes ship as `minor`;
-the group's major tracks `@objectstack` — AGENTS.md 版本号策略, mechanically
-enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/7323-adapter-factory-return.md b/.changeset/7323-adapter-factory-return.md
deleted file mode 100644
index 2f4925d08a..0000000000
--- a/.changeset/7323-adapter-factory-return.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-'@object-ui/data-objectstack': minor
----
-
-`createObjectStackAdapter` declares the adapter it returns, not the shared `DataSource`
-interface (objectui#7323).
-
-The factory returned `new ObjectStackAdapter(config)` while declaring `DataSource`.
-A wider value is assignable to a narrower annotation, so nothing ever failed to compile
-— the loss was entirely on the reading side. Measured against the shipped
-`dist/index.d.ts` with the doc-snippet gate's own compiler options, nine reads through
-`ReturnType` failed with TS2339: `getClient`,
-`getCacheStats`, `invalidateCache`, `clearCache`, `getConnectionState`, `isConnected`,
-`onConnectionStateChange`, `onBatchProgress` and `setSystemCapabilities`. Eight of those
-nine reads are on this package's README API Reference list, and four whole README
-sections are built on them; the ninth measured read is the one the factory's own JSDoc
-links to (`[ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}`). The README
-list is itself **nine** adapter-only members, not eight — `connect()` is adapter-only
-too and was documented all along; it simply was not one of the reads the card's
-reproduction measured. So the file's own doc comment pointed the reader at a method its
-declared return hid, and the two documented ways to obtain the same object — the factory
-and `new ObjectStackAdapter(…)` — handed back different type surfaces.
-
-**What the declared return now is: the whole class, not those nine reads.** The nine
-above are what the reproduction measured, not the size of this change. The factory's
-declared return is now `ObjectStackAdapter` itself, so **every public member of the
-class** is part of what the factory promises. Against `DataSource` that is **20**
-members, not nine — `tsc`-computed as
-`Exclude, keyof DataSource>`: `clearCache`,
-`connect`, `getCacheStats`, `getCached`, `getClient`, `getConnectionState`,
-`getDiscovery`, `getItems`, `invalidateCache`, `invalidateViewKeys`, `isConnected`,
-`listImportMappings`, `onBatchProgress`, `onConnectionStateChange`, `onSaveAdvisory`,
-`onWriteWarning`, `probeAppAccess`, `queryDataset`, `setSystemCapabilities`,
-`updateDashboard`. The eleven past the documented nine were already in the shipped class
-type — none is `@internal` or `@deprecated`, `stripInternal` is not set, and all were
-already reachable through `new ObjectStackAdapter(…)` and through every
-`ObjectStackAdapter`-typed seam in `@object-ui/react` and `app-shell` — so what widens
-here is what the **factory declares**, not what the package ships. Two are escape-hatch
-shaped and worth knowing before building on them: `getCached(key)` is a raw cache read,
-and `getDiscovery()` reaches an internal property of the underlying `ObjectStackClient`.
-
-**Branch taken: A (widen the factory's declared return), and why.** The card offered
-three. B — moving caching, connection state and batch progress onto `DataSource` — was
-rejected because those are this adapter's concerns, not every data source's; every other
-`DataSource` implementation would then declare members it does not have. C — documenting
-a cast — teaches a cast around a declaration that is merely narrower than the value,
-which is the opposite of `declared = enforced`. A is one line and makes declared match
-shipped for every documented member at once.
-
-Two questions decided the shape and both were answered from the code before the diff.
-`ObjectStackAdapter` was **already** exported from the package's only entry
-(`src/index.ts`, tsup's single entry; the class is in the shipped `dist/index.d.ts`
-export list, two pin tests assert the exported spelling, and `apps/console` re-exports it
-by name) — so widening the return exports nothing by implication. And the narrow return
-was **not** a deliberate swappability guarantee: no comment, ADR or test pinned it, and
-the commit that added `autoReconnect` / `maxReconnectAttempts` / `reconnectDelay` to the
-factory's own config bag left the members that observe those features off the factory's
-declared return in the same change.
-
-**One caller shape breaks: a structural stand-in for the factory's return.** A
-hand-written object literal annotated `ReturnType` no
-longer satisfies that type, because it is now a class with private members (TS2740) —
-annotate such a fake as `DataSource` instead, which is what it was standing in for.
-Nothing else moves: a wider return is assignable to the narrower annotation, so
-`const ds: DataSource = createObjectStackAdapter(…)` keeps compiling and keeps giving
-the narrow surface to anyone who wants it.
-
-The README's note saying the page could not yet teach the factory's shape is removed, and
-the four sections built on the adapter-only members (Metadata Caching, Connection State
-Monitoring, Batch Operation Progress, Troubleshooting → Cache Issues) now continue from
-Basic Setup's `createObjectStackAdapter(…)` call instead of declaring the class by hand.
-The docs-site page `content/docs/utilities/data-objectstack.mdx` is corrected the same
-way: its prose, its factory signature fragment and its "hold the class type to reach
-these" section described the old narrow return, and its Mutations and Troubleshooting
-examples told the reader to construct the class by hand to reach members the factory now
-declares. `src/adapterFactoryReturn.types.test.ts` pins the card's TS2339 reproduction
-inverted,
-with two controls: the adapter-only members stay absent from `DataSource` (fires on
-option B), and the widened return stays assignable to `DataSource` (swappability kept).
diff --git a/.changeset/7324-plugin-form-nameable-parameter-types.md b/.changeset/7324-plugin-form-nameable-parameter-types.md
deleted file mode 100644
index fccfe94b6b..0000000000
--- a/.changeset/7324-plugin-form-nameable-parameter-types.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/plugin-form': minor
----
-
-Publish the parameter types of the entry's own exported functions, so a consumer can
-name what it must pass (objectui#7324).
-
-`ChildObjectSchemaLike` and `FieldDefaultsSchemaLike` are now exported from
-`@object-ui/plugin-form`. They are **type-only** additions — no runtime name is added
-to the entry, which is pinned.
-
-**Why `minor`, not `patch`.** Nothing breaks and no behaviour changes, but two names
-join the published surface of a published package. Additions are `minor` in this repo,
-and a new public export is the kind of addition a consumer's lockfile-pinned range
-should be able to see.
-
-**What was wrong.** Five exported derive functions (`deriveDetail`, `deriveColumns`,
-`deriveFormFields`, `findRelationshipField`, `resolveInlineMode`) take a `childSchema`,
-and the exported `omitServerResolvedDefaults` takes an `objectSchema` — and neither
-parameter type reached the entry. A host with its own form renderer (the reason
-objectui#6059 published `omitServerResolvedDefaults` in the first place) has to hold
-that schema in a variable or a prop, and could not annotate it. Structural typing means
-such a host still compiled by writing the shape out by hand, so the cost was not a hard
-failure but a producer-owned shape restated in every consumer, invisible to every gate
-until the producer's shape moved. The package README carried exactly that restatement,
-and now imports the real name instead.
-
-**Renamed at the declaration site first, deliberately.** Both types were called
-`ObjectSchemaLike`, in two files, and they are **not** the same type: the defaults one
-pins the four field members its rule reads (`defaultValue`, `type`, `reference`,
-`reference_to`), while the child one leaves a field value as `any` because the derive
-functions read much more of it. Measured with `tsc`, they are mutually assignable
-**only** through that `any` — replace it with `unknown` and the child → defaults
-direction fails (TS2322) — so re-exporting either under the shared name would have put
-a name on the public surface that already meant something else two files over, with
-nothing in the name to say which. Neither old name was reachable from outside the
-package (the package `exports` map has a single `.` entry and the entry never re-exported
-them), so the rename is not a break for any consumer.
-
-**Not** `@object-ui/types`' `ObjectSchemaMetadata`: measured, it requires `name`,
-requires a `type` on every field, and has no `reference_to` member — while
-`isCurrentUserSeedField` honours both `reference` and `reference_to` on purpose. Adopting
-it would have narrowed what these functions accept and dropped one of the two honoured
-spellings, not widened anything.
diff --git a/.changeset/7337-i18n-covered-specifier-flip.md b/.changeset/7337-i18n-covered-specifier-flip.md
deleted file mode 100644
index d5e1eff94b..0000000000
--- a/.changeset/7337-i18n-covered-specifier-flip.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Finish the `@object-ui/i18n` slice of the `check-vi-mock-inherit` ratchet: the
-last frozen factory (`DeclaredActionsBar.test.tsx`) obtains and spreads the real
-module, and the specifier joins `COVERED_SPECIFIERS`, so the gate now holds the
-population at zero instead of a pin file doing it by hand (objectui#7337). Test
-and CI-script only; no published behaviour changes and no package is released by
-this change.
diff --git a/.changeset/7337-vi-mock-i18n-inherit.md b/.changeset/7337-vi-mock-i18n-inherit.md
deleted file mode 100644
index 844a186fb0..0000000000
--- a/.changeset/7337-vi-mock-i18n-inherit.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Sweep the `vi.mock('@object-ui/i18n', …)` factories that hand-list the mock's
-export surface over to the obtain-and-spread form, and fix the
-`check-vi-mock-inherit` recogniser's nested-generic blind spot
-(objectui#7337). Test and CI-script only; no package is released by this
-change.
diff --git a/.changeset/7340-docs-retired-handler-keys.md b/.changeset/7340-docs-retired-handler-keys.md
deleted file mode 100644
index 565c5caa64..0000000000
--- a/.changeset/7340-docs-retired-handler-keys.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
----
-
-Docs-only: ten rows across eight `content/docs` pages still listed one of
-objectui#6124's 22 RETIRED `on*` handler keys as a callable prop, after PR
-#7339 turned those keys into `?: never` tombstones with named refusal arms on
-the zod mirror. Seven pages have the rows removed;
-`components/basic/button-group.mdx` keeps its `ButtonGroupButton.onClick` row
-spelled `never` with the node-type pointer, because
-`button-group-doc-surface-6347.test.ts` asserts set equality between that block
-and the mirror's `.shape` and a refusal arm is still a key of that shape. A new
-test-only pin
-(`packages/types/src/__tests__/component-docs-retired-handler-keys-7340.test.ts`)
-measures the retired population off `packages/types/src` and holds every doc row
-whose `(interface, key)` pair resolves to a tombstone to the `never` spelling,
-with six runtime-slot rows as the blanket-sweep control (objectui#7340). No
-published package behaviour changes.
diff --git a/.changeset/7344-handler-string-any-mirrors.md b/.changeset/7344-handler-string-any-mirrors.md
deleted file mode 100644
index 47fb1a24f5..0000000000
--- a/.changeset/7344-handler-string-any-mirrors.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/types': minor
----
-
-The eight `on*` handler keys PR #7339's census could not see now refuse by name
-(objectui#7344 — the objectui#6182 ruling of 2026-08-25 that the handler-expression
-string dialect is not a supported authoring form, executed in the objectui#6124 shape).
-
-**The accept set of published validators moves** (`@object-ui/types/zod`):
-
-- Four mirrors declared the string dialect (`z.string()`) — `AppActionSchema.onClick`,
- `ReportBuilderSchema.onSave` / `.onCancel`, `DetailViewSchema.onBack` — so an authored
- `onBack: 'goBack'` parsed green and then reached a slot that CALLS it
- (`DetailView.handleBack`), throwing `onBack is not a function` at click.
-- Three declared `z.any()` — `ActionSchema.onClick`, `DetailSchema.onBack`,
- `CRUDDialogSchema.onClose` — wider than the callable the TypeScript face declares, so
- any JSON value parsed green (the objectui#7069 direction).
-- One, `CalendarViewSchema.onEventClick`, was `z.function()` in a multi-line spelling the
- anchored census missed.
-
-All eight now carry `handlerKeyRefusal(key, disposition, label)`: an authored string, an
-authored object and a live function are each refused at the key's own path with
-`code: 'custom'` and a message that names the key, says why JSON cannot author it and
-points at the node-type spelling. Nothing that used to be refused parses green.
-
-**The TypeScript face, measured per key** — a function type only where a runtime consumer
-reads a function, else `?: never`:
-
-- Runtime slots (callable kept): `DetailViewSchema.onBack` (now `() => void`, the prop
- `DetailView` invokes — it declared `string`), `DetailSchema.onBack`, `ActionSchema.onClick`,
- `CalendarViewSchema.onEventClick`.
-- Retired (`?: never`): `AppAction.onClick` (no reader touches `onClick`, on the action
- or on `items[]` — `AppComponentSchema.actions[]` itself IS read, by
- `@object-ui/runner`'s `LayoutRenderer`, so the array being unread was never the
- reason this key is inert; corrected at objectui#7721),
- `ReportBuilderSchema.onSave` / `.onCancel` (no `report-builder` renderer is registered),
- `CRUDDialogSchema.onClose` (no `crud-dialog` renderer is registered).
-
-The three `views.zod.ts` `z.string()` keys that are event NAMES (`onViewChange`, the two
-`onChange`, PR #6899) are untouched; their describe text says so and the new pin reads it.
-`zod-mirror-parity.test.ts` records the three new runtime-slot drift rows;
-`content/docs/core/app-schema.mdx` spells its `AppAction.onClick` row `never` and drops the
-string example, the two edits the #7340 docs pin and `check:doc-snippets` require.
diff --git a/.changeset/7345-master-detail-outcome-toast.md b/.changeset/7345-master-detail-outcome-toast.md
deleted file mode 100644
index 692b9175e3..0000000000
--- a/.changeset/7345-master-detail-outcome-toast.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/plugin-form': patch
----
-
-A master-detail form no longer ends on a screen asserting both a failure and a success
-(objectui#7345).
-
-`MasterDetailForm` raised its two save outcomes — `handleSaved`'s confirmation and
-`handleError`'s refusal — under sonner's auto-generated ids, so nothing held a handle on
-the previous attempt's toast. A save the server refused left its error toast on screen,
-and when the user corrected the input and saved again inside that toast's lifetime the
-confirmation landed *beside* the refusal, exactly the objectui#7252 defect on a renderer
-that fix did not touch.
-
-Both outcomes now travel under one stable per-form id (`React.useId()`-scoped, the same
-spelling the form renderer and the console's `FormPage` publish under), and each save
-attempt retires the previous attempt's toast before it starts:
-
-- with no host `onSuccess` (SDUI / embedded hosts), the confirmation supersedes the
- refusal instead of stacking beside it;
-- with a host `onSuccess` (the console), where the built-in confirmation is deliberately
- skipped, the dismissal is what retires the refusal — otherwise it stood over a save
- that had succeeded.
-
-Toast durations are unchanged: this is about supersession, not lifetime.
diff --git a/.changeset/7352-drill-down-config-mirror.md b/.changeset/7352-drill-down-config-mirror.md
deleted file mode 100644
index b3d40dd2e0..0000000000
--- a/.changeset/7352-drill-down-config-mirror.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`DrillDownConfigSchema` is the zod mirror of `DrillDownConfig`, and both
-declarations that carry `drillDown` reference it — `ChartSchema`
-(`zod/data-display.zod.ts`) and `ObjectDataTableSchema` (`zod/objectql.zod.ts`)
-— so the published validator under `@object-ui/types/zod` reads the key for the
-first time (objectui#7352).
-
-`DrillDownConfig` has been declared on the TypeScript face since objectui#6058
-seeded the parity ledger, and objectui#6576 declared it on a second type. No zod
-mirror existed, so under `BaseSchema`'s `.passthrough()` a
-`drillDown: { enabled: 'yes' }` parsed green and rode through to a widget that
-reads `enabled` as truthy — `declared !== enforced` on a published surface.
-
-Accept-set change on the published validator, stated plainly:
-
-- NARROWS: a `drillDown` whose declared key holds a value outside its declared
- type (`enabled: 'yes'`, `mode: 'jump'`, `maxRows: '50'`, `report: 'pipeline'`)
- is now refused BY NAME on a `chart` or `object-data-table` node, where it
- previously rode through untouched.
-- Unchanged: every value the TypeScript declares still validates, including the
- `{ enabled: true }` / `{ enabled: true, mode: 'record' }` blocks the dashboard
- renderer synthesises, `report`'s two structural forms, and an inline report's
- extra keys (the declaration's index signature is `.catchall(z.unknown())`).
-- Output shape, worth knowing before you read a parsed `drillDown.report`: the
- member is a union, and its two arms differ in what they KEEP. The inline arm
- carries the declaration's index signature as `.catchall(z.unknown())`, so extra
- report keys survive; the named-reference arm is a plain object, so a value that
- reaches it keeps only `name` (`{ name: 'x', columns: [] }` is accepted, and
- parses to `{ name: 'x' }`). Both were accepted and unvalidated before, and
- neither is refused now.
-- Unchanged: `PivotTableSchema.drillDown` has no zod mirror at all, and
- `DataTableSchema` declares the key on neither face — both are untouched here.
-- New export on `@object-ui/types/zod`: `DrillDownConfigSchema`.
-
-`DrillDownConfigSchema` is deliberately NOT `@objectstack/spec/ui`'s
-`ChartDrillDownSchema`: that object models the chart-only subset strictly and
-refuses `mode` and `report` by name, both of which are live keys on the table /
-pivot / metric widgets that share `DrillDownConfig`.
diff --git a/.changeset/7363-objectql-union-arms.md b/.changeset/7363-objectql-union-arms.md
deleted file mode 100644
index b660f6a8f2..0000000000
--- a/.changeset/7363-objectql-union-arms.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ObjectGallerySchema` and `ObjectDataTableSchema` are members of
-`ObjectQLComponentSchema` on both faces — the TS union in `objectql.ts` and the
-zod union in `zod/objectql.zod.ts` — so `AnyComponentSchema`, and with it
-`validateSchema` / `safeValidateSchema` / `objectui validate`, has an arm for
-`object-gallery` and `object-data-table` nodes (objectui#7363).
-
-PR #7355 (objectui#6576) minted both schemas beside the other `Object*Schema`
-members and deliberately left the unions alone. Until now a document carrying
-either node was refused as matching NO arm — exactly as before the schemas
-existed — and a wrong-typed declared key on it could never be diagnosed by name.
-
-Accept-set change on the published validator, both directions, stated plainly:
-
-- WIDENS: a well-formed `object-gallery` / `object-data-table` node now
- validates instead of being refused for having no arm.
-- NARROWS in effect: a malformed one (`searchable: 'yes'`, `imageField: 42`,
- `onRowClick` authored as JSON) is now refused BY NAME by the arm, where it was
- previously refused only as "no arm matches".
-- TS face: `Extract< ObjectQLComponentSchema, { type: 'object-gallery' } >`
- resolves to `ObjectGallerySchema` instead of `never`; likewise for
- `object-data-table`. `SchemaByType` has no in-repo consumer, and the wider
- `AnySchema` union already carried `BaseSchema`, so nothing narrows there.
-
-No renderer behaviour changes; both nodes rendered before and render the same.
diff --git a/.changeset/7368-studio-package-list-error-posture.md b/.changeset/7368-studio-package-list-error-posture.md
deleted file mode 100644
index a929e84b36..0000000000
--- a/.changeset/7368-studio-package-list-error-posture.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio top bar: a failed package-list fetch is reported and told apart from the
-other two states it used to look exactly like (objectui#7368).
-
-`PackageSwitcher` held the list as `PkgEntry[] | null` and caught the
-`fetchPackages()` rejection into an empty block, so `null` meant both "still
-loading" and "the fetch failed". The trigger rendered `current?.name ?? packageId`,
-which collapses a third situation into the same pixels: a package whose producer
-declared no name at all (`parsePackages` falls `name` back to the id). Any failure
-of `GET /api/v1/packages` therefore left the Studio top bar printing the raw
-reverse-domain package id — `app.b2r4` — with no toast, no console line and no
-retry, forever, and the author had no way to tell whether to go fix the manifest or
-to go retry.
-
-The switcher now records the failure beside the list and renders the three states
-apart: `loading` keeps a spinner next to the id, `failed` adds a "Failed to load"
-marker carrying the error on its tooltip and replaces the popover's forever-"Loading…"
-line with the reason, and `loaded` leaves the id standing alone — where a bare id now
-really does mean "this package declares no name". The two sibling `fetchPackages()`
-callers on the same surface (the writability courtesy gate and the object-namespace
-lookup) stop swallowing their rejections too; all three report on one shared sonner
-id, so a single outage produces one toast rather than three.
-
-The degradation itself is deliberately kept — the switcher still navigates and one
-503 does not take the top bar down — and no retry was added: retry policy (how many,
-what backoff, what to show after giving up) is undecided, and retrying would delay
-the moment the failure becomes visible, which is the opposite of what this fixes.
diff --git a/.changeset/7371-ai-usage-indicator-weekly-reset.md b/.changeset/7371-ai-usage-indicator-weekly-reset.md
deleted file mode 100644
index 37fa3b379a..0000000000
--- a/.changeset/7371-ai-usage-indicator-weekly-reset.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/i18n': patch
----
-
-Fix `AiUsageIndicator` to recognize the free plan's new `resetKind: 'weekly'` and its
-`resetsAt` (objectui#7371, consumer of cloud PR #1852's rolling 7-day AI quota window).
-
-Before this change a `weekly` meter fell through to the component's unrecognized-kind
-path and rendered no reset line at all — not a crash, but silently wrong information
-next to a live progress ring. The indicator now shows "Resets in N days" (or "Resets in
-N hours" once inside the final day, e.g. `console.ai.usage.resetsWeeklyHours`), computed
-from the endpoint's `resetsAt`, in both languages via `@object-ui/i18n`
-(`console.ai.usage.resetsWeeklyDays` / `resetsWeeklyHours`, real i18next plural families
-with a base key so every locale pack resolves correctly, all ten packs translated). D5 is
-preserved — no token count is ever rendered, only the days/hours until reset.
-
-Contract-first: `resetsAt` is read verbatim from the endpoint, never re-derived or
-guessed client-side. A `weekly` meter with `resetsAt: null` (nothing counted yet in the
-window) and any `resetKind` this build does not recognize both render no reset line —
-fail-soft, not a crash or stale copy.
-
-`AiUsageResetKind` (`packages/app-shell/src/hooks/useAiUsage.ts`) gains the `'weekly'`
-member; `resetsAt` was already `string | null` and needed no shape change.
diff --git a/.changeset/7397-remove-duplicate-chart-primitives.md b/.changeset/7397-remove-duplicate-chart-primitives.md
deleted file mode 100644
index 27dee2d1c6..0000000000
--- a/.changeset/7397-remove-duplicate-chart-primitives.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-'@object-ui/components': minor
----
-
-BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`,
-`ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`,
-`ChartStyle` and the `ChartConfig` type — are removed. `@object-ui/plugin-charts`
-is the single implementation (objectui#7397, maintainer ruling 2026-09-04).
-
-**Migration: import the chart primitives from `@object-ui/plugin-charts`.**
-
-(The bump is `minor` by this repo's release model — objectui's major is pinned to
-the `@objectstack` family major, and its own breaking changes ship as `minor` with
-the break spelled out here, per `scripts/check-changeset-no-major.mjs`. This
-paragraph is that spelling-out: the break below is real and consumer-visible.)
-
-- **What breaks, by specifier**: `import { ChartContainer, ChartTooltip,
- ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } from
- '@object-ui/components'` and `import type { ChartConfig } from
- '@object-ui/components'` no longer resolve — TS2305 at build time, `undefined` at
- runtime. They were reachable through two `export *` hops (`src/index.ts` →
- `./ui` → `./chart`), so this is a real removal from the published surface, not a
- tidy-up of dead code.
-- **Not affected**: `ChartSkeleton` — the chart-area loading placeholder in
- `src/custom/view-skeleton.tsx` — is a different symbol and stays exported.
- `@objectstack/spec/ui` still owns the authored-chart `ChartConfig`; only the
- per-series style map published from this package is gone. `@object-ui/plugin-charts`
- calls its own map `ChartContainerConfig`, so the two names no longer collide.
-- **Why the copy had to go rather than be fixed in place**: it duplicated
- `packages/plugin-charts/src/ChartContainerImpl.tsx` and carried the
- label-resolution hole objectui#7248 had already fixed there. `ChartLegendContent`
- resolves a label as `config[nameKey || item.dataKey || 'value']` while rendering
- the colour swatch unconditionally, so a legend entry whose config lookup misses
- paints an anonymous coloured dot — on a scatter that reads as a data point drawn
- outside the plot area, which is exactly how objectui#7248 was reported. Two copies
- of one primitive is how a fixed bug returns; consumers importing from
- `@object-ui/components` were getting the unfixed one.
-- **Why not re-export the plugin's copy from here instead**: `@object-ui/plugin-charts`
- depends on `@object-ui/components` (`workspace:*`), so the dependency direction
- forbids it.
-- **Consumer census**: zero in-repo importers, measured with a lit control — no file
- under `apps/**`, `examples/**` or `packages/**` imported any of these names from
- `@object-ui/components`. `plugin-charts` reaches its own copy by relative path.
- The `hotcrm` and `cloud` repositories could not be read from the seat that made
- this change (HTTP 403), so no claim is made about them.
-
-`packages/components/shadcn-components.json` records `chart` under
-`customComponents` with `movedToPlugin: "@object-ui/plugin-charts"`, which is what
-keeps `pnpm shadcn:update-all` from re-fetching the primitive and silently undoing
-this.
diff --git a/.changeset/7401-chart-family-from-type.md b/.changeset/7401-chart-family-from-type.md
deleted file mode 100644
index 5c31009704..0000000000
--- a/.changeset/7401-chart-family-from-type.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/plugin-charts": patch
----
-
-fix(plugin-charts): `pie-chart`, `donut-chart`, `radar-chart` and `scatter-chart` render as the family they name
-
-A schema written as `type: 'pie-chart'` (or `plugin-charts:pie-chart`, and likewise donut / radar / scatter) drew a **bar chart**. The four registrations declared their family as `defaultProps: { chartType: … }`, and nothing on the SDUI path has ever read a registration's `defaultProps` — so `ChartRenderer` resolved no family and `AdvancedChartImpl` fell to its `'bar'` default. Valid data, a confidently wrong picture, and no `data-chart-error` that could fire.
-
-`ChartRenderer` now derives the family from the schema's own `type`, through `normalizeChartSchema` — the package's single translation point, so the exported `normalizeChartSchema` answers what the runtime actually draws. An explicit `chartType` still wins, so `plugin-charts:chart` with `chartType: 'scatter'` is unchanged.
-
-The five inert `defaultProps: { chartType: … }` are removed with it rather than left beside a mechanism that works. Registration `defaultProps` remains unread on the SDUI path repo-wide; activating it generally is a separate, wider change and is not this one.
-
-⚠️ `scatter-chart` now genuinely reaches the scatter arm, so a two-series `scatter-chart` now renders the `scatter-multi-series` refusal it was always supposed to.
diff --git a/.changeset/7402-scatter-ignores-compareto.md b/.changeset/7402-scatter-ignores-compareto.md
deleted file mode 100644
index b4fe1206b2..0000000000
--- a/.changeset/7402-scatter-ignores-compareto.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@object-ui/plugin-charts': minor
-'@object-ui/plugin-dashboard': minor
----
-
-`compareTo` on a `scatter` chart is no longer supported — scatter joins pie / donut /
-funnel on the list of chart families that ignore it (objectui#7402, maintainer ruling
-2026-09-03).
-
-**This removes a published capability, deliberately.** Until now a `chartType: 'scatter'`
-chart (and the dashboard widget types `scatter` and `bubble`, which both render as one)
-with `compareTo` set synthesised a muted "previous period" overlay series. It drew the
-wrong picture: a scatter binds ONE measure, and the renderer reads y through the single
-`YAxis dataKey={series[0].dataKey}`, so the overlay was plotted on the PRIMARY series' y
-— "previous period" painted exactly on top of "current" (objectui#7194).
-
-Enforce-or-remove: rather than keep drawing that, the capability is removed until it can
-be drawn honestly. Drawing a real second measure on a scatter needs the multi-measure
-projection recorded as option A of objectui#7194, which is not built (zero authored
-callers). **If and when that projection lands, `compareTo` on a scatter returns with
-it** — it is the same missing mechanism, one payment.
-
-What changes for authors:
-
-- A `compareTo` on a scatter is now IGNORED rather than drawn. The primary series still
- renders exactly as before — nothing refuses, nothing goes blank, and no comparison
- query is issued on the inline chart path.
-- No `__comparison` (inline chart) / `__compare` (dashboard) series is
- appended for a scatter, so a compare-to scatter document also never reaches the
- two-or-more-series scatter refusal being added under objectui#7194.
-- Charts that keep the overlay: line, area, bar, horizontal-bar, combo. Charts that
- ignore `compareTo`: pie, donut, funnel and — as of this change — scatter (and the
- `bubble` widget type that renders as a scatter).
-
-Reachability at the time of the change: **0** authored scatter/bubble instances in-repo
-across both spellings (control `"type": "bar"` fires at 5 example files); incidence in
-deployed tenant metadata is not measurable from this repo.
diff --git a/.changeset/7415-action-type-input-rename.md b/.changeset/7415-action-type-input-rename.md
deleted file mode 100644
index 9c1923125e..0000000000
--- a/.changeset/7415-action-type-input-rename.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'@object-ui/components': minor
----
-
-**Renamed:** the declared input carrying the action's execution type on `action:button` and
-`action:icon` is now `actionType`. It used to be `type`, which collides with the SDUI
-envelope's component discriminator. **No alias and no transition window** — a declaration
-still spelling it `type` sets the discriminator, not the input.
-
-Implements objectstack#14490 ruling A (maintainer, 2026-09-02, decision batch #13 item 4,
-verbatim 「同意」). The objectui half; the pinned `sdui.manifest.json` in objectstack follows
-as a separate change.
-
-**Why the old name could not stay.** `action:button` and `action:icon` were the only two of
-the manifest's 57 components declaring an input named `type`, and on no tier could an author
-actually set it:
-
-- **html tier** — `parse.ts` composes a node as `{ type: tag, ...props }`, props spread
- last, so `type="api"` replaced the component discriminator and the node stopped resolving
- to a component at all. `validate.ts` cannot report that: `type` is in `BASE_PROPS`, so it
- is skipped before the declared-input check runs. Two mechanisms, one outcome, no
- diagnostic. objectstack PR #14274 landed a refusal on this tier whose prescription
- ("write the tag you meant") is wrong for exactly these two components.
-- **react-page tier** — the wrapper stamps `type: tag` last and parks the author's value
- under `specType` (objectui#2880), which neither action renderer reads.
-- **JSON / `SchemaRenderer`** — the node's `type` is the component id, and the renderer
- forwarded it to `ActionRunner` as the action type. `'action:button'` binds no handler and
- no builtin: the click did nothing, with no error and no toast (the objectui#6306 shape).
-
-`actionType` is not a new vocabulary. It is the spelling this repo already used for this
-exact value: `action:bar` renames the declared type as it spreads a member onto its child
-(`type: componentType, actionType: action.type`), `ActionRunner.execute` resolves
-`action.type || action.actionType || action.name`, and both renderers already read
-`schema.actionType` FIRST. The rename makes the one working spelling the declared one.
-It follows the resolution `page:tabs.type` got upstream for the same carrier collision
-(retired in favour of `tabStyle`, objectstack#6776) rather than inventing a new convention.
-
-**Census of the authored corpus, measured before the rename, not assumed.** Across 4,797
-files in `examples/`, `apps/console/`, `content/docs/` and `packages/`: **zero** authored
-nodes set the input. 517 JSON files (433 of them the schema catalog) contain 2,410
-`type`-bearing nodes and **no** `action:button` / `action:icon` node at all — control: 127
-plain `button` nodes on the same walk. 207 parsed fenced JSON blocks in md/mdx carry **5**
-`action:button` nodes, all of them `{ type, label, icon?, action }` — the discriminator plus
-the `action` channel, none setting an execution type — control: 28 plain `button` nodes, and
-the count independently matches the corpus census already recorded in `SchemaRenderer.tsx`
-("`action:button` (5 nodes)"). No docs page documents the input; no catalog entry uses the
-components. So the rename breaks no authored document in this repo.
-
-**What changes for a consumer.** The renderers no longer fall back to `schema.type` when
-`actionType` is absent — that fallback is the old spelling, and the ruling forbids an alias.
-A node that declares neither now forwards `type: undefined`, and `ActionRunner` falls
-through to its own `action.name` leg instead of being handed a component id. Declarations
-composed by `action:bar`, `action:group` and `action:menu` are unaffected: they carry the
-spec `ActionSchema.type` inside an `actions` array, which is a different surface and is
-unchanged.
-
-Pinned by `action-type-input-html-tier.test.tsx` (the html tier authors the renamed input
-end to end; the manifest built from the live registry accepts `actionType` and reports a
-bogus prop as the control) and by the rewritten standalone rows in
-`action-bar-member-type-resolution.test.tsx`, one of which now fails if the `|| schema.type`
-leg is ever added back.
diff --git a/.changeset/7421-gantt-tasklistwidth-tombstone.md b/.changeset/7421-gantt-tasklistwidth-tombstone.md
deleted file mode 100644
index e772d8ebc2..0000000000
--- a/.changeset/7421-gantt-tasklistwidth-tombstone.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
----
-
-Removes the `taskListWidth_LEGACY_REMOVED` tombstone from `GanttView`, and records the fact
-it was carrying at the site that owns it.
-
-The line was one `const` bound to the literal `null` inside the component body, left behind
-by the finished task-list-width refactor. Measured on `origin/main` `a27d153c2`, the
-identifier appeared exactly once in the whole repo — its own declaration — and zero times in
-a read position; the same probe run returned 20 read sites for the live `taskListWidth` and
-360 for `rowHeight`, so the zero is a reading and not a broken grep. Nothing in the platform
-repo declared it either. It carried no state, no side effect and no contract surface.
-
-What it did carry was a false signal. A name spelling `_LEGACY_REMOVED`, sitting inside the
-component a few hundred lines below the real width derivation, reads as a seam deliberately
-retained for a reason recorded elsewhere, so the next reader goes looking for that reason.
-
-Its trailing comment held one fact worth keeping — that the width now comes from the
-container `useResizeObserver` — and that fact was stated nowhere at the observer call site.
-It moves there rather than dying with the line: the observed container width is named as the
-source the auto-sized row height, base column width and task-list pane width all derive
-from. No behaviour changes, and nothing published moves.
diff --git a/.changeset/7429-expand-fls-seven-sites.md b/.changeset/7429-expand-fls-seven-sites.md
deleted file mode 100644
index 16b04154fa..0000000000
--- a/.changeset/7429-expand-fls-seven-sites.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'@object-ui/plugin-kanban': patch
-'@object-ui/plugin-tree': patch
-'@object-ui/plugin-view': patch
-'@object-ui/plugin-map': patch
-'@object-ui/plugin-list': patch
-'@object-ui/plugin-timeline': patch
-'@object-ui/app-shell': patch
----
-
-FLS-gate the `$expand` projection at the seven remaining `buildExpandFields`
-call sites (objectui#7429).
-
-objectui#7215 / PR #7229 gated the two projection sites in its scope
-(`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more
-(`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This
-closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView`
-(the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`,
-`ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch.
-
-**All seven pass no column list at all**, which makes every one of them the
-sharp shape: `buildExpandFields` reads an absent column list as "no column
-restriction" and falls back to **every declared relation on the object**,
-denied ones included. So each of these components asked the server to resolve
-the object's full relation set by default, not by configuration — the
-ordinary shape of each surface, not a corner of it.
-
-**`PagePreview` is the one site where the judged principal is not the page's
-eventual audience.** It calls the browser's own `fetch` with
-`credentials: 'include'` rather than `DataSource.find`, so it runs under
-whichever session is loading the Studio preview. Gating on that same session's
-`usePermissions()` is still the correct principal: it is exactly the request
-the browser is about to make, on its own credentials, regardless of who later
-opens the published page.
-
-**Reproduced before it was fixed**, as a failing test per site (and, for the
-two sites — `ObjectView`, `PagePreview` — where the gate was implemented
-before its test was run red, a reverse-verification: the gate was reverted,
-all four denial-and-set pins on each went red, and the two deferral/positive
-control pins stayed green, before the gate was restored).
-
-**Grading, measured rather than assumed** — the same reading objectui#6898,
-#7215 and #7230 recorded: against ObjectStack's own server this is
-defence-in-depth, not a live disclosure. `plugin-security`'s
-`FieldMasker.maskRecord` deletes every unreadable key from each returned row
-and objectql's expand path writes the resolved record back under that same
-key, so one statement removes the expanded object and the bare id alike; the
-expansion sub-read is itself gated (the referenced object's full CRUD + RLS +
-FLS treatment, objectstack#7626). It is load-bearing for any backend that does
-not strip, and the client-request side is real regardless.
-
-**Nothing a permitted view did stops working.** The gate judges each site's
-`buildExpandFields` OUTPUT, which contains only the object's declared
-reference-bearing fields, so the "`checkField` answers false for an
-undeclared key" trap cannot be reached. An unanswered permission policy
-filters nothing. `buildExpandFields` itself is unchanged.
-
-`@object-ui/permissions` is added to `dependencies` for `plugin-kanban`,
-`plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth
-one objectui#7429's own dependency count missed (it named four); `plugin-list`
-and `app-shell` already had it.
diff --git a/.changeset/7439-approvals-teardown-race.md b/.changeset/7439-approvals-teardown-race.md
deleted file mode 100644
index b0bc5aad13..0000000000
--- a/.changeset/7439-approvals-teardown-race.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Close the network-escape window in
-`RecordDetailView.approvalDeclaredActions.test.tsx` (objectui#7439): the `fetch`
-double is now installed once for the whole file instead of being torn down by an
-unconditional `afterEach`, which used to race the record page's own
-`refreshAfter: true` approvals re-read. Test only; no package is released by
-this change.
diff --git a/.changeset/7443-datetime-compact-style.md b/.changeset/7443-datetime-compact-style.md
deleted file mode 100644
index 70900b838d..0000000000
--- a/.changeset/7443-datetime-compact-style.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/fields': minor
-'@object-ui/components': patch
----
-
-One home for the `datetime` display convention (objectui#7443).
-
-`formatDateTime` gains a named `'compact'` style, selected through
-`options.style` — the dense grid face, `7/4/2024 7:00 am` in `en-US` — which
-`DateTimeCellRenderer` used to build from its own inlined `Intl` option bags.
-The cell now reads `field.format` (it destructured `value` only, so a
-`datetime` field could not reach the style vocabulary a `date` field has) and
-renders through the shared function, and `data-table`'s `formatCellValue`
-calls `formatDateTime` instead of a third, independently authored option bag.
-Every existing cell without an authored `format`, and every cell authoring
-`'compact'`, renders byte-identically; `'compact'` is today's face named and
-rehoused, not a new one. A `datetime` field that authors any OTHER non-empty
-`format` does change: the cell previously ignored `field` altogether and always
-painted the compact face, and now anything other than `'compact'` selects the
-verbose `formatDateTime` default — measured as `Jul 4, 2024, 07:00 AM` in
-`en-US` for the instant whose compact face is `7/4/2024 7:00 am`. An
-unrecognised value is neither rejected nor passed through; it silently lands on
-that verbose face. No `datetime` field in this repository authors a `format`, so
-no cell here moves — a consumer that authored one is the case this sentence is
-for. Note that `format` has no declared value vocabulary to check a value
-against: `@object-ui/types` types it `format?: string`, and `@objectstack/spec`
-carries one free-form `format?: string` on its shared field schema, described
-"Format string (e.g. email, phone)" and accepting any string. `'compact'` is
-therefore the only value with a defined `datetime` meaning, and every other
-value means "the verbose face" by fallthrough rather than by design.
-
-Additive, no signature change: `formatDateTime(value, options?)` is unchanged
-and `formatDateTime(v, { locale })` keeps meaning what it meant.
-`DateDisplayOptions` gains an optional `style` key (read by `formatDateTime`
-only; `formatDate` still takes its style positionally), and
-`formatDateTimeCompactParts` is a new export of `@object-ui/core`, re-exported
-by `@object-ui/fields`, returning the compact face as the two halves a grid
-cell paints separately. `@object-ui/components` changes no rendered output —
-the table's datetime cell is measured identical before and after in `en-US`,
-`zh` and `de-DE`.
diff --git a/.changeset/7454-calendar-allday-spelling.md b/.changeset/7454-calendar-allday-spelling.md
deleted file mode 100644
index 96710753fe..0000000000
--- a/.changeset/7454-calendar-allday-spelling.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
----
-
-fix(plugin-calendar): the all-day lane header reads the same string with or without an I18nProvider
-
-`CalendarView`'s `DEFAULT_TRANSLATIONS` table — the `defaults` map behind its
-`createSafeTranslation` factory — spelled `calendar.allDay` as `all-day`, while
-all ten locale packs carry the key and `en` spells it `All Day`. Since
-`createSafeTranslation` serves that table only when no `I18nProvider` is
-mounted, the same lane header rendered `all-day` in a standalone embed and
-`All Day` inside the console. The table now matches the pack, so both paths
-render one string.
-
-The table entry was the only one of the seven that disagreed with its `en`
-value, and the packs predate it by three months — the drift was an oversight,
-not a compact spelling chosen for the 56px gutter (both strings are seven
-characters wide).
-
-Also removes the dead `t('calendar.allDay') === 'calendar.allDay'` ternary that
-guarded the lane header: the factory's provider-less arm returns `defaults[key]`
-before it could ever return the bare key, and with a provider the merged
-resources always carry `calendar.allDay`, so its lowercase branch was
-unreachable from either side.
diff --git a/.changeset/7459-timeline-absent-date-axis-refusal.md b/.changeset/7459-timeline-absent-date-axis-refusal.md
deleted file mode 100644
index 6488b748ee..0000000000
--- a/.changeset/7459-timeline-absent-date-axis-refusal.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-'@object-ui/plugin-timeline': minor
-'@object-ui/i18n': minor
----
-
-`ObjectTimeline` refuses an undeclared date axis instead of inventing one
-(objectui#7459).
-
-Steps ① and ② of the three-step sequence the maintainer ruled on objectui#7070
-(2026-09-01, 总监批 #28). House posture, on record with that ruling:
-日期轴永不虚构 — a date axis is never fabricated.
-
-`ObjectTimeline` resolved its date axis through five declared bindings and then
-closed the chain with a sixth rung that was a bare literal field name nobody
-has ever declared. A name therefore ALWAYS resolved: for a view that declared
-no axis, every record read a key its object does not carry, every event landed
-in the "No date" bucket, and the screen read as a timeline that had been built
-and simply had nothing in it.
-
-Two changes, shipped together because neither is observable alone:
-
-- **The renderer now refuses.** An object-bound timeline with no declared date
- axis renders a diagnostic naming every binding it accepts —
- `timeline.startDateField`, `timeline.dateField`, `mapping.date`, and the two
- deprecated flat spellings — instead of a chart. The twin of `ObjectGantt`'s
- "Gantt configuration required" screen, in the shape objectui#7070 settled.
-- **The invented sixth rung is gone**, which is the only thing that makes the
- refusal reachable. Added while the floor stood, it would have been dead code;
- retired without the refusal, it would have produced exactly the silent
- "No date" outcome the ruling rejects.
-
-**What changes for an author.** A view that declares a date axis is completely
-unaffected — all five declared spellings resolve exactly as before, and a
-timeline authored from literal `items` is never refused, since its items carry
-their own dates and no field name is read for them. A view that declared no
-axis anywhere, and was rendering an empty-looking timeline, now says so.
-
-⚠️ Both premises were RE-MEASURED on the current tree before anything was
-edited, rather than taken from the card: the renderer had no absent-axis
-refusal (against a live control term that fires in the same file), and the
-floor was still present and still spelled as reported. The pairing itself is
-pinned — the refusal cases go red the moment the floor returns, including one
-whose records carry a column that happens to be named `date`, where a returned
-floor renders a convincing timeline rather than an empty one.
-
-Refusal is distinguished from "renders an empty timeline" by asserting the
-canvas is ABSENT, not merely event-free. The component's success surface is now
-named (`data-testid="timeline-canvas"`) so that distinction can be measured;
-every other terminal state of the component already named itself.
-
-Step ③ of the ruling — the `'created_at'` floors on the two plugin faces —
-stays on objectui#7070 and is deliberately NOT in this change.
diff --git a/.changeset/7466-timeline-colorfieldladder-settlement.md b/.changeset/7466-timeline-colorfieldladder-settlement.md
deleted file mode 100644
index d41d8952b6..0000000000
--- a/.changeset/7466-timeline-colorfieldladder-settlement.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only: `colorsFor` in the timeline `colorField` ladder fixture now returns the
-`lastItems` array its own readiness predicate accepted, instead of re-reading the
-module-level global on the next line. No published behaviour changes — the shared
-`createFieldColorResolver` ladder is untouched.
diff --git a/.changeset/7477-listview-object-provider-fold.md b/.changeset/7477-listview-object-provider-fold.md
deleted file mode 100644
index 39fac8ca2c..0000000000
--- a/.changeset/7477-listview-object-provider-fold.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/core': minor
----
-
-ListView: fold `data={{ provider: 'object', object }}` onto `objectName`, and read the
-author's view kind from `specType` / `type` (objectui#7477 — step 6 of #2890, released
-by the maintainer's ruling B on objectstack#14791, 2026-09-03).
-
-**What was broken.** A react page bound the way the published `react-blocks` contract
-recommends —
-
-```jsx
-
-```
-
-— validated green against `@objectstack/spec` and then rendered an **empty grid** with no
-diagnostic. Both halves of that binding were inert in the renderer: `ListView` read
-`data.provider === 'object'` at zero sites (`'value'` and `'api'` are both live there, so
-the gap was real and not a dead instrument), and it read `specType` — the slot the react
-page tier parks an author's `type` in, because the SDUI envelope claims the `type` key
-(ADR-0078) — at zero sites, so an absent `viewType` forced the view to `grid`.
-
-**What changed.** `normalizeListViewSchema` (`@object-ui/core`) gains two folds. Per
-AGENTS.md #0.1 they live in the one documented normalizer — not as a seventh per-block
-copy of the six sibling `data.object` reads, and not as a renderer-side `??` dual-read.
-
-- `data: { provider: 'object', object }` → `objectName`. The `object` provider is a
- `strictObject` carrying exactly `{ provider, object }`, so `objectName` captures all of
- it. Two deliberate departures from the folds around it, both narrowing: an
- already-present `objectName` **wins** (the fold only fills a gap and can never re-point
- a binding that already resolves), and `data` is **not** deleted — it has four
- providers, `api`/`value` are read live, and the block is forwarded to child views whose
- own `getDataConfig` reads `data` before `objectName`.
-- the author's view kind is read from `specType`, then from a bare `type` when it names a
- kind ListView draws (the component discriminator `'list-view'` never does) — the same
- two legs, in the same order, as `normalizeChartSchema`'s chart-family read. An explicit
- `viewType` still wins; this only fills the gap that used to resolve to `grid`, and a
- kind ListView does not draw is left to that `grid` default rather than written through.
-
-**Accept behaviour widens.** Metadata that previously had no effect now binds a view: a
-list view carrying an `object` data source, or an author `type`, renders differently
-after this change than before. Nothing that renders today renders differently. No
-authored spelling is removed here — `objectName` / `viewType` remain accepted; their
-retirement is objectstack's, after this ships.
diff --git a/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md b/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md
deleted file mode 100644
index 4074b8a982..0000000000
--- a/.changeset/7493-componentinput-retire-label-defaultvalue-advanced.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/core': patch
-'@object-ui/components': patch
-'@object-ui/layout': patch
-'@object-ui/app-shell': patch
-'@object-ui/plugin-ai': patch
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-charts': patch
-'@object-ui/plugin-chatbot': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-designer': patch
-'@object-ui/plugin-detail': patch
-'@object-ui/plugin-editor': patch
-'@object-ui/plugin-form': patch
-'@object-ui/plugin-gantt': patch
-'@object-ui/plugin-grid': patch
-'@object-ui/plugin-kanban': patch
-'@object-ui/plugin-list': patch
-'@object-ui/plugin-map': patch
-'@object-ui/plugin-markdown': patch
-'@object-ui/plugin-report': patch
-'@object-ui/plugin-timeline': patch
-'@object-ui/plugin-tree': patch
-'@object-ui/plugin-view': patch
----
-
-**Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and
-`ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781;
-maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove).
-They are the three keys the manifest serializer does not forward, and nothing read them on any
-publication or consumption path.
-
-No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s
-serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`,
-`type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three
-never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type
-has no slot for them; the registry's data-source seam reads `name` only; and neither the designer
-nor the app-shell inspectors consult registry `inputs` at all. A structural census over every
-`inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type`
-951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on
-nearly every registration, read by nothing.
-
-FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on
-the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an
-undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162
-authored values in silence. The tombstone is what makes the refusal loud and by name.
-
-- `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror.
- Migration: delete the key. An input is identified by its `name` on every path that reaches it;
- nothing ever rendered a label for it.
-- `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the
- key. The renderer's own fallback read IS the default; tell the author about it in `description`,
- which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class,
- since nothing reads the value.)
-- `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key.
- No designer surface ever hid an "advanced" input; there is nothing to write instead.
-
-The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one
-is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema`
-(`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with
-`code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message
-(one string, both channels). Pinned in
-`packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a
-tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`.
-
-Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a
-`ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses
-RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone
-`ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule
-requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized
-`ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT`
-drops its `label`. The patch entries on the other packages record exactly that: their registrations
-stop authoring inert keys, with no runtime or published-manifest change.
-
-The nine test files that read `defaultValue` off a registration were re-pinned against the
-renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the
-declaration that went away; two assertions that only restated the shadow default were dropped with
-the reason on the line.
-
-The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys
-is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop
-into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` /
-`defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has
-ruled on that face; that it now has no reader either is recorded as objectui#7911.
diff --git a/.changeset/7507-non-grid-ceiling-correctives.md b/.changeset/7507-non-grid-ceiling-correctives.md
deleted file mode 100644
index 1d72d6d1f1..0000000000
--- a/.changeset/7507-non-grid-ceiling-correctives.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
-all of them inside a change that has not been released yet — so nothing here is
-a user-visible fix, and the ceiling's own changeset carries the release note.
-
-- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
- `patch` to `minor` (they carry the behaviour break), the example footnote is
- replaced with the copy that actually renders — no thousands separators, since
- the i18next config declares no `format` and the provider-less path
- interpolates through `String(v)` — and the export list names
- `NonGridCeilingResult`.
-- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
- footnote raised by this component's own truncated fetch cannot outlive the
- rows it described. Latent today: the only host that passes `data` passes it
- from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
-- The map and calendar ceiling pins now assert the row count handed to the
- view, not only `$top` and the footnote; the four pins' reverse-verification
- docblocks are rewritten to the mechanism that was measured rather than the
- one that was predicted.
-- Comment-only: the false "the only package all four already depend on"
- rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
- note in the ten locale packs.
diff --git a/.changeset/7509-dashboard-root-title-retired.md b/.changeset/7509-dashboard-root-title-retired.md
deleted file mode 100644
index 93994209bf..0000000000
--- a/.changeset/7509-dashboard-root-title-retired.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-dashboard': minor
-'@object-ui/plugin-designer': minor
----
-
-Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509,
-maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049).
-
-**What changes for an operator.** A stored dashboard whose header came from a legacy
-root `title` now shows its `label`. `label` is the only header source, then the raw
-`name`.
-
-Per surface:
-
-- Console dashboard page (`DashboardView`) — header falls to `label`, then `name`.
-- Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document
- with no `label` now shows no header title at all.
-- The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to
- `label`, then the generic `Dashboard`.
-- Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage`
- heading) — both fall to `label`, then `name` / the generic heading.
-
-**Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name**
-(`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no
-authored dashboard can acquire the key, and what retires is compatibility with documents
-stored before that refusal existed. Until now five surfaces read the legacy spelling
-independently, which meant a legacy document could show one header in the console and a
-different one in the designer. One spelling now answers everywhere.
-
-**Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard
-already carries it and needs no change — it simply starts showing that `label` instead of
-the legacy `title`. A document carrying `title` and no `label` was already invalid; give
-it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found
-9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'`
-component examples that declare no `header`, so none of them rendered a header title
-either before or after.
-
-**Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared**
-key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings,
-the designer's widget-title input and its per-locale write path all behave exactly as
-before. Root and widget arms were separated by receiver, and the retirement's pins carry
-widget-level controls on every surface for that reason.
diff --git a/.changeset/7509-dashboardview-title-comment-corrected.md b/.changeset/7509-dashboardview-title-comment-corrected.md
deleted file mode 100644
index 9d9111956e..0000000000
--- a/.changeset/7509-dashboardview-title-comment-corrected.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
----
-
-Comment-only correction in `app-shell`'s `DashboardView` header block — no type,
-schema, export or runtime path is touched, so nothing releases.
-
-The block over the header's title fallback claimed "Per @objectstack/spec,
-`DashboardSchema.title` is 'the dashboard title displayed in the header'".
-Measured on the installed `@objectstack/spec@17.2.0`, `DashboardSchema` refuses
-`title` **by name** — `unrecognized_keys(title)` at the document root, against a
-spec-legal control (`{ name, label, widgets }`) that parses — and its 20 keys
-spell the display name `label`, not `title`. `header` declares `showTitle` /
-`showDescription` / `actions` only, so it toggles a title and never carries one;
-`{ header: { title } }` is refused too (`unrecognized_keys(title)@header`).
-
-The old wording made `title` read as authorable dashboard metadata, which is the
-expensive way to learn it is not: the save route refuses the whole body with
-`422 INVALID_METADATA` / `unrecognized_keys` before persistence, so an author
-who follows the comment gets no header and a rejected document. The replacement
-says what `title` actually is — the legacy objectui spelling, the same
-legacy-then-canonical pair `DashboardRenderer` reads — and states the 422, which
-is the fact the next reader needs.
-
-It also corrects a second reading the old block invited: `previewSchema` is not
-a host-supplied preview channel. `DashboardView` takes no such prop; the value
-is the view's own widget-pruned copy of `dashboard`, so both arms of `headerSrc`
-read the same stored document.
diff --git a/.changeset/7512-ganttview-unread-residues.md b/.changeset/7512-ganttview-unread-residues.md
deleted file mode 100644
index 04764dea59..0000000000
--- a/.changeset/7512-ganttview-unread-residues.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
----
-
-Internal cleanup in `@object-ui/plugin-gantt`: `GanttView.tsx` dropped three
-declarations (four bindings) that nothing in the tree ever read.
-
-- `const HEADER_HEIGHT = 50;` and `const COLUMN_WIDTH = 100;` — module-level
- constants with zero readers repo-wide. Both were lazy: unread module bindings
- cost nothing at runtime, so removing them changes no behaviour at all.
-- `const [currentDate, setCurrentDate] = React.useState(() => tzShift.now());` —
- a different class of residue. Unlike the two constants this one was **live**:
- every mount allocated a state slot and ran the `tzShift.now()` initializer,
- even though neither the value nor the setter was ever referenced. Removing it
- has a real (if tiny) runtime effect, and no observable one — nothing rendered
- from it and nothing could set it, so no output, prop, or timing a consumer can
- see changes.
-
-No published symbol moves: none of the four were exported, `COLUMN_WIDTH` is not
-the same binding as the local `const COLUMN_WIDTH = 110` that three sibling test
-files declare for themselves, and `tzShift` keeps 27 other readers in the file so
-its import stays live. Nothing to release — declared as a no-bump change.
diff --git a/.changeset/7515-unreferenced-source-gate.md b/.changeset/7515-unreferenced-source-gate.md
deleted file mode 100644
index f9bba023c3..0000000000
--- a/.changeset/7515-unreferenced-source-gate.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-"@object-ui/components": patch
----
-
-Remove `src/ui/toast.tsx`, an unreferenced primitive, and the dependency only it imported
-
-The file was reachable from nothing: no importer anywhere under
-`packages/components/src`, and `ui/index.ts` never carried it, so the barrel's
-`export * from './ui'` did not reach it either. It shipped all the same —
-`dist/ui/toast.d.ts` was in the published tarball — while contributing nothing to
-`dist/index.js` and nothing to the package's export surface. `ui/sonner.tsx`
-(`Toaster`) is the live implementation and is unaffected.
-
-`@radix-ui/react-toast` is dropped from `dependencies` in the same change: the
-removed file was its only importer anywhere in the repository, so it would
-otherwise have stayed a declared dependency of every install with nothing to
-resolve it.
-
-No exported name changes. A consumer who was resolving `@radix-ui/react-toast`
-through this package's dependency was relying on hoisting rather than on a
-declaration, and should declare it directly.
diff --git a/.changeset/7519-retire-mobile-residue-types.md b/.changeset/7519-retire-mobile-residue-types.md
deleted file mode 100644
index 4c5ad7afb2..0000000000
--- a/.changeset/7519-retire-mobile-residue-types.md
+++ /dev/null
@@ -1,76 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/mobile': minor
----
-
-**Removes two published exports.** Retire the `MobileResponsiveConfig` and
-`GestureConfig` types (objectui#7519, ADR-0049 enforce-or-remove). Both names
-are deleted from `@object-ui/types` and from `@object-ui/mobile`, which
-re-exported them — after this release `import type { MobileResponsiveConfig }`
-or `import type { GestureConfig }` from either package is a compile error, not a
-deprecation warning.
-
-Each had exactly one consumer: the `responsive` and `gestures` members of
-`MobileComponentConfig`, which objectui#5942 retired. Re-measured on current
-`main` before anything was deleted, each was a declaration plus the two barrel
-re-exports and nothing else — no type mounted either, nothing extended,
-annotated, cast to or imported them outside the barrels, and the example apps
-and the `objectstack` sibling checkout had zero authors. A value written against
-either could not reach a renderer or a handler by any path. That is the same
-declared-surface-with-no-consumption-path shape as `MobileComponentConfig`
-itself and `MobileOverrides` (objectui#4919) before it, one level down.
-
-Removed outright rather than kept as `?: never` tombstones, measured against
-this package's two-prong discriminator (a tombstone steers authors to a named
-live replacement key, or keeps loud a key the docs taught as working). Prong 1:
-neither has a replacement key — the behaviour they named lives in hooks, and
-`SpecGestureConfig` is a different contract, not a successor. Prong 2: the only
-release-note lines naming either are the objectstack#4115 rename-ledger rows
-and, for `GestureConfig`, the objectui#3363 reclaim note; none taught a
-renderer or dispatcher reading them, and no member carried a published
-`@default` (contrast `triggerIcon`, tombstoned by objectui#7654 on exactly that
-evidence). Structurally there is also no silent-strip hazard for a tombstone to
-guard: whole interfaces go, nothing ever parsed them, and the mobile module has
-never had a `zod/` twin to host a `retirementTombstone()`. The compiler was the
-only channel these names ever had, and the refusal now lives there.
-
-## Upgrading
-
-**No behaviour changes and there is nothing to migrate at runtime.** An object
-authored against either type did nothing before and does nothing now; what
-changes is that the contract no longer claims otherwise, so the mistake surfaces
-at authoring time instead of silently type-checking.
-
-- **You imported a type only** (the only thing that was possible — nothing
- accepted either as a value): delete the import. If you kept a local object
- annotated with it, drop the annotation; it was never passed anywhere that read
- it.
-- **You wanted per-breakpoint layout:** it exists and is not being retired —
- `useResponsive` / `ResponsiveContainer` / `useBreakpoint` in
- `@object-ui/mobile`. `ResponsiveValue` and `BreakpointName` stay exported from
- both packages.
-- **You wanted to bind a gesture to a handler:** `useGesture` in
- `@object-ui/mobile` takes `{ type: GestureType, onGesture, threshold?,
- longPressDuration?, enabled? }`. `GestureType` and `GestureContext` stay
- exported from both packages.
-- **You want a declarative mobile config surface:** that re-enters deliberately
- as designed product surface on its own card, with the renderer that reads it
- landing in the same change as the declaration — not by restoring these
- declarations.
-
-**Do not follow the compiler's suggestion for `GestureConfig`.** Measured against
-the built declarations: `import type { GestureConfig }` from either package now
-fails as TS2724 with `Did you mean 'SpecGestureConfig'?`. That is a lexical
-near-match, not a migration target. `SpecGestureConfig` is the retired
-`@objectstack/spec` `ui/touch` **tuning** record (`{ type, label, enabled,
-swipe, pinch, longPress }`) that `useSpecGesture` reads; it has no `action`
-member and does not bind a gesture to anything. `MobileResponsiveConfig` fails
-as a plain TS2305 with no suggestion from either package.
-
-Marked `minor`, not `major`, per this repo's version-alignment rule (AGENTS.md
-版本号策略), which reserves `major` for following `@objectstack` across a major —
-the same classification objectui#5942 and objectui#4919 used for identically
-breaking type removals. **Breaking for TypeScript consumers of the two names
-only.** The in-repo consumer count is zero; consumers outside this repository
-that import either name from either package are not visible from here, which is
-why this entry is graded on the published-surface change and not on that count.
diff --git a/.changeset/7521-timeline-colorfieldladder-race.md b/.changeset/7521-timeline-colorfieldladder-race.md
deleted file mode 100644
index 6c3c03a931..0000000000
--- a/.changeset/7521-timeline-colorfieldladder-race.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-Removes the shared-`lastItems` race in
-`packages/plugin-timeline/src/ObjectTimeline.colorFieldLadder-7243.test.tsx`. No
-runtime, type, schema or exported-symbol change — the diff is one test file, so
-this declares "not published" rather than a version bump.
-
-The test helper `colorsFor` left every render mounted and waited on
-`lastItems.length` alone. React Testing Library's auto-cleanup runs in
-`afterEach`, never between two renders inside one `it`, so the two `it`s that
-call `colorsFor` twice had two `ObjectTimeline`s alive writing one module-level
-`lastItems` — and a length-only predicate cannot say which of them wrote it.
-
-The late write was structural rather than incidental: `ObjectTimeline`'s data
-effect lists `objectDef` in its dependencies and a separate metadata fetch sets
-`objectDef`, so every mount issues two `find()` calls. The second is still in
-flight when the predicate goes green, which left each `colorsFor` returning with
-a write queued against a component nothing ever unmounted. Under load that write
-landed after the next call had reset `lastItems`, and rung 2 read the previous
-rung's colour back out.
-
-Because the failure was load-dependent it could kick any PR that happened to be
-in the merge queue, not just changes to `plugin-timeline`.
diff --git a/.changeset/7530-predicate-envelope-declared.md b/.changeset/7530-predicate-envelope-declared.md
deleted file mode 100644
index 2023934eb2..0000000000
--- a/.changeset/7530-predicate-envelope-declared.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**`BaseSchema.visible` / `.hidden` / `.disabled` now declare the CEL envelope object the renderer already evaluates, as one named wire type** (objectui#7530, maintainer ruling 2026-09-04, option A).
-
-Each of the three keys goes from `boolean | string` to `boolean | ExpressionWire` on both faces, where `ExpressionWire` is `string | { dialect?: string; source: string }` — the exact string-or-envelope union `FormField.visibleWhen` and its `*When` / `*On` siblings already carried, and the exact accept set of `@object-ui/core`'s `toPredicateInput` / `hasDeclaredPredicate`. The Zod mirror's `z.union([z.boolean(), z.string()])` becomes `z.union([z.boolean(), ExpressionWireSchema])` on all three.
-
-Two names are new on the published surface:
-
-- `ExpressionWire` (type, main entry) — the TypeScript wire union, in `packages/types/src/expression.ts`.
-- `ExpressionWireSchema` (`@object-ui/types/zod`) — its runtime twin, hoisted out of `zod/form.zod.ts` (where it was module-private) into `zod/expression.zod.ts` and imported by both `base.zod.ts` and `form.zod.ts`. One envelope type, reused by reference; no second spelling.
-
-This is a **widening**, not a replacement, and the renderer's behaviour is untouched: `SchemaRenderer`'s `shouldHide` / `shouldDisable` chains already routed all three keys through core's one definition of "declared" and evaluated the value, and the envelope was already pinned as working on `hidden` and `disabled` — through a `Record` cast, because no key declared it. Measured before this change, `BaseSchema.safeParse({ type, hidden: { dialect: 'cel', source: 'true' } })` returned `success: false` (`invalid_union` at path `hidden`) while the identical envelope on `FormField.visibleWhen` parsed one file over; that is the gap this closes, on all three keys at once. Every boolean and string value keeps parsing and keeps type-checking unchanged. `dialect` is optional and unconstrained on the wire because the runtime reads it that way (only `'cel'` keeps its envelope on the canonical engine; anything else is unwrapped onto the legacy path).
-
-Not changed: `hasDeclaredPredicate` (no per-key branch — option B was rejected), the `*On` / `visibleWhen` sibling keys, and `ActionSchema.condition`, which already declared the envelope inline.
-
-Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略").
diff --git a/.changeset/7540-option-editor-key-loss.md b/.changeset/7540-option-editor-key-loss.md
deleted file mode 100644
index 63d7849061..0000000000
--- a/.changeset/7540-option-editor-key-loss.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-fix(app-shell): the object-field options editor no longer drops `default` and `visibleWhen` on save
-
-Opening a picklist field in the metadata-admin designer, editing any option and
-saving used to write the option back without its `default` or `visibleWhen`
-key. It was not a validation failure — the payload stayed perfectly valid, just
-smaller than what the author wrote — so the loss was silent, and it took a
-picklist default with it: `default` is `enforce` on the object-field face, and
-the engine seeds the insert path from the option holding it.
-
-The loss started in the reader, not the writer. `readOptions` projected each
-authored option down to `value` / `label` / `color`, so both keys were already
-gone before `patchOptions` ran. `readOptions` now carries the keys the editor
-has no control for and `patchOptions` writes them back, which also protects any
-option key the spec accepts later. No authoring UI changed.
diff --git a/.changeset/7544-listview-chart-capability-gate.md b/.changeset/7544-listview-chart-capability-gate.md
deleted file mode 100644
index 39cbf0c204..0000000000
--- a/.changeset/7544-listview-chart-capability-gate.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-The list view's capability gate now resolves `chart` (objectui#7544).
-
-A `grid` list view that declared a complete `chart:` block and whitelisted
-`appearance.allowedVisualizations: ['grid', 'chart']` was never offered the Chart
-toggle. `availableViews` builds the resolvable set from each visualization's binding
-and intersects it with the whitelist (ADR-0047, whitelist ∩ resolvable); seven
-visualizations had a capability check there — kanban, gallery, calendar, timeline,
-gantt, map, tree — and `chart` had none, so the author's own whitelist was filtered
-down to nothing and the view fell back to `['grid']`. `chart` entered the offered set
-only through the "always allow switching back to the schema's own viewType" leg, i.e.
-only when the view was already `viewType: 'chart'`.
-
-Both halves were spec-legal and authorable the whole time: `chart` is a
-`VisualizationTypeSchema` switcher target and `chart:` is a list-view key
-(`ListChartConfigSchema`). Only the gate never asked. This is `map`'s objectui#5042 one
-visualization over, and is fixed the same way.
-
-**What now resolves.** A chart block that binds to names the author wrote: the ADR-0021
-shape (`dataset` plus at least one measure in `values`), or the legacy inline shape (a
-declared category — `xAxisField` / `categoryField` — together with a declared measure —
-`yAxisFields[0]` / `valueField`), in the view-level `chart` block or the legacy
-`options.chart` bag. A block that declares no binding stays unoffered: reaching the
-renderer with nothing declared lands on the legacy branch's invented `'name'` /
-`'value'` floor, and the switcher must not offer a route into it. That floor itself is
-unchanged here (objectui#7547).
-
-The gate and the `case 'chart'` render branch now read ONE resolver
-(`resolveListChartBinding`) rather than two copies of the condition, so they cannot
-drift about what a usable chart block is. `schema.chart` also joins the memo's
-dependency array, so a block that arrives on a later render is seen.
diff --git a/.changeset/7546-chart-series-keys-declared.md b/.changeset/7546-chart-series-keys-declared.md
deleted file mode 100644
index 123b480a0f..0000000000
--- a/.changeset/7546-chart-series-keys-declared.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ChartDataSeriesSchema` (and its TS twin `ChartDataSeries`) declares the six series keys the
-renderer reads — `label`, `variant`, `opacity`, `dashArray`, `stack`, `yAxis` — which the
-non-strict Zod object had been **stripping in silence** while `safeParse` reported success
-(objectui#7546, `domain:ui` PM ruling: measure per key, declare what is live, report the rest).
-
-⚠️ Shipped as `minor`, not `patch`. The declared value domains are the read's own, so the accept
-set widens toward what already renders — but one document class that validated before now
-**refuses**: a series carrying one of these keys with a value the renderer drops in silence
-(`variant: 'bogus'`, `yAxis: 'top'`, `opacity: '0.4'` / `Infinity`, a non-string `stack` /
-`dashArray`, a non-string non-map `label`) — and, separately, `variant: 'current'`, which the
-renderer does NOT drop: the normalizer keeps that renderer-internal spelling and draws it exactly as
-`primary`, but it is not a member of the published pair, so it now refuses at `variant` (below).
-Such a document draws a chart today — the normalizer ignores the bad value, or honours `current`
-— so this is a narrowing away from something that renders, which is the
-distinction objectui#6939's grading language turns on, and it takes the level objectui#6896 and
-objectui#7113 set for the same transition in this same file. This repository's `major` is a
-cross-repo pin to `@objectstack`'s major, not a severity dial; the change is announced here.
-
-## The defect, measured
-
-`ChartDataSeriesSchema` is a non-strict `z.object` — not `.passthrough()` like `BaseSchema` — so an
-undeclared key is removed, not kept. Reproduced red on `origin/main` `a472b071` before the change:
-
-```
-input : { name, label, stack, yAxis, opacity, dashArray, variant }
-parse : success = true
-output: { name }
-```
-
-Every one of the six is read by `normalizeSeries` (`@object-ui/plugin-charts`,
-`normalizeChartSchema.ts:242-255`) and does real work in `AdvancedChartImpl.tsx` — `label` names
-the legend entry, `variant === 'comparison'` selects the muted overlay, `opacity` / `dashArray` set
-stroke and fill, `stack` becomes Recharts' `stackId`, `yAxis` binds the secondary axis. Any consumer
-of the parse output — `objectui check` / `objectui validate` via `safeValidateSchema`, a JSON
-schema derived from the mirror, or any pipeline that keeps `parse()`'s result — lost them outright.
-
-## Per-key liveness, not a blanket declare
-
-"The renderer reads it" was ruled insufficient (a read leg can sit on a value nothing produces —
-objectui#7642), so each key was measured on producers, real work in the reader, and consumer
-surprise, with a lit control on every count. The six are `@objectstack/spec`'s own
-`ChartSeriesSchema` members under the same names and value domains; this node's `series`
-accepts the spec shape by design; in-repo producers write them onto `type: 'chart'` nodes
-(`DashboardRenderer`, `ObjectChart`, `DatasetWidget`, `core/utils/chart-presentation`); and
-the `variant` / `yAxis` narrowings are design intent the reader already enforces.
-
-**`chartType` — the seventh key the review found — is deliberately NOT declared.** It is the first
-limb of `str(raw.chartType) ?? str(raw.type)`, but it is the renderer's *internal* spelling of the
-declared `type`; the spec's `ChartSeriesSchema` lists it as an alias of `type` and refuses it by
-name; and zero documents, fixtures, catalog entries or designer inputs write it on this face
-(controls lit). Declaring it would mint a second writable name for one override. It is reported
-for its own card; the mirror still strips it, and the pin test holds that gap visible.
-
-## FROM → TO
-
-```ts
-// ChartDataSeries — all optional, all additive on the TS face
-+ label?: string | I18nLabel; // spec I18nLabel: string | inline locale map
-+ variant?: 'primary' | 'comparison'; // the spec's own pair
-+ opacity?: number; // finite; NaN / Infinity / strings refused (the spec's 0–1 bound is not enforced — the read's domain)
-+ dashArray?: string;
-+ stack?: string;
-+ yAxis?: 'left' | 'right';
-```
-
-`variant` is the spec's own pair. The normalizer also tolerates a third spelling, `current`, but that
-is the renderer's internal default — written only by the compare-to producers (`ObjectChart`,
-`DatasetWidget`) onto internal-shape arrays that never pass this mirror, and by nothing an author
-writes (docs, fixtures, designer inputs: 0, controls lit) — so it is not a member here: declaring it
-would have fossilised a renderer-side tolerance into a second contract. The normalizer's tolerance
-is unchanged; objectui#7682 owns that decision.
-
-## Unchanged, deliberately
-
-The object stays non-strict — a truly undeclared key is still stripped, exactly as
-`chart-inline-data-retired.test.ts` pins; this change declares what is read, it does not close
-the object. The `data` tombstone (objectui#6896) and the at-least-one-binding refinement
-(objectui#6939 / #7113) are untouched. No reader changed.
-
-Pinned in `packages/types/src/__tests__/chart-series-keys-7546.test.ts` — the card's fixture
-surviving byte-for-byte, each key on the mirror's own `.shape`, each value domain refusing at its
-own path, the TS face in lockstep, and the `chartType` gap.
diff --git a/.changeset/7547-forced-path-invented-bindings.md b/.changeset/7547-forced-path-invented-bindings.md
deleted file mode 100644
index 81375e7479..0000000000
--- a/.changeset/7547-forced-path-invented-bindings.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-list': minor
-'@object-ui/app-shell': minor
----
-
-Stop inventing a gallery cover binding, and offer the view switcher only when it
-can switch (objectui#7547).
-
-**The object page no longer floors `gallery.imageField` at `'image'`.** It used
-to supply that key for every object view, declared or not. The screen damage was
-small — `ObjectGallery` collapses the cover area when no record yields a cover —
-but the key fed `ListView`'s ADR-0047 capability gate, which reads
-`options.gallery.imageField`, so **Gallery was offered on every object view that
-whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now
-forwards the view's own declared block (both legacy cover spellings still
-cross-fill each other, and `titleField` keeps its `'name'` display floor) and
-emits no cover key when the view declared none. Same class and same route as
-objectui#7029 (calendar) and objectui#7070 (gantt dates).
-
-⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery
-toggle. That is the ADR-0047 rule working: it was only ever offered because this
-relay answered the gate on the author's behalf.
-
-**The visualization switcher is drawn for the resolved list, not the whitelist.**
-`showViewSwitcher` was computed from the LENGTH of
-`appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects
-it with the capability gate — so a view whitelisting `['grid', 'timeline']` with
-no timeline block drew switcher chrome around a single Grid entry. The predicate
-now lives at the one site that holds both halves. Views whose whitelisted types
-all resolve are unaffected.
diff --git a/.changeset/7572-i18n-resources-deep-merge.md b/.changeset/7572-i18n-resources-deep-merge.md
deleted file mode 100644
index 624ad4ec10..0000000000
--- a/.changeset/7572-i18n-resources-deep-merge.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-fix(i18n): `createI18n`'s `resources` deep-merges, so overriding one key of a namespace keeps the rest
-
-A language pack's top-level keys ARE the namespace groups (`common`, `calendar`,
-`list`, ...), each a nested object, but `createI18n` merged `resources` over the
-built-in packs one level deep. Supplying a partial group therefore **replaced**
-it rather than merging into it:
-
-```ts
-createI18n({ defaultLanguage: 'en', resources: { en: { calendar: { today: 'Heute' } } } });
-```
-
-left `calendar.today` set and dropped `month`, `week`, `day`, `allDay`,
-`newEvent`, `moreEvents` and `unscheduled` from the instance. `t('calendar.allDay')`
-then returned the bare key, and `calendar.allDay` reached the DOM as literal
-text — silently, with no error and no warning.
-
-The merge now recurses, so a partial group override touches only the keys it
-names. Packs nest up to four levels below the group
-(`console.ai.empty.build.title`), so it recurses rather than adding one fixed
-extra level. Two in-repo call sites were affected by this and are repaired by
-the change: `packages/plugin-gantt/demo` supplied 53 of the 80 `zh.gantt.*` keys
-and silently lost the other 27 (its own comment says the demo "is never
-half-translated" — it was), and the `skills/objectui/guides/i18n.md` setup
-example, the documented way to use this API, dropped 46 of the 48 `common.*`
-keys in both `en` and `zh`.
-
-**Arrays are replaced, not concatenated** — stated because it is a decision, not
-a library default. No built-in pack carries an array value today (every leaf in
-all ten packs is a string), so nothing observable rides on it; the rule decides
-what a future array means. An author who writes an array is naming the whole
-list, so replacement is the only rule that lets them shorten or reorder one and
-the only one that stays idempotent when the merge runs again. This is
-deliberately narrower than i18next's own `deepExtend`, which the provider's
-async `addResourceBundle` path uses: that recurses into arrays index-wise and
-would leave a longer base array's tail behind, which is the same silent-hybrid
-shape this fix removes.
-
-No caller depended on the old replacement semantics: a census of every
-`resources` literal in the repo (47 parsed sites, plus the dynamic ones resolved
-by hand) found no site that supplied a partial group in order to clear the rest.
diff --git a/.changeset/7578-classname-style-describe.md b/.changeset/7578-classname-style-describe.md
deleted file mode 100644
index c939c9470a..0000000000
--- a/.changeset/7578-classname-style-describe.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`ClassNameStylePropsSchema` describes itself by its two keys (objectui#7578).
-
-The schema's `.describe()` text changes from `Style properties` to
-`className and inline style`. **This is published runtime metadata, not a
-comment**: on this package `.describe()` is what lands in the generated
-JSON-Schema `description` field and in the derived docs, so a consumer that
-renders or diffs those will see the new string. The object itself is unchanged —
-same two optional keys (`className`, `style`), same accept set, same types, same
-export name; nothing validates differently.
-
-Why it moved. objectui#5928 renamed the const away from `StyleProps`, because
-the like-named TypeScript `StyleProps` is the Tailwind-scale vocabulary
-(`padding`, `margin`, `gap`, `backgroundColor`, ...) and shares zero keys with
-these two. That rename only reached readers who can see the const name; the
-description still said what the retired name said, so a reader who meets this
-schema through generated JSON-Schema or docs was left hunting `padding` or `gap`
-under a label that promised them. Naming the two keys ends that at the one place
-that reader actually sees.
-
-The new text uses the verbatim key spellings, so the label answers "what is in
-here" with names the reader can act on. Pinned in
-`packages/types/src/__tests__/classname-style-describe-7578.test.ts`, read off
-the live schema exported by the published `@object-ui/types/zod` barrel.
diff --git a/.changeset/7579-relatedlist-value-gates.md b/.changeset/7579-relatedlist-value-gates.md
deleted file mode 100644
index 81d5da2bbb..0000000000
--- a/.changeset/7579-relatedlist-value-gates.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only: settle the fetched value before two RelatedList tests read it (objectui#7579). No published behaviour changes.
diff --git a/.changeset/7580-responsive-vocabulary-rehome.md b/.changeset/7580-responsive-vocabulary-rehome.md
deleted file mode 100644
index 08626f0ba3..0000000000
--- a/.changeset/7580-responsive-vocabulary-rehome.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/layout': minor
-'@object-ui/mobile': minor
-'@object-ui/core': minor
----
-
-Re-home the breakpoint layout vocabulary and delete the two dead responsive
-implementations (objectui#7580, maintainer ruling 2026-09-04, option A).
-
-**Breaking, deliberately, in one direction only.** `@objectstack/spec` retired its whole
-`ui/responsive` vocabulary in objectstack#11027 — `ResponsiveConfigSchema`,
-`BreakpointName`, `BreakpointColumnMapSchema` and `BreakpointOrderMapSchema` — on the
-stated ground that the four types "had no other authorable carrier". That ground is
-measurably false on the renderer side: `responsive-grid` is a REGISTERED SDUI component
-whose authorable `columns` input is typed by `BreakpointColumnMap` and applied by
-`resolveColumnClasses` on the render path, and `BreakpointName` types four live readers in
-`@object-ui/mobile`. The tombstone's own return condition — the vocabulary "returns if and
-when a renderer implements it" — is already met here, so the two types a renderer reads
-are re-homed rather than retired.
-
-What survives, under the same names and the same members:
-
-- `BreakpointName` (`xs`…`2xl`) is now declared in `@object-ui/types` (`mobile.ts`) instead
- of re-exported from the spec. **No consumer change**: same name, same six members, same
- export sites on `@object-ui/types` and `@object-ui/mobile`. Only its provenance moved.
-- `BreakpointColumnMap` is now declared in `@object-ui/layout` (`ResponsiveGrid.tsx`),
- verbatim from the retired `$strict` schema: six optional column counts, no index
- signature. `responsive-grid`'s `columns` input and its resolver are unchanged.
-
-What is removed:
-
-- `BreakpointOrderMap` (`@object-ui/layout`) — retired with the key, not re-homed. It had
- no read point in the package; it was published only because the retired
- `ResponsiveConfigSchema` paired it with the column map, so an author configuring `order`
- needed the type. With the schema gone there is no order vocabulary for it to be the type
- of, and re-declaring it would be the declare-without-enforce shape ADR-0049 removes.
-- `useResponsiveConfig` (`@object-ui/mobile`), with its `SpecResponsiveConfig` and
- `ResolvedResponsiveState` exports, and `ResponsiveProtocol` (`@object-ui/core`), with
- `resolveResponsiveConfig` / `getVisibilityClasses` / `getColumnClasses` /
- `getOrderClasses` / `shouldHideAtBreakpoint`. Both read the retired
- `ResponsiveConfigSchema` and both were measured at zero callers (objectui#4773).
-- `SpecResponsiveConfig` / `SpecBreakpointName` (`@object-ui/types`) — dead re-exports once
- the two implementations above went, dropped rather than re-declared locally, the same
- disposition the retired i18n names in that file already carry.
-
-No behaviour is retired. The live per-breakpoint readers — `useBreakpoint`,
-`ResponsiveContainer`, `BREAKPOINTS` / `BREAKPOINT_ORDER` / `getCurrentBreakpoint`, and
-`responsive-grid` itself — are untouched.
-
-**Sequencing.** objectui's next `@objectstack/spec` pin bump must carry `Blocked-by:`
-objectui#7580: the retirement is merged upstream and unreleased, so this must land first.
diff --git a/.changeset/7581-strict-authoring-face-measurement.md b/.changeset/7581-strict-authoring-face-measurement.md
deleted file mode 100644
index 1dd5fa4796..0000000000
--- a/.changeset/7581-strict-authoring-face-measurement.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Add `scripts/measure-strict-authoring-face.mjs`, the measurement throwaway for
-objectui#7581 (first deliverable of the #5250 ruling). It derives strict twins
-of every node schema in memory and counts undeclared keys per component across
-the catalog fixtures, the `content/docs` JSON fences and the authored documents
-under `apps/**` and `packages/*/examples/**`. Tooling only; no schema, gate or
-`.passthrough()` changes, and no package is released by this change.
diff --git a/.changeset/7586-primary-field-consumer-reads.md b/.changeset/7586-primary-field-consumer-reads.md
deleted file mode 100644
index 1d72e0ee6b..0000000000
--- a/.changeset/7586-primary-field-consumer-reads.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/components': minor
-'@object-ui/plugin-detail': minor
----
-
-The last two consumer-side reads of `primaryField` off an OBJECT def are gone (objectui#7586).
-
-`primaryField` is a `DetailViewSchema` key (`@object-ui/types` `views.ts`) — a **view** key,
-which `DetailView.resolveDisplayTitle` reads off `schema` and is welcome to. Read off an
-**object** def it is undeclared: `@objectstack/spec`'s object schema is a `strictObject`
-answering `unrecognized_keys: ['primaryField']`, and `ObjectSchema.create()` throws.
-`primaryField` appears in **zero** files of the shipped `@objectstack/spec@17.2.0` dist,
-against 68 for the canonical `nameField`. objectstack#6326 removed the identical read from
-two lint rules; objectui#7287 / PR #7585 removed it from `resolveTitleField`. These two
-survived it — and three of this repo's own changelogs already called the probe *"not a spec
-property — always undefined"* while the code kept honouring it.
-
-**They are two different repairs, not one patch applied twice.**
-
-`@object-ui/components` — `PageHeaderRenderer`'s record-chip chain ranked
-`objectSchema.primaryField` directly under `schema.title` and **above** the unified ADR-0079
-resolver, on the surface that renders the **actual H1** of a synthesized record page. The
-rung is deleted, so the heading now comes from `titleFormat` → the ADR-0079 resolver
-(`nameField` → `displayNameField` → type-aware derivation) → the record-key walk, the same
-precedence `DetailView`'s own header uses.
-
-`@object-ui/plugin-detail` — `record:details`' dedupe ladder decides **which row the body
-grid hides**, so that the field already shown as the H1 is not repeated underneath it. That
-is a different question from "what is the title", and `primaryField` was its *first*
-candidate. The rung is deleted; the ladder is the literal display-name walk that mirrors the
-tail of the header chip's chain. Its docstring, which had described the chip as resolving
-"from objectSchema.primaryField", went stale when PR #7585 landed and now describes the chip
-as it is.
-
-**User-visible, deliberately.** A payload that carries the off-spec key anyway changes in two
-ways: the H1 of its record page stops being `primaryField`'s value, and a different row
-survives the detail grid. Nothing spec-legal can reach either path.
-
-`DetailViewSchema.primaryField` is untouched, and so is `ObjectDefLike.primaryField` in
-`buildDefaultPageSchema` — a deliberate declaration (not a read) that keeps an external
-caller's object literal type-checking.
diff --git a/.changeset/7587-quota-reset-promise-pin.md b/.changeset/7587-quota-reset-promise-pin.md
deleted file mode 100644
index 481d0b8bf0..0000000000
--- a/.changeset/7587-quota-reset-promise-pin.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Pin what a free-plan 429 actually renders after cloud PR #1852 inverted
-`error.details.resetsTonight` and added a sibling `resetsAt` (objectui#7587).
-Measured: `resetsTonight` has no reader past `parseAiQuotaError`, so the
-inversion changes nothing on screen and the banner is the server's own sentence
-verbatim — pinned as an exact accounting of the rendered text, and as the flag
-being inert. Tests plus one field comment; no package is released by this
-change.
diff --git a/.changeset/7593-record-alert-success-icon.md b/.changeset/7593-record-alert-success-icon.md
deleted file mode 100644
index 9b8e07cd84..0000000000
--- a/.changeset/7593-record-alert-success-icon.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-Fix `record-alert` with `severity: 'success'` rendering a database glyph instead of a
-check mark (objectui#7593).
-
-`SEVERITY_STYLES.success.icon` was the string `'CheckCircle2'`. That literal reaches
-` `, whose resolver degrades an unresolvable name to the `Database`
-icon — deliberately, because server-driven schemas reference icons from other
-libraries and it is better to degrade than to throw. The result was an emerald
-"success" banner with a database glyph in it, on every such alert, silently.
-
-The fix is the live spelling `'circle-check'`, and it is **substitution-free**: it was
-derived by identity rather than remembered. On `lucide-react@1.31.0`,
-`CheckCircle2 === icons.CircleCheck` is `true` — the same glyph object under two names
-— so the repair cannot change which glyph is drawn, only whether one is drawn at all.
-`CheckCircle2` remains a live named export, so static `import { CheckCircle2 }`
-call-sites are unaffected; it is dead only for NAME-BASED lookup, which is the route
-this constant takes.
-
-The load-bearing half is a new pin, because nothing could catch this in either
-direction. The icon gate does not judge dynamic-surface resolvers (its own verdict line
-says so), and the renderer's suite mocks `LazyIcon`, so its assertion could only echo
-the literal back — and it did, pinning the dead spelling. The pin consults the real
-resolver and asserts that every `SEVERITY_STYLES` icon RESOLVES, rather than that it is
-spelled any particular way, so it fails on the actual failure mode rather than
-restating the diff.
-
-The other three severities (`Info`, `AlertTriangle`, `AlertCircle`) are **not** changed
-and were never defective: two of them are absent from lucide's `icons` record but
-present on the dynamic surface (1767 keys vs 2025 names), which is the surface that
-decides here, so they resolve and render correctly today.
diff --git a/.changeset/7603-resource-edit-read-decoration-strip.md b/.changeset/7603-resource-edit-read-decoration-strip.md
deleted file mode 100644
index f54483aad9..0000000000
--- a/.changeset/7603-resource-edit-read-decoration-strip.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Strip the framework's read-time decorations off a served draft before the metadata
-editor's client Zod gate judges it (objectui#7603).
-
-**The symptom.** Opening an item that has a **pending draft** in the metadata admin
-raised a false "this item is invalid" banner plus inline field errors — on a body the
-server accepts, and on exactly the workflow where an author is mid-edit. Every wired
-metadata type whose schema is `.strict()` was affected (14 of the 15); `sharing_rule`
-was shielded only by accident, because `AUTHOR_SHAPE_ONLY_TYPES` switches its edit gate
-off for an unrelated reason.
-
-**The cause.** `client.getDraft()` serves a **decorated** body: the strict draft branch
-returns `item: decorateMetadataItem(type, …)`, which attaches `_diagnostics` whenever
-the type has a registered Zod schema, and `_draft` on the preview-draft branch.
-`ResourceEditPage` merged that body over the layered baseline and handed the result
-straight to `validateMetadataDraft`. The layered half is clean — `getMetaItemLayered`
-serves RAW layers — so the misfire required a pending draft to exist, which is why it
-stayed invisible: an item with no draft passes today.
-
-`@objectstack/spec` names both keys a read-time decoration and states that a served body
-"is therefore NOT a valid input to the schema that produced it until these are removed".
-The server was right and the client was wrong.
-
-**The fix.** `extractDraftBody` — the one function that turns a served draft envelope
-into a body, and the chokepoint all three merge sites go through (the load effect, the
-post-save refresh, the post-publish refresh) — now passes the body through the spec's
-own exported `stripReadDecorations`, the same helper `MetadataService.saveFields`
-already uses on the write side. The strip runs after the presence verdict, so removing
-our own decorations can never turn a served draft into "no draft".
-
-No schema was loosened and no key list is restated in this repo. The list is the spec's,
-reached through its helper, because a local copy goes stale the next time the framework
-adds a decoration — and a decoration this code does not know to remove is precisely the
-defect. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) is deliberately
-**not** on that list: those keys are allowlisted by the closed schemas so provenance
-survives a re-parse, and this strip leaves them alone. A genuinely undeclared key on the
-same draft is still refused, by name.
diff --git a/.changeset/7615-field-api-name-follows-label.md b/.changeset/7615-field-api-name-follows-label.md
deleted file mode 100644
index a698c21c82..0000000000
--- a/.changeset/7615-field-api-name-follows-label.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio object designer: a new field's API name now follows the Label on **every**
-keystroke, not just the first one (objectui#7615).
-
-Typing a label one character at a time — what a person at a keyboard actually does —
-used to leave the field permanently named after its first letter: `Health Score`
-produced the API name `h`, and no later keystroke moved it. Pasting or autofilling the
-same label produced `health_score`, so the two ways of entering the same label
-disagreed. The one-letter name then leaked into the REST API, formulas and exports,
-with nothing on screen saying the API name had stopped following the Label.
-
-Cause: the derivation asked "is this name still auto-generated?" by pattern-matching
-the name itself (`field_` / `` / `_`), and its own first rename
-destroyed that shape. The inspector now records who owns the API name instead of
-re-reading it off a string the feature rewrites.
-
-Three boundaries, unchanged in intent from objectui#2260:
-
-- a new field whose API name the author has not touched re-derives on every label
- change;
-- a field that arrived already named (i.e. saved) is never renamed by a label edit;
-- once the author types in the API name box the Label stops moving it — including
- when what they typed looks like an auto-generated placeholder, which no
- pattern-match on the string alone can tell apart from a real one.
diff --git a/.changeset/7620-data-table-date-convention.md b/.changeset/7620-data-table-date-convention.md
deleted file mode 100644
index d8815092b6..0000000000
--- a/.changeset/7620-data-table-date-convention.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/components': minor
----
-
-One home for the `date` display convention in `data-table` (objectui#7620).
-
-`data-table`'s fallback cell (`formatCellValue`) sniffs ISO strings and
-formats them. Its date-only branch built its own `Intl.DateTimeFormat` bag —
-`{ year: 'numeric', month: 'short', day: 'numeric' }` — while the shared
-`formatDate` drops the year inside the CURRENT year on purpose (the year
-rarely helps on an in-progress record and crowds the cell). So one table
-rendered two faces for the same value depending on which path a cell took:
-the `date` field cell showed `Jul 4` and the fallback cell showed
-`Jul 4, 2026`. The branch now calls `formatDate` (default style).
-
-**Visible change**: in every `data-table`, a current-year date-only value in a
-column that renders through the fallback cell loses its year — `Jul 4, 2026`
-becomes `Jul 4` in `en-US` — and now matches the `date` field cell beside it.
-Past- and future-year dates are byte-identical (`Jul 4, 2024`), which is why
-the split was easy to miss: the two faces only ever diverged on the dates
-users look at most. The datetime branch, the non-date passthrough and every
-cell with its own renderer are untouched.
-
-A column that genuinely wants the year on every row is an explicit `format`
-style honoured by both paths, not a second option bag — the objectui#7443 /
-objectui#4576 lesson, one type over.
diff --git a/.changeset/7623-retire-dashboard-root-title-declaration.md b/.changeset/7623-retire-dashboard-root-title-declaration.md
deleted file mode 100644
index 8cf4f79fa3..0000000000
--- a/.changeset/7623-retire-dashboard-root-title-declaration.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the
-dashboard-root `title` member (objectui#7623).
-
-Its doc comment said "Dashboard title displayed in the header", and that stopped being
-true one release earlier: objectui#7509 retired all five dashboard-root `title` read
-arms under ADR-0049, leaving the key declared, documented as rendering, and inert. The
-header text is the spec-canonical `label` on `BaseSchema`, resolved through
-`pickLocalized`.
-
-What an author loses is the **type-level suggestion** only, and there is **no runtime
-and no validation behaviour change**. `BaseSchema`'s index signature means an existing
-`title:` line still compiles; objectui's Zod twin extends `.passthrough()` `BaseSchema`,
-so it parsed a root `title` before this release and still does; and a document validated
-against `@objectstack/spec`'s strict `DashboardSchema` was already refused there
-(`unrecognized_keys: ['title']`) long before it reached a renderer. Nothing that rendered
-stops rendering — the read arms were already gone.
-
-**Unlike `aria` (objectui#5830), no tombstone comes with this.** The spec refuses a root
-`title` as an unrecognized key, not with a named removal message, so there is nothing to
-inherit by reference and none was invented; the pin
-(`packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts`) asserts the
-key-set half only — `title` is out of the interface's declared members while `columns`,
-`widgets` and `header` stay in.
-
-**Not affected, despite the shared name:** widget-level `DashboardWidget.title` — the
-spec's `I18nLabel` on a different receiver, live, declared and read by
-`DashboardRenderer`, `DashboardGridLayout` and `DashboardWithConfig`. The pin carries it
-as an explicit control.
-
-**Migration:** delete a dashboard-root `title` from any authored dashboard and use
-`label` (it accepts a string or a per-locale map). Widget titles are unchanged.
diff --git a/.changeset/7625-drop-recharts-from-components.md b/.changeset/7625-drop-recharts-from-components.md
deleted file mode 100644
index edbc9b70fb..0000000000
--- a/.changeset/7625-drop-recharts-from-components.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`@object-ui/components` no longer declares `recharts`. The dependency became
-unused when objectui#7397 deleted `src/ui/chart.tsx`, which was its only
-consumer inside this package; `@object-ui/plugin-charts` declares its own
-`recharts` and remains the single implementation (objectui#7625).
-
-**No migration.** Nothing is added to or removed from the published surface, no
-type changes, no behaviour changes. Since objectui#7397 this package publishes
-no recharts-typed export at all, so there is no supported import that reaches
-`recharts` through `@object-ui/components`.
-
-**What actually changes** is the install graph: every consumer of
-`@object-ui/components` was resolving and installing a charting library it could
-not reach. AGENTS.md section 3 constrains this package — the Atoms layer — to
-"Shadcn primitives, zero heavy 3rd-party deps", and heavy widget dependencies
-belong in `@object-ui/plugin-*`; the declaration outlived the reason it existed.
-
-- **Why `patch` and not `minor`.** This repo ships its own breaking changes as
- `minor` with the break spelled out (`scripts/check-changeset-no-major.mjs`),
- so the bump has to state which of the two this is. Nothing breaks for a
- supported consumer: the built output is byte-identical (this package's Vite
- `external` predicate is path-based and never reads `dependencies`, so no
- import graph, chunk or `.d.ts` moves), and the package exposes no recharts
- surface to import. The one consumer this can reach is someone importing
- `recharts` without declaring it and getting it hoisted out of this package's
- dependency by a FLAT `node_modules` layout (npm/yarn). That is a phantom
- dependency — undeclared, unsupported, and un-typed here since objectui#7397 —
- and it is named rather than omitted so the trade is on the record.
-- **Not a bundle-size change.** `vendor-charts` stays eagerly reachable through
- the plugins, so `check:eager-closure` is unaffected in both directions. The
- cost this removes is install-graph weight, not shipped bytes.
diff --git a/.changeset/7627-shared-record-source-reader.md b/.changeset/7627-shared-record-source-reader.md
deleted file mode 100644
index ac7d4d29df..0000000000
--- a/.changeset/7627-shared-record-source-reader.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/plugin-calendar': minor
-'@object-ui/plugin-gantt': minor
-'@object-ui/plugin-grid': minor
-'@object-ui/plugin-map': minor
-'@object-ui/plugin-tree': minor
-'@object-ui/react': minor
----
-
-`@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which
-object is this block bound to" (objectui#7627).
-
-Six view plugins each spelled that resolution locally — `ObjectCalendar` twice,
-`ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three
-wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in
-dataConfig` test. They now delegate to one function that states the published
-objectui#6939 record-source ladder (`data`, then `staticData`, then `objectName`) once.
-
-**No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim
-into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its
-post-collapse spelling across the whole contract-valid input matrix — both bindings
-present, data only, `objectName` only, empty `objectName`, empty `data.object`, the
-`api` / `value` / `staticData` / array-shorthand providers, and nothing bound.
-
-**Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477,
-ruling B of PR #7628) is untouched and is NOT re-pointed at the new reader: it answers
-how `objectName` gets POPULATED when absent, where an already-present `objectName` wins.
-The new reader answers which object a block RESOLVES, where the `data` block wins — the
-order declared on both published faces in `@object-ui/types` and pinned by
-`objectql-record-source-refinement-6939.test.ts`. Merging them would silently override
-one standing ruling or the other.
-
-**`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted
-order, with an in-place comment saying why: its receiver is a localStorage key
-(`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every
-saved layout and filter-chip set of a view carrying both bindings. Two more sites the
-finding listed are not object-name readers at all and were struck: `ObjectGantt`'s
-refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider`
-type-guard over a widget's `data`.
-
-`useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four
-lines that taught it, so the copies cannot re-seed from the hook that replaced them.
diff --git a/.changeset/7632-shared-record-source-config.md b/.changeset/7632-shared-record-source-config.md
deleted file mode 100644
index b455fd38be..0000000000
--- a/.changeset/7632-shared-record-source-config.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-gantt': patch
-'@object-ui/plugin-grid': patch
-'@object-ui/plugin-map': patch
-'@object-ui/plugin-tree': patch
----
-
-One shared record-source ladder, five plugins delegate (objectui#7632).
-
-`@object-ui/core` publishes `resolveRecordSourceConfig(schema)` — the ONE implementation
-of the ruled three-rung record source ladder: `data` first, then `staticData` wrapped as
-`{ provider: 'value', items }`, then `objectName` folded to `{ provider: 'object' }`, and
-`null` when nothing is bound. It is the PRODUCER whose output the objectui#7627 reader
-`resolveRecordSourceObjectName` consumes, and it now sits beside it in the same module.
-
-That ladder is published contract on both faces — `packages/types/src/objectql.ts` and its
-zod mirror both ship `.describe()` strings naming `getDataConfig`'s order (objectui#6939,
-maintainer ruling 2026-09-02), pinned by `objectql-record-source-refinement-6939.test.ts` —
-and it was hand-copied into five plugin components with no gate holding them together. A
-change to the ruled order had five edit sites and nothing that noticed a missed one; that
-is the AGENTS.md #0.1 drift class.
-
-**No behaviour changes.** `ObjectCalendar`, `ObjectGantt` and `ObjectTree` now call the
-shared reader directly. `ObjectGrid` and `ObjectMap` keep their own bare-array `data`
-shorthand as a documented head above the shared call and are otherwise unchanged.
-`record-source-config.behaviourNeutrality-7632.test.ts` transcribes all five pre-collapse
-bodies verbatim and asserts the post-collapse spelling agrees with each across the whole
-input matrix, so a later edit to the shared reader that moves any site turns red.
-
-**Two divergences were measured rather than assumed, and both are preserved.**
-
-`ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is
-the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key.
-That is a TypeScript narrowing device with no runtime effect — an absent property reads
-`undefined`, falsy either way — so the guard could never change which rung is taken. The
-shared reader's optional-property parameter accepts the union directly, and the
-equivalence is pinned on a fixture that really lacks both keys rather than argued.
-
-`ObjectGrid` and `ObjectMap` normalize a bare-array `data` to `{ provider: 'value', items }`;
-`ObjectCalendar`, `ObjectGantt` and `ObjectTree` do not, and return the array verbatim. That
-is a real divergence on off-contract input — `ViewData` is a discriminated union over object
-variants, so an array under `data` cannot be published. It is NOT unified here: the shared
-rung stays contract-strict and the two sites keep the head locally, the same way objectui#7627
-left the off-contract `{ provider: 'object' }` tails at their sites. Both sides of the fork are
-pinned, so neither folding the head in nor deleting it as redundant can happen silently.
-
-`ObjectTree`'s copy took `schema: any`; it now goes through the shared reader's typed
-parameter. Types are erased at runtime, so nothing it resolves moves.
diff --git a/.changeset/7638-navigation-url-follows-record-source.md b/.changeset/7638-navigation-url-follows-record-source.md
deleted file mode 100644
index ad2d5dc14d..0000000000
--- a/.changeset/7638-navigation-url-follows-record-source.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
-'@object-ui/plugin-tree': patch
-'@object-ui/react': patch
----
-
-A record-page URL now names the object the clicked rows actually came from, in
-`ObjectTree` and `ObjectCalendar` (objectui#7638).
-
-`useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed,
-and both components handed it the bare top-level `schema.objectName` while resolving
-their own rows through the objectui#6939 record-source ladder (`data`, then
-`staticData`, then `objectName`). objectui#6939 published `objectName` as that ladder's
-THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one
-record source — and a row fetched through `data.object` whose click built
-`/{schema.objectName}/record/{id}` named a record that the URL's own object does not
-contain.
-
-Two shapes change, both toward the object the rows came from:
-
-- a block carrying **both** bindings navigated to the top-level key and now navigates to
- `data.object`;
-- a **data-only** block had no name to build a URL from at all, so the hook took its
- `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed
- record URL.
-
-`ObjectCalendar` is where the divergence was plainest: on one click it resolved the
-detail drawer through the ladder and the navigation URL through the top-level key. The
-URL now reuses the very `schemaObjectName` that already keys the calendar's record query
-and its `$expand` derivation, so query, drawer and URL agree by construction.
-
-**Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName`
-tail for the off-contract `data: { provider: 'object' }` that carries no `object`
-(`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName`
-already carries, and the same conservatism objectui#7627 applied when it published the
-shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an
-`objectName`, and only what callers hand it has changed.
-
-The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is
-why there were copies to convert at all — component authors copied the divergence out of
-the documentation, correctly, as written — so it now points at
-`resolveRecordSourceObjectName` and says explicitly that a caller with no data config
-has nothing above rung three and should keep passing `schema.objectName`.
-
-`ObjectKanban` is deliberately **not** converted: it has no data config, no
-`getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so
-`schema.objectName` already IS its record source and its board, drawer and URL already
-agree.
diff --git a/.changeset/7645-schema-registry-kanban-honesty.md b/.changeset/7645-schema-registry-kanban-honesty.md
deleted file mode 100644
index 8322a99834..0000000000
--- a/.changeset/7645-schema-registry-kanban-honesty.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`SchemaRegistry['kanban']` stops describing a component it cannot name
-
-`SchemaRegistry` documents itself as "the Single Source of Truth for component
-type lookups". Its `'kanban'` entry named `DeclarativeKanbanSchema` — the
-authoring/validation face declared in this package — while the renderer
-registered for that key is `ObjectKanbanRenderer` in `@object-ui/plugin-kanban`
-(`ComponentRegistry.register('kanban', …)`), which consumes that package's own
-`KanbanSchema`. The two are distinct dialects, measured member by member: the
-plugin face declares 19 members in its own body, the declarative face 7, and
-they share three names — `type`, `columns` and `onCardMove`. `onCardMove` has
-the same signature on both sides; `columns` is required
-`DeclarativeKanbanColumn[]` on one and optional `KanbanColumn[]` on the other,
-the two element types sharing five of their six members; the remaining 16
-plugin members and the declarative `draggable` / `onCardClick` have no
-counterpart across the gap. For this one key the map's value did not describe
-the component the key names.
-
-Pointing the entry at the honest type is not reachable from this layer, and
-that was measured rather than assumed. Importing `@object-ui/plugin-kanban`
-here is a phantom dependency — `check:phantom-deps` rejects it by file and
-pair, type-only imports included — and declaring the dependency would close the
-cycle `@object-ui/types` → `@object-ui/plugin-kanban` → `@object-ui/types`,
-because this package is the zero-workspace-dependency bottom layer.
-objectui#6172's ruling (2026-08-31) kept the plugin's bare names rather than
-relocating that dialect down here — that is why this entry cannot name the
-plugin's type at this commit. That half of the ruling has since been reversed:
-objectui#7664's ruling (a) (2026-09-05) rewrites this package's `'kanban'`
-arm to the plugin's shape, has `@object-ui/plugin-kanban` conform to it, and
-retires the `DeclarativeKanban*` trio; `registry.ts` is on its execution list,
-so this entry is scheduled to be re-pointed at the declared type. The value
-below is the transitional state under that ruling, not the permanent one.
-
-So the entry now asserts only what this layer can prove, and what BOTH dialects
-satisfy: `BaseSchema & { type: 'kanban' }`.
-
-**What this changes for consumers.** `keyof SchemaRegistry` — and therefore the
-published `ComponentType` union — is unchanged; `'kanban'` is still a member,
-pinned at compile time so it cannot be narrowed away silently. What changes is
-the value side of this one key, in two ways, one loud and one silent:
-
-- **Breaking, loudly, for consumers who indexed `SchemaRegistry['kanban']` and
- flowed the value into the declarative face.** Before this change
- `SchemaRegistry['kanban']` *was* `DeclarativeKanbanSchema`, so
- `const s: DeclarativeKanbanSchema = registryValue` compiled. It no longer
- does: the entry lacks the required `columns` member, and that assignment is
- a compile error (TS2741). Nothing in this repository performed such an
- indexed access, so no in-repo code moves — the break is for consumers
- outside it.
-- **Silent, for member reads.** `BaseSchema` carries an index signature
- (`[key: string]: any`), so reading the declarative face's own members —
- `columns`, `draggable`, `onCardMove`, `onCardClick` — off the new entry is
- not an error: the reads resolve through the index signature and type as
- `any`. `SchemaRegistry['kanban']['columns']` was `DeclarativeKanbanColumn[]`
- and is now `any`; nothing turns red, the read just stops being checked.
- (Undeclared keys already read as `any` before, through the same signature;
- what changes is those declared members.)
-
-Migration: name the face you actually mean. `DeclarativeKanbanSchema` is still
-exported from `@object-ui/types` unchanged for the authoring face, and
-`KanbanSchema` from `@object-ui/plugin-kanban` for the rendered one.
-
-This is a breaking change shipped as `minor`: this repository's
-version-alignment rule keeps objectui's major pinned to `@objectstack`'s and
-ships objectui's own breaking changes as `minor` with the break spelled out in
-the changeset body, which is what the two bullets above are.
diff --git a/.changeset/7654-chatbot-display-mode-tombstone.md b/.changeset/7654-chatbot-display-mode-tombstone.md
deleted file mode 100644
index 5c8f73d54d..0000000000
--- a/.changeset/7654-chatbot-display-mode-tombstone.md
+++ /dev/null
@@ -1,123 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-chatbot': patch
----
-
-Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an
-ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's
-"Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed
-(objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44).
-
-⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.**
-Ships as `minor` per the launch-window convention: objectui's `major` is a cross-repo pin
-to `@objectstack`'s so that "same major means compatible" holds across the two repos
-(`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes ship as
-`minor` with the break named where it lands — this entry is the channel that carries it.
-
-## What was retired, and why
-
-The node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one
-selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling
-of that same choice, and no renderer has ever read it: `chatbot-floating` renders the
-trigger and panel unconditionally, and `chatbot` never looked at the key, so
-`displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a
-`chatbot-floating` node changed nothing. It was nevertheless declared on both faces,
-painted as a **Display Mode** control in the designer's property panel, and written as
-`'floating'` into every node the designer created — two surfaces teaching a switch that
-did not exist.
-
-Re-measured on this branch's base rather than inherited from the card: a whole-repo
-`git grep` census over tracked files, build output excluded, returned the declarations,
-the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and
-two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same
-pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was
-not blind.
-
-FROM → TO:
-
-- `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an
- ADR-0049 retirement tombstone whose comment points at `type` as the replacement.
-- `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**,
- the same tombstone. objectui#7655 declared the key on the floating face with
- `ChatbotSchema`'s own lines precisely so this retirement would find it on both faces;
- leaving the copy typed would have kept the published face teaching the switch.
-- `chatbot-floating` `inputs`: the **Display Mode** control is removed.
-- `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into
- designer-created nodes.
-
-A control is restated, never deleted into a vacuum (objectui#7070): the restatement of
-the removed control is the tombstone's guidance plus this note.
-
-**Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema`
-or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` —
-`'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for
-inline. **No JSON document needs editing** — see the next section.
-
-## Stored documents: runtime validation of this key is unchanged — zero before, zero after
-
-`displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for
-both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is
-`.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node
-the designer ever created) parses green before this change and parses green after it,
-and the value is dropped at render time exactly as it always was.
-
-That is deliberate, and it is why this tombstone has **no `retirementTombstone()`
-half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis
-(objectui#6152), a different defect, and a parse outcome the ruling did not ask for.
-`packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins'
-shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if
-objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the
-mirror adds the `retirementTombstone()` half at that time, flipping the control rather
-than deleting it.
-
-## Why a tombstone and not a deletion — measured on this carrier
-
-`ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index
-signature, and on such a carrier deleting an optional member is **silent in every value
-shape**: the index signature defeats both excess-property checking and the weak-type
-check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature
-control carrier (`FloatingChatbotConfig`) lit in the same run:
-
-| route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` |
-|---|---|---|---|
-| declared (before) | clean | `TS2322` | clean |
-| deleted | clean | **clean** | clean |
-| tombstoned (after) | `TS2322` | `TS2322` | `TS2322` |
-
-Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned,
-**presence with any value** is a compile error — a channel deletion cannot produce on
-this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not
-louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678).
-Prong 2 of that discriminator licenses the tombstone: the key was advertised in the
-3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the
-presentation switch. The deleted row is pinned in the test file as a live control — an
-undeclared key that rides both shapes with no directive — so the contrast cannot rot.
-
-## Accept-set change, one line per face
-
-- **TypeScript.** A write of `displayMode` against either chatbot face used to compile
- and now does not.
-- **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document
- carrying the key parses green before and after, and keeps the value.
-- **Designer.** The **Display Mode** control disappears from the `chatbot-floating`
- property panel, and newly created nodes no longer carry the key.
-- **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating`
- registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to
- 8, so the manifest projected from them no longer lists the prop. Measured on both sides
- of this change: `validateTree` on a stored `chatbot-floating` node carrying
- `displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity
- **`warning`**, message `` ` has no prop "displayMode"` `` — which is
- what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs
- the props interface `generateDts` derives from those same `inputs` drops from 20 members
- to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those
- generated intrinsics no longer type-checks the attribute.
-
- **This is author-time only: no stored document stops parsing and nothing at render
- moves.** The value survives compilation — `compile()` returns a tree still carrying
- `displayMode: 'floating'`, byte-for-byte the same keys before and after — and a
- `warning` never blocks a page, because the page renderer filters the diagnostics to
- `severity === 'error'` before deciding whether to fail. Two neighbouring instruments are
- untouched and worth naming so the scope is not read wider than it is: `os validate` runs
- `safeValidateSchema`, the Zod path, and is silent on this key before and after; and the
- build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does
- not contain `chatbot-floating` on either side of this change.
diff --git a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md
deleted file mode 100644
index 9b382d360c..0000000000
--- a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Retire `FloatingChatbotConfig.triggerIcon` (objectui#7654, ADR-0049 enforce-or-remove).
-
-`triggerIcon` was declared `?: string` with `@default 'MessageCircle'` and read by nothing.
-`FloatingChatbot` destructures six of the interface's seven keys — `position`,
-`defaultOpen`, `panelWidth`, `panelHeight`, `title`, `triggerSize` — and never this one,
-and `FloatingChatbotTrigger` takes no icon prop at all, so the advertised default never
-rendered either. Re-measured on this branch's base rather than inherited from the card: a
-whole-repo `git grep` census over tracked files, build output excluded, returns the
-declaration and one historical CHANGELOG line and nothing else, while the same pass over
-`triggerSize` — a key that IS read — returns ten sites across four files, so the instrument
-was not blind.
-
-It is also absent from the `chatbot-floating` registration's `inputs` AND from its
-`defaultProps` (`packages/plugin-chatbot/src/renderer.tsx`), both re-confirmed here. No
-designer control ever offered it and no designer-created node carries it, so TypeScript was
-the only way to reach the key. That is what makes this half of objectui#7654 an ordinary
-retirement; the card's other key, `displayMode`, is seeded into `defaultProps` and is NOT
-touched here.
-
-FROM → TO: `triggerIcon?: string` → **tombstoned**, `?: never` on the interface. The FAB
-trigger renders a fixed icon and takes no icon prop; there is no authored spelling that
-changes it.
-
-## This tombstone has NO Zod half, deliberately
-
-Every other tombstone in this package pairs `?: never` with a `retirementTombstone()`
-refusal on the Zod twin. There is no twin here to carry one: `FloatingChatbotConfig` has no
-Zod mirror at all, and `floatingConfig` sits in the `UnmirroredDeclared` ledger
-(`zod-mirror-parity.test.ts`, `complex.zod.ts#ChatbotSchema`). `BaseSchema` is
-`.passthrough()`, so the whole `floatingConfig` object rides through unvalidated — before
-this change and after it. Minting a mirror to host a refusal would be the
-declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and
-inert, or unmirrored and live, and fixing one says nothing about the other. This change
-does not widen into it.
-
-**Accept-set change, stated plainly for reviewers:** on the TypeScript face, a write of
-`FloatingChatbotConfig.triggerIcon` used to compile and now does not. On the runtime face,
-nothing changes at all — the key parsed green before and parses green after. The refusal is
-TYPE-LEVEL ONLY, which is narrower than this package's other tombstones and is the reason
-this carries a contract-review label rather than being filed as an internal tidy-up.
-
-## Why a tombstone and not a deletion, when the usual argument does not apply
-
-The usual case for `?: never` argues from the mirror: an undeclared key is silently
-STRIPPED by a non-strict `z.object`, so deleting trades one silent no-op for another. With
-no mirror, that argument is unavailable, so the route was measured on the `tsc` channel
-alone instead:
-
-| route | fresh object literal | widened (non-fresh) value |
-|---|---|---|
-| deleted | `TS2353` excess-property error | **compiles CLEAN** |
-| tombstoned | `TS2322` | `TS2322` |
-
-Excess-property checking only reaches a fresh literal, so deletion would have left the
-widened path — `const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig =
-raw;` — silently accepting a key nothing reads. The declared `never` makes the assignment
-itself ill-typed, so freshness stops mattering. Both rows are pinned in
-`packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts`, the "deleted"
-row as a live control on a genuinely undeclared key rather than as prose, so the contrast
-cannot rot.
-
-That file also pins the runtime half as a **tripwire**: it asserts that a node carrying
-`floatingConfig.triggerIcon` still parses green. If objectui#6152 ever mints a
-`FloatingChatbotConfigSchema`, it goes red — the intended signal that whoever lands the
-mirror must add the `retirementTombstone()` half at the same time and flip the control
-rather than delete it into a vacuum.
diff --git a/.changeset/7655-chatbot-registration-authoring-faces.md b/.changeset/7655-chatbot-registration-authoring-faces.md
deleted file mode 100644
index c5a8e2fbff..0000000000
--- a/.changeset/7655-chatbot-registration-authoring-faces.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-chatbot': patch
----
-
-One named, importable authoring-face type per `plugin-chatbot` registration:
-`ChatbotEnhancedSchema` and `ChatbotFloatingSchema` join `ChatbotSchema`
-(objectui#7655, under the objectui#6169 / #6172 family ruling — every component
-node has exactly one named, importable authoring-face type).
-
-`packages/plugin-chatbot` registers three components — `chatbot`,
-`chatbot-enhanced`, `chatbot-floating` — and `@object-ui/types` published ONE
-face for the family with `type` pinned to `'chatbot'`. An author annotating a
-`chatbot-enhanced` or `chatbot-floating` node either dropped to untyped JSON or
-annotated with `ChatbotSchema` and lied about `type`; the docs' floating example
-had to be a `json` fence because no `tsx` fence could compile. The two
-registrations' real key sets lived in anonymous `ChatbotSchema & { ... }`
-intersections local to the renderer, referenceable by nothing outside that file.
-
-## The shape, and why not the smaller diff
-
-One interface per registration, not `ChatbotSchema['type']` widened to the union
-of the three keys. The union would give three nodes ONE type and re-open what
-#6169 closed — a single interface declaring keys only some of its own `type`
-values read — and this card exists because the family's declarations had already
-drifted from its reads. Each face declares what ITS registration reads, censused
-per key on the PR's base (one `schema.KEY` read per registration body in
-`renderer.tsx`, lit by keys that are NOT shared: `processVisibility` 0 / 1 / 0,
-`floatingConfig` 0 / 0 / 1), and the twenty keys all three read are picked off
-`ChatbotSchema` by name (`ChatbotSharedKey`) so they stay one declaration:
-
-- **`ChatbotEnhancedSchema`** (`type: 'chatbot-enhanced'`): the shared twenty,
- plus `maxHeight` and `processVisibility` (read here, not by the floating
- panel), plus `enableMarkdown`, `enableFileUpload`, `surface` (`'card' |
- 'plain'`, objectui#6687) and the `onClear` runtime slot — four keys
- `ChatbotSchema` never declared.
-- **`ChatbotFloatingSchema`** (`type: 'chatbot-floating'`): the shared twenty,
- plus `enableMarkdown`, `enableFileUpload`, `onClear`, and the two keys it
- declares alongside `ChatbotSchema` — `floatingConfig` (`FloatingChatbotConfig`)
- and `displayMode`. No `maxHeight`, `processVisibility` or `surface`: the
- floating registration has no named read for any of them. (Its trailing raw
- props spread does carry authored keys into the panel today — `processVisibility`,
- `surface` and `showAvatars` are live there, measured through the real host;
- that accidental channel is tracked as objectui#7708, and this face neither
- declares nor promises it.)
-- Neither face declares `ChatbotSchema`'s six legacy members (`loading`,
- `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown`, `height`) — no
- registration reads them by name — and neither redeclares `disabled`, which
- stays `BaseSchema`'s `boolean | string` (objectui#7087).
-
-**`ChatbotSchema` is unchanged.** It keeps `displayMode` and `floatingConfig`
-(declarations verbatim), and the floating face declares the same two, so
-`ChatbotSchema['displayMode']` and `ChatbotSchema['floatingConfig']` stay the
-typed members they were — the objectui#7669 `triggerIcon` tombstone keeps its
-reach on `chatbot` nodes, now pinned on the node. `floatingConfig`'s doc comment
-is rewritten on both faces: the old text said it was "only used when
-`displayMode` is `'floating'`", which was false — it is read by `chatbot-floating`
-alone and forwarded to the panel. `displayMode` is RULED RETIRED — objectui#7654,
-maintainer ruling B (2026-09-05): `?: never` tombstone, designer control and
-`defaultProps` seed removed, in that card's own change. This change carries the
-key untouched on both faces (still unmirrored, still read by nothing) so that PR
-finds the member exactly as ruled, and a tripwire test pins that any value still
-parses green until that PR flips it.
-
-**New published symbol:** `ChatbotSharedKey`, the string-literal union of the
-twenty keys all three registrations read. It is exported from `complex.ts`
-because an exported interface may not extend a `Pick` over a private name
-(TS4022), so it is emitted into `dist/complex.d.ts` and is reachable through the
-published `@object-ui/types/complex` subpath (it is not re-exported from the
-package entry). It is a census, not an authoring face.
-
-## Zod twins, in lockstep
-
-`@object-ui/types/zod` gains `ChatbotEnhancedSchema` and `ChatbotFloatingSchema`
-(and `ComplexSchema` routes the two new discriminants). Every declared key is an
-arm except: the three runtime slots (`onError`, `onSend`, `onClear`), refused by
-name per objectui#6124; and, on the floating twin only, `floatingConfig` (no
-`FloatingChatbotConfig` mirror exists — minting one is objectui#6152's axis) and
-`displayMode` (unmirrored on `ChatbotSchema`'s twin too; retired by ruling on
-objectui#7654 and executed there). The twins mirror the API body params under the
-key the renderer reads, `requestBody`, and inherit `body` as the children slot —
-they do not copy `ChatbotSchema`'s `body` naming collision.
-
-**Accept-set change, stated plainly:** a `chatbot-enhanced` or `chatbot-floating`
-document parsed through the family's only twin used to fail on `type`; through
-its own twin it now parses, and the keys the twin declares are VALIDATED where
-they rode through `.passthrough()` unexamined before (`surface: 'frameless'`,
-`enableMarkdown: 'yes'` and `requestBody: 'x'` are refused). A `chatbot` node's
-parse outcome is unchanged: `ChatbotSchema`'s twin did not move.
-
-## `@object-ui/plugin-chatbot`
-
-The `chatbot-enhanced` and `chatbot-floating` registrations type `schema` as the
-published faces and drop the anonymous intersections. One consequence:
-`chatbot-floating` used to write `disabled={schema.disabled}` and then spread
-`{...props}` AFTER it — and `SchemaRenderer` always includes `disabled: verdict
-|| undefined` in those props, so the raw read was overridden on every render.
-With `disabled` honestly typed as `boolean | string` the raw union cannot be
-forwarded into the panel's `boolean` prop, so the registration now names the
-host verdict (`disabled: hostDisabled`) the way its two siblings have since
-objectui#4431. No render outcome moves; the pin renders through the real host
-both ways.
-
-This ships as `minor` for `@object-ui/types` because it widens the published
-surface with two new node types, two new Zod twins and one new type alias;
-`ChatbotSchema`'s own accept set does not move: objectui's major is pinned to `@objectstack`'s
-(`scripts/check-changeset-no-major.mjs`), and objectui's own contract changes
-ship as `minor` with the semantics spelled out — as above.
diff --git a/.changeset/7658-toc-code-span-anchor-parity.md b/.changeset/7658-toc-code-span-anchor-parity.md
deleted file mode 100644
index ebc03d3009..0000000000
--- a/.changeset/7658-toc-code-span-anchor-parity.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/plugin-markdown': patch
----
-
-Fix `extractToc` deleting tag-shaped text that lives INSIDE an inline code span,
-so its `#id` links resolve to the heading they name again (objectui#7658).
-
-`stripInline()` applied its rules in sequence: the inline-code rule unwrapped
-`` `objectui add ` `` to `objectui add `, and the raw-HTML
-rule that ran next over that same text deleted `` as if it were
-markup. The slug became `objectui-add` while `rehype-slug` — which slugs the
-RENDERED heading, where a code span's content is a literal text value — put
-`objectui-add-component` on the anchor. The TOC entry rendered, was clickable,
-and silently went nowhere. The same sequencing let the emphasis rules eat the
-underscores out of `` `a_b_c` `` and the link rule rewrite `` `[x](y)` ``.
-
-Code spans are now lifted out before any other inline rule runs and restored
-verbatim at the end, so nothing reaches inside one. The raw-HTML rule is
-unchanged and still strips genuine markup — `remark-rehype` runs without
-`allowDangerousHtml`, so the renderer likewise drops raw html nodes and keeps the
-text they wrapped. Link labels that are code spans still collapse (`` [`getData`](/api) ``
-→ `getData`), because the placeholder stays ordinary text to the link rule.
-
-Seven live headings in this repo's own docs were affected
-(`content/docs/utilities/cli.mdx`, `content/docs/utilities/runner.mdx`,
-`packages/cli/README.md`). Pinned against the real render pipeline rather than a
-second derivation of the slug rules: the new test renders each heading through
-`MarkdownImpl` and compares `extractToc`'s id to the `id` attribute
-`rehype-slug` actually emitted.
diff --git a/.changeset/7664-kanban-arm-plugin-dialect.md b/.changeset/7664-kanban-arm-plugin-dialect.md
deleted file mode 100644
index f699bdff08..0000000000
--- a/.changeset/7664-kanban-arm-plugin-dialect.md
+++ /dev/null
@@ -1,90 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**BREAKING** — the `'kanban'` validator arm now accepts the shape the registered
-renderer reads, and the six `DeclarativeKanban*` exports retire (objectui#7664,
-maintainer ruling (a), 2026-09-05).
-
-For an authored `type: 'kanban'` document two different types were
-authoritative depending on who asked. `safeValidateSchema` — what the CLI's
-`validate` / `check` commands apply — honoured `DeclarativeKanbanSchema`
-(`columns` with `color`, `draggable`, cards with `labels` / `assignees` /
-`priority`), while the renderer registered for the key, `ObjectKanbanRenderer`
-in `@object-ui/plugin-kanban`, consumed that package's own `KanbanSchema`
-(`objectName` / `groupBy` / `cardTitle` / `cardFields`, cards with `badges`).
-The two were unrelated dialects, so a board could pass `objectui validate` and
-render **empty**. The ruling: the plugin dialect is authoritative.
-
-**What changes on this package's published surface:**
-
-- **The `'kanban'` arm's accept set is replaced.** `ComplexSchema` →
- `AnyComponentSchema` → `safeValidateSchema` now validate the plugin dialect,
- declared here as `KanbanSchema` / `KanbanColumn` / `KanbanCard` /
- `CardTemplate` / `ColumnWidthConfig` (TypeScript) and `KanbanSchema` /
- `KanbanColumnSchema` / `KanbanCardSchema` / `CardTemplateSchema` /
- `ColumnWidthConfigSchema` (`@object-ui/types/zod`). An `objectName` /
- `groupBy` board passes. A static `columns[].cards[]` board passes — that
- spelling is the same document in both dialects and always rendered. A board
- in the retired dialect is **refused by name** at the keys that betray it: a
- board-level `draggable` and a column `color` are `?: never` tombstones on the
- TypeScript face and named refusal arms on the mirror, each message naming the
- retired `DeclarativeKanbanSchema` shape and the spelling to write instead.
- Both were measured inert (zero read sites in the plugin). The retired card
- keys are deliberately *not* refused: a card is an open record
- (`[key: string]: any`), and `priority` or `dueDate` are legitimate record
- fields.
-- **Every handler key the retired arm refused is still refused, and one more
- joins them.** The successor arm carries all five `#6124` refusal arms under
- the same `'kanban'` key — `onCardMove`, `onCardClick` and `onQuickAdd` as
- RUNTIME SLOTS (callable on the TypeScript face, refused by name on the
- mirror: `KanbanRenderer` forwards all three off `schema.*` in one block),
- `onColumnAdd` and `onCardAdd` as `?: never` tombstones. `onQuickAdd` is the
- one that is newly refused — the plugin dialect declared it, the retired
- declarative face did not. ⚠️ `onCardClick` is the key this arm must never
- drop rather than refuse: the plugin dialect it is modelled on never declared
- the member (the renderer read it undeclared), and because `BaseSchema` is
- `.passthrough()`, leaving it out does not refuse it — it stops being judged
- and the value is kept. Measured on the built dist, `{ type: 'kanban',
- columns: [], onCardClick: { action: 'toast' } }` is REFUSED, beside the same
- document at `onCardMove` / `onQuickAdd` / `onColumnAdd` / `draggable`.
-- **Six exports retire — the second step of the objectui#6172 rename.**
- objectui#6172 (PR #7643, same release line) renamed this package's trio from
- the bare names to `DeclarativeKanbanSchema` / `DeclarativeKanbanColumn` /
- `DeclarativeKanbanCard` and the three Zod mirrors to `DeclarativeKanban*Schema`
- so the bare names could belong to the renderer's dialect. objectui#6172's own
- stop condition was "if the renamed copy has no retained value, escalate", and
- the retained value it cited was precisely the validator arm. This ruling moves
- that arm to the plugin dialect, so the renamed copies have no consumer left
- and retire under ADR-0049 (enforce-or-remove): `DeclarativeKanbanSchema`,
- `DeclarativeKanbanColumn`, `DeclarativeKanbanCard` from `@object-ui/types` and
- `DeclarativeKanbanSchema`, `DeclarativeKanbanColumnSchema`,
- `DeclarativeKanbanCardSchema` from `@object-ui/types/zod` are gone.
- Importing any of them is a compile error (TS2305).
-- **`SchemaRegistry['kanban']` is `KanbanSchema`.** objectui#7645 (PR #7662)
- weakened the entry to `BaseSchema & { type: 'kanban' }` because this layer
- could not name the plugin's type; it now names the declaration the plugin
- itself imports. `keyof SchemaRegistry` — the published `ComponentType` union —
- is unchanged.
-- **The bare names return to this package with a different shape than they had
- before objectui#6172.** `KanbanSchema` here is now the plugin dialect, not the
- declarative one the pre-rename `KanbanSchema` was. A consumer that never
- migrated off the old bare name and expected `columns` to be required, or
- `draggable` to exist, gets a type error rather than a silent change.
-- `KanbanConditionalFormattingRuleSchema` is newly exported from
- `@object-ui/types/zod`: the rule union the `'object-kanban'` arm already
- applied, now shared with the `'kanban'` arm.
-
-**Migration.** Author boards in the plugin dialect — `objectName` + `groupBy`
-for an object-bound board, or `columns[].cards[]` with `badges` for a static
-one. Replace `DeclarativeKanbanSchema` imports with `KanbanSchema` (from
-`@object-ui/types`, or the Zod `KanbanSchema` from `@object-ui/types/zod`;
-`@object-ui/plugin-kanban` re-exports the same `KanbanSchema` type). Delete
-`draggable` (drag-and-drop is always on) and column `color` (style a lane
-through `className`). `content/docs/api/schema-reference.md`'s kanban section
-now documents this dialect.
-
-This is a breaking change shipped as `minor`: this repository's
-version-alignment rule keeps objectui's major pinned to `@objectstack`'s and
-ships objectui's own breaking changes as `minor` with the break spelled out in
-the changeset body, which is what the bullets above are.
diff --git a/.changeset/7664-plugin-kanban-declared-schema.md b/.changeset/7664-plugin-kanban-declared-schema.md
deleted file mode 100644
index 3c814de4e1..0000000000
--- a/.changeset/7664-plugin-kanban-declared-schema.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-kanban': patch
----
-
-`KanbanSchema` / `KanbanColumn` / `KanbanCard` / `CardTemplate` /
-`ColumnWidthConfig` are now the `@object-ui/types` declarations, re-exported
-from this package rather than declared in it (objectui#7664, maintainer ruling
-(a)). Nothing this package renders changed and every existing import keeps
-resolving; what changed is that `safeValidateSchema` in `@object-ui/types` now
-validates an authored `type: 'kanban'` document against this very shape, so a
-board that validates is a board these renderers draw.
-
-**The shape is not member-for-member what this package declared — it is that
-shape plus four members**, counted off `origin/main`'s
-`plugin-kanban/src/types.ts` (19 members on `KanbanSchema`, 6 on
-`KanbanColumn`, 7 on `KanbanCard`) against the `@object-ui/types`
-declarations:
-
-- **`onCardClick` is DECLARED for the first time.** This package's dialect
- never had the member, while `KanbanRenderer` has always forwarded
- `onCardClick={schema.onCardClick}` — an undeclared read (objectui#7742). It
- is declared here as a `#6124` RUNTIME SLOT: callable on the TypeScript face,
- refused by name on the mirror, like the `onCardMove` and `onQuickAdd` beside
- it in the same forward block.
-- **Four `?: never` tombstones** carry the retired declarative face's keys
- under the same `'kanban'` key so those spellings keep being refused by name:
- `draggable`, `onColumnAdd` and `onCardAdd` on `KanbanSchema`, and `color` on
- `KanbanColumn`. None of the four was ever a member of this package's dialect;
- each is refused, not silently accepted, because the retired face taught it.
- The full accept-set statement is on the sibling `@object-ui/types` entry.
diff --git a/.changeset/7667-toc-emphasis-flanking.md b/.changeset/7667-toc-emphasis-flanking.md
deleted file mode 100644
index c48aec0a36..0000000000
--- a/.changeset/7667-toc-emphasis-flanking.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/plugin-markdown': patch
----
-
-Fix `extractToc` eating the underscores out of a `SCREAMING_SNAKE` heading, so
-its `#id` links resolve to the heading they name again (objectui#7667).
-
-`stripInline()` ended with two hand-rolled emphasis rules that gave `*` and `_`
-one shared regex — `(\*\*|__)(.*?)\1` and `(\*|_)(.*?)\1`. Neither knew
-CommonMark's flanking rule, under which a `_` run INSIDE a word opens nothing:
-it is both left- and right-flanking with no adjacent punctuation, so it may
-neither open nor close emphasis. The renderer obeys that and keeps the
-underscores; the shared rule paired the first two underscores of
-`### NON_GRID_ROW_CEILING` and ate `GRID`, then resumed and ate `ROW`. The TOC
-said `nongridrow_ceiling` while `rehype-slug` put `non_grid_row_ceiling` on the
-anchor, so the entry rendered, was clickable, and silently went nowhere.
-
-The underscore form is now its own flanking-aware rule and the asterisk rules
-are left alone, because only `_` carries the intraword exemption — giving `*`
-the same one would break `a*b*c`, which the renderer really does emphasise.
-Underscore runs are matched whole (`(?`
-(`plugin-chatbot/src/index.tsx`) — the component the `chatbot` registration renders —
-declares `messages`, `placeholder`, `onSendMessage`, `disabled`, `showTimestamp`,
-`userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, `assistantAvatarFallback` and
-`maxHeight`, and **not one of the six**.
-
-| key | why not enforce | migration |
-| :-- | :-- | :-- |
-| `loading` | Chat progress is runtime state the chat runtime owns — the registration derives it from `useObjectChat` as `isLoading`. A static authored boolean would fight the runtime, not configure it | delete the key |
-| `showAvatars` | No target on ``. Declaring it on the two faces that DO reach `` would re-open by declaration the channel objectui#7708 closed by fence, one card earlier | delete the key — a `chatbot` node already renders an avatar beside every message |
-| `userAvatar` | A second authorable spelling of an image `userAvatarUrl` already carries (AGENTS.md #0.1: one strict contract, not N dialects) | `userAvatarUrl` (+ `userAvatarFallback`) |
-| `assistantAvatar` | Same | `assistantAvatarUrl` (+ `assistantAvatarFallback`) |
-| `markdown` | `` prints message content as text and has no markdown path; on the two nodes that render markdown, `enableMarkdown` is the live key | `type: 'chatbot-enhanced'` with `enableMarkdown` |
-| `height` | `` has no `height` prop, and the live `maxHeight` it forwards is a `string`, not this key's `string \| number` union | `maxHeight`, or `floatingConfig.panelHeight` on a floating node |
-
-## FROM → TO
-
-Each member goes to `?: never` on `packages/types/src/complex.ts` and to
-`retirementTombstone(...)` on `packages/types/src/zod/complex.zod.ts` — both halves, in
-lockstep, the convention `MarkdownSchema.sanitize` (objectui#6972),
-`TimelineSchema.timeScale` (objectui#6355) and `ObjectViewSchema.viewTabBar`
-(objectui#7779) already carry. Each refusal names the key, says why it is retired, and
-points at what to write instead; one string feeds both the parse-time message and the
-`.describe()` metadata, so the two cannot drift.
-
-## Accept-set change, one line per face
-
-- **TypeScript.** A write of any of the six against `ChatbotSchema` used to compile and
- now does not — including through a widened (non-fresh) value, which is the half a
- deletion would have missed on a `BaseSchema` carrier.
-- **Runtime (Zod / `safeValidateSchema`).** A `chatbot` document authoring one of the six
- used to parse **green** and now parses **red**, `invalid_type` at the key's own path
- with the guidance as the message. This is the narrowing that carries
- `needs:contract-review`.
-- **`chatbot-enhanced` / `chatbot-floating`.** Unchanged in both directions. Those faces
- never declared the six (objectui#7655 censused them out), their twins have no arm to
- refuse one, and `BaseSchema` is `.passthrough()` — so a stored node of either type
- carrying a retired key parses exactly as it did. Pinned.
-
-## Why tombstones and not deletions
-
-All six **have** a Zod arm, and that is what decides the route here. `BaseSchema` is
-`.passthrough()` on the Zod side and carries a `[key: string]: any` index signature on the
-TS side, so an UNDECLARED key is not refused — it is KEPT. Deleting the members would hand
-the authored spelling exactly the silent no-op this card exists to close, on both faces at
-once. The two-prong discriminator (`mobile.ts`, objectui#5941 / #7526 / #7678) leaves that
-structural hazard to the carrier: where there is no mirror there is "no silent-strip hazard
-for prong 2 to guard". Here there is a mirror to host the refusal, and prong 1 holds by the
-letter for four of the six. The "deleted" row is pinned live as a control in
-`packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts`, so the contrast
-cannot rot into prose.
-
-## Docs
-
-`content/docs/plugins/plugin-chatbot.mdx` gains the restatement of the six removed keys
-with their replacements (objectui#7070: a control is restated, never deleted into a
-vacuum), and its three present-tense claims that the `chatbot-floating` props spread is
-still unfiltered are corrected to what PR #8077 actually left behind.
diff --git a/.changeset/7704-schema-registry-chatbot-keys.md b/.changeset/7704-schema-registry-chatbot-keys.md
deleted file mode 100644
index aaf1ecec86..0000000000
--- a/.changeset/7704-schema-registry-chatbot-keys.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`SchemaRegistry` names all three `plugin-chatbot` registrations, so the published
-`ComponentType` union does too (objectui#7704).
-
-`packages/plugin-chatbot/src/renderer.tsx` registers three components — `chatbot`,
-`chatbot-enhanced` and `chatbot-floating` — and `SchemaRegistry` mapped one of them.
-Since `ComponentType = keyof SchemaRegistry` is the published union, a consumer
-discriminating on it was told two registered keys do not exist: an author narrowing a
-node by `ComponentType`, or writing a `Record` table, had no arm for
-either. The asymmetry that showed which half was wrong is that
-`packages/cli/src/utils/known-schema-types.ts` keeps its own parallel list containing
-both keys, precisely because this map did not.
-
-**Additive.** Two keys join the map under its `// Complex` group; no existing entry
-changes, `'chatbot'` still maps to `ChatbotSchema`, and nothing is removed. The union
-widens, which cannot break a consumer that produces `ComponentType` values and can only
-help one that consumes them — except an exhaustive `Record` or
-`switch`, which now needs the two new arms. Nothing in this repo has one: the only
-consumer of `ComponentType` outside its own declaration is a pin test.
-
-**Why the entries can be honest now.** This map's value has to be the type the
-registered renderer honours, and until objectui#7655 there was none to point at —
-`ChatbotSchema` pins `type` to `'chatbot'`, and each registration's real key set lived
-in an anonymous `ChatbotSchema & { … }` intersection local to the renderer file.
-objectui#7655 published `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` from this
-package, and both registrations already take them as their `schema` parameter, so the
-map's value and the renderer's prop type are one declaration — the same property the
-`'kanban'` arm gained in objectui#7664. Being declared here also makes them reachable:
-`@object-ui/types` has zero workspace dependencies, which is exactly what blocked the
-`kanban` case objectui#7645 measured, where the honoured type lived in a plugin.
-
-Pinned in `src/__tests__/schema-registry-chatbot-keys-7704.test.ts` in two channels —
-compile-time (`tsc -p tsconfig.test.json`: the keys survive in `keyof`, each value is
-the face its renderer honours, and each value's own `type` literal is its key) and
-runtime (a TypeScript-AST census of the interface source, plus each key selecting its
-own arm through `safeValidateSchema`).
-
-Scope: these two keys, whose authoring faces now exist — not a sweep of the map's other
-entries, which objectui#7665 holds.
diff --git a/.changeset/7708-chatbot-floating-spread-fence.md b/.changeset/7708-chatbot-floating-spread-fence.md
deleted file mode 100644
index 3ad24f5747..0000000000
--- a/.changeset/7708-chatbot-floating-spread-fence.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-chatbot': minor
----
-
-`chatbot-floating` now fences its `` spread the same way its
-two sibling registrations (`chatbot`, `chatbot-enhanced`) already do —
-`{...toDomProps(props)}`, at the head of the element, instead of a raw
-`{...props}` spread at the end (objectui#7708). This is a deliberate,
-user-visible behavior change, not a refactor:
-
-- **A message sent through a floating chatbot now actually renders.**
- Previously the authored `messages` seed (whatever array was on the node
- when it was authored) silently overrode the live runtime messages on every
- render, because the raw spread landed AFTER `messages={runtimeMessages}`.
- Neither the user's own message nor an `autoResponse` reply ever appeared —
- the identical send on `chatbot-enhanced` worked correctly. Fixed.
-- **`displayMode`, `systemPrompt` and `model` stop leaking as DOM attributes**
- on the panel's root element (`systemPrompt` / `model` are still read
- normally, by name, for the request they configure — only the second,
- unfiltered forward is gone). Closes objectui#4425's leak class on the one
- `plugin-chatbot` registration that had not closed it yet.
-- **Three undeclared keys go dark on `chatbot-floating` nodes:**
- `processVisibility`, `surface` and `showAvatars` reached the panel's
- `ChatbotEnhanced` through the raw spread even though `ChatbotFloatingSchema`
- never declared them. `ChatbotFloatingSchema` documents this explicitly and
- always has — the face never promised these keys — so this closes an
- accidental channel rather than removing declared behavior. A document that
- relied on any of the three to affect a floating node loses that effect;
- author them on a `chatbot-enhanced` node instead, where they are part of
- the declared, tested contract.
-
-`@object-ui/types`: `ChatbotFloatingSchema`'s doc comment is updated to match
-— no type-shape change, so nothing that imports the type needs to change.
diff --git a/.changeset/7709-edit-mode-chip-sample-data.md b/.changeset/7709-edit-mode-chip-sample-data.md
deleted file mode 100644
index f9a759722d..0000000000
--- a/.changeset/7709-edit-mode-chip-sample-data.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/i18n': minor
-'@object-ui/app-shell': minor
----
-
-**The maker's edit-mode starter offers sample data, not an automation v1 cannot
-build (objectui#7709).** Bound to an existing app (`?package=`), the maker's
-empty state offered four starters: add a field, add an object, add a dashboard,
-and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and
-notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the
-measured behaviour on the sibling chips was not a refusal but a silent degrade
-into a view — so the product recommended an automation and would have handed
-back a page.
-
-Rewording it was not available: asking for a field, a view or a dashboard
-duplicates one of the three chips beside it. The fourth chip is now
-`addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all
-ten packs and in the call-site `defaultValue` fallback, which is a second copy
-of the same string. The three surviving chips all add STRUCTURE; what an app
-that already has objects most often lacks is DATA, and `seed` is on v1's
-authoring whitelist. A note beside the keys in every pack and at the call site
-says this chip's automation wording comes back when ADR-0112 v2 re-adds flows
-and actions, and the retired sentence for each pack is kept in the guard suite
-so v2 has it verbatim.
diff --git a/.changeset/7711-calendar-filter-is-not-a-config-slot.md b/.changeset/7711-calendar-filter-is-not-a-config-slot.md
deleted file mode 100644
index 080975d209..0000000000
--- a/.changeset/7711-calendar-filter-is-not-a-config-slot.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-'@object-ui/plugin-calendar': minor
----
-
-Retire the `filter.calendar` configuration spelling on `object-calendar` (objectui#7711).
-
-**Breaking, deliberately.** `getCalendarConfig` no longer probes `schema.filter` for a
-`calendar` key. A calendar whose configuration was written as
-`filter: { calendar: { startDateField: … } }` no longer resolves a configuration at all
-and now renders the component's existing "Calendar configuration required. Please
-specify startDateField and titleField." refusal screen. Write the configuration under
-the declared `calendar` container instead — the read for it already existed, directly
-below the retired arm.
-
-`filter` is the query filter and nothing else. `@objectstack/spec`'s
-`ComponentPropsMap['object-calendar']` declares `calendar` as the configuration
-container and `filter` as the base query filter, and admits no `filter.calendar`
-spelling; the renderer nevertheless read the config out of the filter FIRST, and the
-comment on the canonical read below called *that* one the "backward compatibility"
-branch — the contract inverted in a source comment. The arm and the comment are both
-gone. No compatibility rung and no deprecation window, per AGENTS.md #0.1: a tolerant
-fallback fossilizes the wrong convention into a second de-facto contract.
-
-**The bug this closes is not only the retired spelling.** One authored key was being
-read twice with two incompatible meanings. `filter: { calendar: 'team' }` — a
-legitimate condition on a field literally named `calendar` — was returned as the
-`CalendarConfig` while the same object still went to `$filter` on the wire. `'team'` is
-truthy, so the refusal screen did not fire either: `startDateField` destructured off a
-string is `undefined`, every record fell into "unscheduled", and the grid rendered empty
-while the author's declared `calendar` block sat unread. Such a filter now reaches
-`$filter` untouched and the declared container is what configures the calendar.
-
-The calendar twin of the `filter.map` retirement in objectui#4034, with one measured
-difference: there is no `Array.prototype.calendar`, so `'calendar' in schema.filter` was
-never true for an array-shaped filter the way `'map' in schema.filter` was true for
-every one of them. Only an object-shaped filter with an own `calendar` key ever reached
-the retired arm, and the calendar answers the loss of a configuration with a named
-refusal on screen rather than the map's silent fallback to default field names — so this
-retirement carries no dev-time diagnostic rider.
-
-**Migration.** A census of this repo found **zero** authored `filter: { calendar: … }`
-sites (both the object-literal and JSON spellings, with positive controls and a planted
-fixture proving the search fires).
-
-Also in this change: the `calendarConfig` memo's dependency list drops `schema.filter`,
-which the function no longer reads, and gains the three flat keys it does read but the
-list never named — `startDateField`, `endDateField` and `allDayField`. Dropping `filter`
-removed an accidental co-trigger that used to pick those up whenever a filter changed
-alongside them.
diff --git a/.changeset/7712-kanban-calendar-filter-input.md b/.changeset/7712-kanban-calendar-filter-input.md
deleted file mode 100644
index 4a375fe6bd..0000000000
--- a/.changeset/7712-kanban-calendar-filter-input.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/plugin-calendar': minor
-'@object-ui/plugin-kanban': minor
----
-
-Declare the `filter` input on every `object-kanban` / `object-calendar`
-registration (objectui#7712) — the html tier stops reporting `unknown-prop` on a
-key the spec declares and both renderers read.
-
-`ObjectKanban.tsx` sends the authored key to the query as `$filter: schema.filter`
-and `ObjectCalendar.tsx` does the same, and `@objectstack/spec`'s
-`ComponentPropsMap` declares `filter` on both blocks (measured: `safeParse`
-accepts it, and refuses an undeclared key by name on the same call). But none of
-the four registrations that publish those two renderers listed `filter` in
-`inputs`, and `sdui-parser`'s `validateTree` reports `unknown-prop` for every key
-no `inputs` entry claims. So an author writing the one spelling that WORKS was
-told it was unknown — objectui#6678's shape, where a correct write draws the same
-diagnostic as a write that does nothing. That is worse than an inert key: it
-actively punishes the correct behaviour, and the honest response to it is to
-delete working metadata.
-
-ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has
-live readers on both ends, so the registrations were the side that was wrong. The
-declaration is `type: 'array'` on all four, matching the `filter` that
-`object-grid`'s `GRID_QUERY_INPUTS` and `object-metric` already publish, and it is
-writable as one shape only because objectui#7711 landed first and retired the
-object-shaped `filter.calendar` spelling — `filter` is the query filter and
-nothing else.
-
-Declared per key against the spec rather than derived from the
-`ElementDataSourceMapping` sitting beside these registrations, even though that
-mapping already asserts `filter` is a live query key. Measured, that derivation
-would be wrong: the kanban mapping also carries `limit`, and the spec's strict
-`ComponentPropsMap['object-kanban']` **rejects** `limit` by name, so emitting it
-would publish a key the save gate refuses.
-
-⚠️ Note for whoever meets this class next: `check:react-blocks-declaration-parity`
-runs manifest → spec, one direction. A key the SPEC declares and the manifest
-omits is structurally outside what that ratchet measures, so fixing these four
-registrations does **not** make the next omission loud. Making that ratchet
-bidirectional is its own card.
diff --git a/.changeset/7714-lookup-draft-stays-client-side.md b/.changeset/7714-lookup-draft-stays-client-side.md
deleted file mode 100644
index 7672973a99..0000000000
--- a/.changeset/7714-lookup-draft-stays-client-side.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-designer': minor
----
-
-A half-filled relationship field stays in the client and is never PUT (objectui#7714,
-maintainer ruling on objectui#7122 item 4, 2026-09-05).
-
-**Breaking, deliberately, and stated rather than implied.** Both metadata writers —
-`MetadataService.saveFields` / `saveObject` in `@object-ui/app-shell`, and
-`MetadataFieldsPage` in `@object-ui/plugin-designer` — now REFUSE a `lookup` or
-`master_detail` field whose `reference` is missing, empty, blank or not a string. A
-caller that previously got a PUT now gets a thrown error and **no request at all**. The
-refusal is raised while the wire `fields` map is being built, so nothing is sent and
-nothing is partially applied.
-
-**Why the draft may not leave the client.** `@objectstack/spec` 17.3.0 turned
-`reference` from prose into a hard requirement on relationship types (a `custom`
-refinement at path `reference`). Driven against a real 17.3.0 backend in a running
-designer: creating a `lookup` and leaving its target empty PUT the whole object,
-came back `422 INVALID_METADATA` at `fields..reference` — and then the NEXT
-edit, to a different and already-saved field, was refused identically, because the
-half-filled draft rides along inside the same document. The author sees that later
-edit rendered as applied while the server has none of it, and the only escape that
-does not require noticing the lookup is a reload, which discards the work. An editing
-session's half-finished state belongs to the client, not to the metadata store.
-
-⛔ **Not** "strip the incomplete field from the body and report a successful save".
-That shows the author a field the server never received — the silent-drop shape
-objectstack#4001 closed.
-
-⚠️ **A declared divergence: objectui is stricter than the contract here.** The
-predicate is `typeof reference === 'string' && reference.trim() !== ''`. Measured on
-the 17.3.0 artifact, at field level and again through `ObjectSchema`, the spec
-ACCEPTS a whitespace-only `reference` while refusing an absent or empty one — so
-`reference: ' '` is refused by these writers on their own authority, not the
-platform's. Kept deliberately: whitespace names no object (the spec's own
-`ObjectSchema.fields` key grammar `/^[a-z_][a-z0-9_]*$/` admits no whitespace-bearing
-name), so admitting it only moves the identical failure past the PUT and into a stored
-document, where it surfaces with no field named. Filed upstream as objectstack#16126;
-if the spec trims, behaviour here is unchanged and only the declaration retires.
-
-The refusal message diagnoses which of the four states it found — absent, empty,
-non-string (`invalid_type`, a value of the wrong kind rather than a missing target),
-or blank — because the repair and the consequence differ per state.
-
-`minor` rather than `major` per `AGENTS.md`: objectui's major tracks `@objectstack`'s,
-so objectui's own breaking changes ship as `minor` with the breaking semantics stated.
diff --git a/.changeset/7721-actions-reader-census.md b/.changeset/7721-actions-reader-census.md
deleted file mode 100644
index 538069fd48..0000000000
--- a/.changeset/7721-actions-reader-census.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
----
-
-Correct a measured-false sentence in the pending objectui#7344 changeset, and widen the
-reader census that produced it (objectui#7721).
-
-`.changeset/7344-handler-string-any-mirrors.md` said `AppAction.onClick` retires because
-"nothing reads `AppComponentSchema.actions[]`". The array IS read — `@object-ui/runner`'s
-`LayoutRenderer` renders its `'button'` arm as toolbar buttons and its `'user'` arm as an
-avatar dropdown. The retirement itself is correct and unchanged: what no reader touches is
-`onClick`, on the action or on `items[]`. That sentence was future published
-`@object-ui/types` CHANGELOG copy, so it is corrected before the next release rather than
-after, when the CHANGELOG is history and no longer editable.
-
-The census in `handler-keys-string-any-mirrors-7344.test.ts` was the origin: it scoped to
-`@object-ui/layout`, `@object-ui/app-shell` and the console, which made it literally true
-and its conclusion false. It is now an assertion whose population is read off the tree, so
-a reader appearing in a package nobody listed fails the pin instead of narrowing the claim
-in silence.
-
-**Nothing published changes**: no accept set moves, no exported symbol changes, no
-TypeScript face moves. The edits are one parenthetical in an unreleased changeset and one
-pin's comment plus its new census — which is why this declares no bump. The `minor` the
-objectui#7344 changeset already declares for `@object-ui/types` is the release this copy
-belongs to.
diff --git a/.changeset/7722-wrapper-class-five-more.md b/.changeset/7722-wrapper-class-five-more.md
deleted file mode 100644
index c5d11dcb0d..0000000000
--- a/.changeset/7722-wrapper-class-five-more.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Declare `wrapperClass` on `SwitchSchema`, `TextareaSchema`, `DatePickerSchema`,
-`SelectSchema` and `ListSchema`, on both faces (objectui#7722 — the read-driven
-residue outside objectui#6938's batch, one key over five more types).
-
-Each of `renderers/form/switch.tsx`, `textarea.tsx`, `date-picker.tsx`,
-`select.tsx` and `renderers/data-display/list.tsx` reads `schema.wrapperClass`
-onto its wrapper element, and neither the TypeScript interface (`form.ts`,
-`data-display.ts`) nor the zod mirror (`zod/form.zod.ts`,
-`zod/data-display.zod.ts`) declared the key. The reads compiled through
-`BaseSchema`'s index signature (objectui#5155) and the values parsed through
-`.passthrough()`, admitted unexamined. The same key, on the same class of read,
-is declared on `CheckboxSchema` (objectui#6938), `FileUploadSchema` and
-`FilterBuilderSchema` (objectui#6150); these five were left out only because
-their doc pages never listed it.
-
-**minor, not patch — the published face gains five members.** objectui#6938 and
-objectui#7295 graded a one- or two-key residue `patch` because "the accept set
-only widens toward what already renders"; that reasoning still describes the
-VALUE dimension here, but this change is the batch shape of objectui#6150
-(`minor`), and it is dispatched under the contract-review tier precisely because
-it widens the PUBLISHED surface: five schemas each gain a member of the shipped
-`.d.ts` and of the mirror's `.shape` that an editor completes, an annotation
-checks and a validator enforces. Two verdicts move, in opposite directions:
-
-- **Nothing well-typed stops validating or compiling.** Key membership was never
- narrow: `[key: string]: any` admitted the key on the TS face and
- `.passthrough()` admitted it on the zod face, so every document that carried a
- string `wrapperClass` parsed green before and parses green now, with the value
- surviving the parse exactly as before.
-- **A non-string `wrapperClass` is now REFUSED at the key** on these five mirrors
- (`{ type: 'switch', wrapperClass: 42 }` parsed green before; it is refused now,
- at `wrapperClass`). That is enforcement of the declared type, not a new
- capability, but it is a behaviour change for a document that carried a
- wrong-typed value under one of these five names — a value the renderer would
- have interpolated into the class string as text.
-
-Keys outside the five are untouched: an undeclared key of any type is still
-admitted unexamined on all five mirrors, pinned per mirror with a control key
-the renderer does not read. `InputSchema.wrapperClass`, declared on the TS face
-only, is a recorded row of the parity ledger (`UnmirroredDeclared`) and stays
-there; the new sweep pin carries it as a self-expiring exemption.
diff --git a/.changeset/7735-zod-mirrors-stop-authoring-defaults.md b/.changeset/7735-zod-mirrors-stop-authoring-defaults.md
deleted file mode 100644
index 8944d97e34..0000000000
--- a/.changeset/7735-zod-mirrors-stop-authoring-defaults.md
+++ /dev/null
@@ -1,74 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': patch
----
-
-The zod mirrors stop authoring defaults; the renderer's fallback is the one
-authoritative default (objectui#7735, director ruling, decision batch #69,
-2026-09-07 — maintainer reply 「其他同意」).
-
-**A validator validates; it does not write values into an author's document.**
-`.default(v)` on a mirror member is not documentation — `parse` SUBSTITUTES `v`
-into the output when the key is absent. One authored document therefore had two
-shapes depending on whether it had been through `safeValidateSchema`, and where
-the mirror and the renderer disagreed the mirror silently won:
-
-| key | mirror wrote | the renderer applies |
-|---|---|---|
-| `ContainerSchema.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` |
-| `FlexSchema.align` | `'center'` | `flex.tsx`: `\|\| 'start'` |
-
-A `container` omitting `maxWidth` rendered `max-w-xl` as authored and `max-w-lg`
-after a round-trip through the mirror; `'center'` is a value neither `flex` nor
-`stack` applies at all. `StackSchema` already declared no defaults, so the file
-was not even self-consistent.
-
-**What changed.** All 41 `.default()` call sites under `packages/types/src/zod/`
-are removed — `layout.zod.ts` 22, `crud.zod.ts` 11, `form.zod.ts` 5,
-`views.zod.ts` 2, `app.zod.ts` 1. `@object-ui/components` reconciles the third
-face objectui#8229 found: `flex`'s registration `defaultProps.align` seeded
-`'center'`, the value its own renderer never applies, so a designer-made node
-laid out differently from a hand-authored one; it now seeds `'start'`.
-
-**Accept set unchanged.** Every one of the 41 was spelled
-`.optional().default(v)`, so none was carrying optionality: nothing that
-parsed before is refused now, and nothing refused before is accepted. This is a
-narrowing of `parse` OUTPUT only.
-
-**⚠️ Breaking for consumers that read `result.data`.** `validateSchema` /
-`safeValidateSchema` no longer substitute: for a document that OMITS one of these
-keys the key is now ABSENT from the parsed output instead of carrying a value the
-document never authored. Every one of the 41 keys changes output this way. If you
-read a default off the parsed document rather than applying your own fallback,
-apply the fallback yourself.
-
-**And rendering changes too, wherever the mirror's value differed from the
-renderer's.** An earlier draft of this note said rendering does not change on the
-ground that "the two keys that disagreed were the bug"; that was the triage
-sample of 7 keys, not a measurement of the population. Auditing all 22 layout
-keys against their renderers finds **six** disagreements, not two:
-
-| key | the mirror wrote | what the renderer applies when the key is absent |
-|---|---|---|
-| `container.maxWidth` | `'lg'` | `container.tsx`: `?? 'xl'` |
-| `flex.align` | `'center'` | `flex.tsx`: `\|\| 'start'` |
-| `grid.columns` | `3` | `grid.tsx`: `let baseCols = 2` |
-| `text.variant` | `'body'` | `text.tsx`: none — a bare node, no typography class (objectui#6942) |
-| `resizable.withHandle` | `true` | forwarded bare; `undefined` draws NO grip |
-| `page.template` | `'default'` | `page.tsx`: a null template falls through to the `pageType` dispatch |
-
-So a consumer that renders `result.data` sees a different result on all six. What
-does NOT change is rendering inside this repository: the render path never ran
-this validator at all — `SchemaRenderer` calls the STRUCTURAL `validateSchema`
-from `@object-ui/core`, which returns `{valid, errors}` and hands the renderer
-the author's own object. The four modules that do import `@object-ui/types/zod`
-were censused: two read only `result.success`, one discards `result.data`, and
-`os validate` reads only `type` / `id` / `label` / `title` / `children`, none of
-which ever carried a default. The four JSDoc `@default` tags that described the
-mirror rather than the renderer (`grid.columns`, `text.variant`,
-`resizable.withHandle`, `page.template`) are corrected in the same change.
-
-Note that `safeValidateSchema` still substitutes through subschemas imported by
-reference from `@objectstack/spec` (`app.active`, `object-view.navigation.*`,
-`list-view.sharing.type`, and others). Those values are written in that package,
-not this one, and are unaffected by this change.
diff --git a/.changeset/7741-list-import-mappings-discriminate.md b/.changeset/7741-list-import-mappings-discriminate.md
deleted file mode 100644
index 18530a9e29..0000000000
--- a/.changeset/7741-list-import-mappings-discriminate.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/data-objectstack': minor
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-`listImportMappings` no longer renders a refused door as "no mapping is registered"
-(objectui#7741).
-
-`ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the
-import wizard hides its saved-mapping selector on an empty list. So "the server served
-zero mappings" and "the server refused, or broke" produced the identical UI on every
-deployment — the feature simply absent — with a `console.warn` as the only
-discriminator, in the browser console, with nothing pointing at it. That silence did not
-merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter
-(objectstack#14026 was filed, routed and worked by two seats against a wizard that had
-been correct since `@object-ui/data-objectstack@17.1.0`).
-
-**The empty-list return is unchanged.** `listImportMappings` still answers
-`Promise` and still never throws, on every arm including the loud ones — this is
-a channel added ALONGSIDE that contract, not a change to it.
-
-- **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe
- channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a
- metadata read failed in a way that is NOT the supported "this deployment does not
- serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the
- object, whether the server `refused` this caller or the answer was `unreadable`, and
- the server's own ADR-0112 code, HTTP status and message.
-- **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported
- so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code`
- first, the status only where no code was declared — and never "is the result an empty
- array", which is what both conditions produce and so can never tell them apart.
-- **The older-server case stays quiet.** A deployment that does not serve the `mapping`
- kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list
- door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no
- event. That is a real, supported deployment shape and it must not become a visible
- fault.
-- **The console now says so.** `AdapterProvider` subscribes to the new channel and
- renders a warning toast naming the object, the remedy and the server's own words, so a
- user without devtools open can tell "there are none" from "we could not find out".
- Three new `console.importMappings*` keys ship in all ten locale packs.
-
-This applies framework #13906 decision 1 option A — *a thing that could not be READ is
-not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not
-a new principle.
diff --git a/.changeset/7744-chart-type-internal-only.md b/.changeset/7744-chart-type-internal-only.md
deleted file mode 100644
index 3d78e64162..0000000000
--- a/.changeset/7744-chart-type-internal-only.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
----
-
-`@object-ui/plugin-charts`: documentation and a pin only — no published behaviour changes.
-`normalizeSeries` returns the identical output for the identical input (measured: 336 input
-cases, byte-identical JSON before and after; the probe was itself shown sensitive — a reader
-reorder moves 49 of those 336).
-
-`normalizeSeries` resolves the per-series family as `str(raw.chartType) ?? str(raw.type)` —
-the internal spelling FIRST. `chartType` is the carrier of the renderer's internal `dataKey`
-series shape (`ChartRenderer`'s `series?` union declares it on that arm and on no other);
-the AUTHORING face refuses it by name via `ChartDataSeriesSchema` (objectui#7694 / PR #7737).
-Because the one function normalizes both shapes, an author who skips validation still gets
-`chartType` honoured at runtime while the validator refuses that same key — the two faces
-disagree.
-
-objectui#7744 ruled that the split is annotated and pinned, NOT retired at the reader:
-retiring the limb would change what the internal producers (`DashboardRenderer`, `ObjectView`,
-the dataset path) render, which is a reader-side decision of its own. So the limb keeps its
-behaviour and gains a docblock that delegates the refusal's rule to where it lives, plus
-`chartType-internal-only-7744.test.ts`, which pins the RELATIONSHIP the two files each held
-half of: the key the validator refuses is the key the reader prefers. Bringing the faces into
-agreement from either side now goes red and names the side that moved.
diff --git a/.changeset/7745-formatdate-options-style.md b/.changeset/7745-formatdate-options-style.md
deleted file mode 100644
index 4a9296ffbe..0000000000
--- a/.changeset/7745-formatdate-options-style.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/core': patch
----
-
-`formatDate` reads `options.style` (objectui#7745).
-
-`DateDisplayOptions` is the one bag `formatDate` / `formatRelativeDate` /
-`formatDateTime` share. `style` was added to it for `formatDateTime`'s `'compact'`
-grid face (objectui#7443, PR #7621) and only `formatDateTime` read it, so on
-`formatDate` the key was inert — and inert beside a POSITIONAL parameter of the same
-name. `formatDate(v, undefined, { style: 'short', locale: 'en-US' })` rendered
-`Jul 4, 2024`, the default face, with no diagnostic; it now renders `Jul 4, '24`.
-This is the additive half of the maintainer's long-run ruling on objectui#7443:
-both functions accepting `options.style`.
-
-**The precedence is pinned: the positional argument wins.** `options.style` is
-consulted only when the positional slot is `undefined` (`??`, not `||`, so `''`
-still counts as given). That is the only direction that is purely additive — it
-fires exactly on the input that is a silent no-op today, so no call that renders a
-face today renders a different one after. The reverse would let a key aimed at a
-SIBLING function outrank an argument written for this call: the bag is shared, and
-carrying `{ style: 'compact', locale }` built for `formatDateTime` into
-`formatDate(v, 'short', bag)` must not cost that call its short face.
-
-**What changes for you.** Only `formatDate(value, undefined, { style: 'short' | 'relative' })` —
-a call that silently rendered the default face before. Every call that passes the
-style positionally, and every `formatDateTime` / `formatRelativeDate` call, renders
-byte-identically to before.
-
-`formatRelativeDate` still does NOT read `style`; the ruling names `formatDate`
-only. Its out-of-window fallback to `formatDate` strips the key so that the new read
-cannot leak in through the delegation — which also keeps
-`formatRelativeDate(v, { style: 'relative' })` from recursing.
diff --git a/.changeset/7747-datetime-format-cast-narrowing.md b/.changeset/7747-datetime-format-cast-narrowing.md
deleted file mode 100644
index b9f09b92e6..0000000000
--- a/.changeset/7747-datetime-format-cast-narrowing.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
----
-
-Narrow the `DateTimeCellRenderer` style cast from `as any` to the subtype that
-actually declares the property (objectui#7747).
-
-`const style = (field as any)?.format || 'compact'` becomes
-`(field as DateTimeFieldMetadata | undefined)?.format`. Some cast is
-load-bearing — `FieldMetadata` is a 37-member union and `BaseFieldMetadata`
-carries no `format`, so the bare read is `TS2339` — but `as any` was wider than
-the job: `DateTimeFieldMetadata` is exported from `@object-ui/types`, which this
-file already imports from, so the narrow cast was available at zero cost. The
-difference that buys: `as any` would also have silenced a typo in the property
-name, and the narrow cast will not.
-
-Type-level only; no package is released by this change. The emitted expression
-is identical, so every cell renders exactly what it rendered before —
-`'compact'` and an absent or empty `format` keep the compact face, and any
-other value keeps selecting the verbose `formatDateTime` default.
diff --git a/.changeset/7752-analytics-where-lowering.md b/.changeset/7752-analytics-where-lowering.md
deleted file mode 100644
index 2ec9ef7aea..0000000000
--- a/.changeset/7752-analytics-where-lowering.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'@object-ui/data-objectstack': minor
----
-
-Array filters on analytics aggregates were posted un-lowered and refused by the runtime with 400; they are now lowered to the canonical `FilterCondition` before the wire.
-
-An `element:number` or an `object-metric` whose filter is authored as an array (`[{ field, operator, value }, ...]`, a comparison tuple, or an `and`/`or` group) reached `POST /analytics/query` as an array. That route parses the body with `AnalyticsQueryRequestSchema` first, and its `where` is a `FilterCondition`, so the widget answered `400 Invalid AnalyticsQuery body: where: ...` instead of its number — leaving the MongoDB-style record as the only authoring form that still worked.
-
-`aggregate()` now lowers the array through `parseFilterAST`, the single sink `@objectstack/spec` names for turning a `FilterArray` into a `FilterCondition`, so the posted `where` is the shape the wire declares. An empty array posts no `where` at all, a record-shaped filter is unchanged, and an array the sink cannot lower — an infix join such as `[condA, 'or', condB]` — is refused with this adapter's `INVALID_FILTER` / 400 error rather than posted or silently dropped into an unfiltered aggregate.
diff --git a/.changeset/7767-collapsible-trigger-union.md b/.changeset/7767-collapsible-trigger-union.md
deleted file mode 100644
index 508d6c78c8..0000000000
--- a/.changeset/7767-collapsible-trigger-union.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**`CollapsibleSchema.trigger` now declares the node array its Zod mirror, its runtime and its own shipped default already accept** (objectui#7767).
-
-`trigger` on `CollapsibleSchema` widens from `string | SchemaNode` to `SchemaNode | SchemaNode[]` on the TypeScript face, and stays required. The dropped `string |` half was redundant rather than an extra: `SchemaNode` is `BaseSchema | string | number | boolean | null | undefined`, so `string | SchemaNode` already denoted exactly `SchemaNode` — a bare string trigger type-checks before and after. `SchemaNode` itself is unchanged.
-
-This is a **widening**, not a replacement: every singular `trigger` keeps type-checking unchanged. The Zod mirror is untouched — `zod/disclosure.zod.ts` already spelled this key `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — and so is the runtime: `renderers/disclosure/collapsible.tsx` hands `schema.trigger` to `renderChildren`, whose `Array.isArray` branch has served the array form all along, and that same registration's `defaultProps.trigger` ships as an array. What changes is that the TypeScript face stops under-reporting an accept set that already ships: copying the renderer's own default into a typed document is no longer a type error against the type that shipped it. The docs page's `trigger` row follows the declaration.
-
-This is the eighth member of the change objectui#7081 made to the overlay family, carried out under the same ruling (2026-09-03 on that card): the validator's accept set does not move, so this is a declaration catching up with what ships rather than a new capability. Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略").
diff --git a/.changeset/7772-page-block-kanban-group-by-control.md b/.changeset/7772-page-block-kanban-group-by-control.md
deleted file mode 100644
index 1427679fc8..0000000000
--- a/.changeset/7772-page-block-kanban-group-by-control.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-**Breaking for the page designer's output:** the page-block inspector's `object-kanban`
-panel now authors `groupBy` (the key the board reads) instead of `groupField` (the key it
-never read), and offers the `limit` control the schema has declared since objectui#7322.
-
-**What was measured, on this branch's base (`0c8dbc49`).** `BLOCK_CONFIG['object-kanban']`
-offered exactly four controls — `objectName` / `groupField` / `titleField` / `cardFields`.
-`ObjectKanbanSchema.groupBy` is REQUIRED on both published faces (`objectql.ts:2765
-groupBy: string`, no `?`; `objectql.zod.ts:1001 groupBy: z.string()`, no `.optional()`) and
-had **no control at all**, while `groupField` — the only control able to set grouping — has
-been a `retirementTombstone()` on the Zod face and `?: never` on the TS face since
-objectui#7322. `ObjectKanban.tsx` reads `schema.groupBy` at thirteen sites and `groupField`
-at zero (control: those same thirteen hits in the one query, so the zero is a reading).
-
-Run against the node those four controls produced, `ObjectKanbanSchema.safeParse` reported
-**two** issues, not one: `groupBy` "expected string, received undefined", and `groupField`
-carrying the retirement guidance verbatim. So this panel could not author a valid
-`object-kanban` node however it was filled in, and the board it produced grouped nothing
-with no diagnostic on any face. Both halves are now pinned against the schema itself rather
-than against a spelling, in `previews/__tests__/block-config.test.ts` —
-`scripts/check-designer-field-key-parity.mjs` judges `PAYLOAD_SHAPES` and does not read
-`BLOCK_CONFIG`, so nothing mechanical was watching this table.
-
-`limit` is not polish: `ObjectKanban.tsx` sends it as a real `$top`
-(`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and renders every fetched record into a lane
-with no pagination, so a board over 100 records was silently truncated with no way to widen
-it. The new number box states `100` — `DEFAULT_KANBAN_LIMIT`, read from the renderer's own
-source by its pin — as the value that applies while it is empty.
-
-**Stored pages.** Documents saved by every released build since
-`@object-ui/app-shell@17.5.0` can carry `properties.groupField` on an `object-kanban` block.
-The inspector's read door now strips it (`stripRetiredBlockProps`, keyed to
-`RETIRED_BLOCK_PROP_KEYS`), so an edit-and-save round-trip comes out without the retired
-key — no migration pass, and no blanket unknown-key purge: every other key the designer
-does not render still survives. It is a strip and **not** a migration into `groupBy` on
-purpose: reading the old spelling into the new box would be a second de-facto alias
-(AGENTS.md #0.1) for a value no board ever acted on.
-
-The **view-level** `kanban.groupField` alias (`core/src/utils/normalize-list-view.ts`) is
-live and untouched — node-level retirement and the view-level alias are different things.
-
-The two locale entries move with the control (`engine.inspector.pageBlock.field.object-kanban.groupBy`,
-plus a new `.limit`); the English wording is unchanged, so the panel reads the same as it
-always did.
diff --git a/.changeset/7773-kanban-adapter-groupfield-write.md b/.changeset/7773-kanban-adapter-groupfield-write.md
deleted file mode 100644
index 3e50cbb8b0..0000000000
--- a/.changeset/7773-kanban-adapter-groupfield-write.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-'@object-ui/plugin-view': minor
-'@object-ui/plugin-list': minor
----
-
-The two kanban adapters stop writing the retired `groupField` onto the
-`object-kanban` node they generate (objectui#7773). `groupBy` — the key the
-renderer actually reads — is unchanged and is now the only lane key emitted.
-
-**What was measured, on this branch's base (`a915064e`).** Both adapters emitted
-the key twice:
-
-```
-packages/plugin-view/src/ObjectView.tsx:1362 groupField: groupBy,
-packages/plugin-list/src/ListView.tsx:2500 groupField: laneField,
-```
-
-The `object-kanban` renderer never read it: `groupField` has ZERO hits anywhere
-under `packages/plugin-kanban/`, against a control of thirteen `schema.groupBy`
-read sites in `ObjectKanban.tsx` from the same query — so the zero is a reading,
-not a blind grep. The write was inert; the board grouped by `groupBy` and
-`groupField` rode along unread.
-
-**Why it is removed rather than tolerated.** objectui#7322 RETIRED
-`groupField` on this node on both published faces — `groupField?: never` on the
-TypeScript interface and a `retirementTombstone()` in the Zod mirror, which
-refuses an authored value BY NAME. So the adapters were producers emitting a
-node their own published contract rejects. That was harmless only because a
-generated node never reaches the mirror at runtime (`SchemaRenderer` runs the
-structural `validateSchema`, not `safeValidateSchema`) — but the CLI's
-`os check` / `os validate` DO run the mirror, so the identical node was already
-refused when authored by hand and admitted when generated. This closes that
-split.
-
-**What changes for a consumer.** Nothing on any documented path: the renderer's
-behaviour is byte-identical, because it never read the key. A host that
-registers its own `object-kanban` component and reads `props.schema.groupField`
-off the generated node now reads `undefined` — read `groupBy` instead, which
-carries the same value and always did. Graded `minor` rather than `patch` for
-exactly that narrowing, following the repo convention that objectui's own
-breaking changes ship as `minor` (AGENTS.md 版本号策略, mechanically enforced by
-`scripts/check-changeset-no-major.mjs`).
-
-**Who is NOT affected — the boundary is node-local.** Every VIEW-LEVEL
-`groupField` read is untouched and still live: it is a legacy alias of the
-spec's `groupByField` on the kanban *view config*, mapped by
-`normalize-list-view.ts`, and both adapters still resolve lanes through it
-(`ObjectView.tsx`'s `kanbanCfg.groupField ||`, `ListView.tsx`'s
-`groupByField || groupField`). Authoring `options.kanban.groupField` on a
-`list-view` or `object-view` keeps working exactly as documented in
-`packages/plugin-list/README.md`. `groupField` is dead only on the generated
-`object-kanban` NODE.
-
-The two tests that pinned the duplicate write are TURNED, not deleted — they now
-assert the key is absent, so restoring the write reddens them instead of being
-silently re-blessed by a missing assertion.
diff --git a/.changeset/7786-storage-spy-counter-probes.md b/.changeset/7786-storage-spy-counter-probes.md
deleted file mode 100644
index 2d931d93f7..0000000000
--- a/.changeset/7786-storage-spy-counter-probes.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
----
-
-Test-only repair of two `Storage.prototype` spy instruments that graded green
-without measuring what their names claimed (objectui#7786). No package is
-released by this change and no runtime source is touched.
-
-- `apps/console/src/__tests__/bootSplash.test.ts` — `it('survives a browser
- with no localStorage access')` injected its fault with
- `vi.spyOn(Storage.prototype, 'getItem')`, which never reached the script
- under test: happy-dom hands `localStorage` out through a proxy that has
- already bound `getItem`, and `vitest.setup.base.ts` may swap the store for a
- plain object that never inherited from `Storage.prototype` at all. Measured
- on Node 22.22.2, the version CI pins, a direct `localStorage.getItem(...)` in
- that test did not throw — so the case asserted that an ordinary boot does not
- throw, under a name promising a blocked browser. The fault is now injected
- where the script resolves it (the property-replacement shape
- `packages/i18n/src/__tests__/provider-locale-persistence.test.tsx` already
- uses against the same hazard), a counter-probe asserts the throw is
- observable before anything is graded on its absence, and a stored `dark`
- choice makes the expected `light` outcome reachable only when storage is
- genuinely unreadable.
-- `packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx` — the
- load-bearing `expect(setItem).toHaveBeenCalledTimes(0)` is a negative
- assertion, which a blind spy satisfies for the wrong reason. It now proves
- the spy observes two writes the test makes itself, in both `sessionStorage`
- and `localStorage`, before asserting anything about writes the component did
- not make.
diff --git a/.changeset/7787-navigation-overlay-doc-example.md b/.changeset/7787-navigation-overlay-doc-example.md
deleted file mode 100644
index 40b0f34ba5..0000000000
--- a/.changeset/7787-navigation-overlay-doc-example.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
----
-
-Point `navigation-overlay.tsx`'s file-header `@example` at the shared record-source
-reader — `resolveRecordSourceObjectName(schema, dataConfig)` from `@object-ui/core` —
-instead of the `objectName: schema.objectName` spelling PR #7648 retired from
-`useNavigationOverlay`'s own doc block (objectui#7787). That fix never reached this
-file: different file, different package, a block documenting a different symbol. The
-example follows the hook's doc block rather than restating its rule, because a ruling
-written out twice is a ruling one of whose copies rots — which is what happened here.
-
-`KNOWN_HAND_SPELLINGS` in `scripts/check-doc-example-shared-reader.mjs` carried this
-file as its one row and is drained in the same change, as that ledger requires: a row
-naming a defect the gate no longer finds fails the gate rather than sitting there as a
-waiver for nothing.
-
-Doc comment and tooling only; no published behaviour changes and no package is
-released by this change.
diff --git a/.changeset/7791-specparity-cwd-root.md b/.changeset/7791-specparity-cwd-root.md
deleted file mode 100644
index d6dd46ff09..0000000000
--- a/.changeset/7791-specparity-cwd-root.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Test-only: the `CapabilityMultiSelectField` spec-parity pin
-(`packages/fields/src/widgets/CapabilityMultiSelectField.specParity-6285.test.tsx`)
-resolved the source it reads against `process.cwd()`, so it failed under the
-package-level `test` script (objectui#7791). Nothing ships — no runtime source
-changed, and no package is released by this change.
-
-The root is now derived from the test file's own `import.meta.url` instead of
-from the cwd, so the pin reaches the same verdict under every invocation. Both
-readings are 133 files / 2178 tests green after the change; the package-level
-one was 1 file / 2 tests red before it.
diff --git a/.changeset/7798-getdataconfig-doc-example.md b/.changeset/7798-getdataconfig-doc-example.md
deleted file mode 100644
index c6676c92f3..0000000000
--- a/.changeset/7798-getdataconfig-doc-example.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
----
-
-Point the two `@example` blocks that still taught `getDataConfig(schema)` at the
-shared record-source producer `resolveRecordSourceConfig(schema)` from
-`@object-ui/core` (objectui#7798): the `useNavigationOverlay` block in
-`packages/react/src/hooks/useNavigationOverlay.ts` and the
-`resolveRecordSourceObjectName` block in `packages/core/src/utils/record-source.ts`.
-
-`getDataConfig` is exported by nothing. It survives only as a file-local wrapper in
-`plugin-map` and `plugin-grid` — and both of those already delegate their ladder to
-`resolveRecordSourceConfig`, keeping only the array shorthand above it — so a reader
-outside those two files could not write the line either example taught: there was no
-module to import it from. The examples now spell `dataConfig` the way the live call
-sites spell it (`plugin-calendar`, `plugin-tree`), which is the same judgement PR #7797
-applied to `navigation-overlay.tsx`: follow the live call sites, not another doc block.
-The `useNavigationOverlay` example also names the import, so the line it teaches is
-copyable as written.
-
-Doc comments only; no published behaviour changes and no package is released by this
-change, so the frontmatter is empty.
diff --git a/.changeset/7799-cwd-rooted-package-tests.md b/.changeset/7799-cwd-rooted-package-tests.md
deleted file mode 100644
index 3bb385375d..0000000000
--- a/.changeset/7799-cwd-rooted-package-tests.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
----
-
-Test-only: 13 test files under `packages/*/src` built a repo path out of the
-process cwd, so they reached a different verdict under their package's own
-`test` script (`vitest run --root ../.. packages/PKG/`, which is what
-`pnpm --filter PKG test` and `turbo run test` run) than under the repo-root form
-CI runs — `--root` moves vitest's root and leaves `process.cwd()` in the package
-directory (objectui#7799).
-
-Nothing ships: no runtime source changed, and no package is released by this
-change. The root is now derived from each test file's own `import.meta.url`,
-copying the precedent landed for objectui#7791, so every one of them reaches the
-same verdict under both invocations.
-
-Readings for the whole class, one tree, cwd the only variable — 17 files /
-373 tests:
-
-- repo root: 373 passed / 0 failed before, 373 passed / 0 failed after
-- package dir: 325 passed / 48 failed before, 373 passed / 0 failed after
-
-`pnpm --filter @object-ui/components test`, the acceptance invocation, goes from
-1 file / 6 tests red to 233 files / 2166 tests green.
diff --git a/.changeset/7821-manage-refresh-failure-is-not-deletion.md b/.changeset/7821-manage-refresh-failure-is-not-deletion.md
deleted file mode 100644
index e67ce38cba..0000000000
--- a/.changeset/7821-manage-refresh-failure-is-not-deletion.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-A failed package-list refresh no longer reads as "the package was deleted" and no
-longer evicts the author from the Studio (objectui#7821).
-
-`onManageChanged` — the callback the Studio's `PackageDetailSheet` fires after every
-package lifecycle action (disable / duplicate / delete / publish / manifest edit) —
-refreshed the list into a local `list` initialised to `[]` and swallowed the rejection
-under a comment reading "keep the stale list". That is true of the `pkgs` state, which
-is simply not written, and false of the local, which stayed `[]`. So after a failed
-`GET /api/v1/packages` the `!list.some(...)` check three lines down was
-unconditionally true, the code took the branch labelled `// Deleted`, and — when the
-managed package was the one under the editor — navigated away with `list[0]`
-undefined, i.e. to `/home`. One transient 503, network blip or auth expiry threw the
-author out of the editor with no toast and no confirmation, while the package was
-still there.
-
-The local now starts as `null` — "the refresh told us nothing" — and only a list that
-actually came back, without the managed package in it, is read as a deletion. A
-failure draws no inference at all: no navigation. It is reported instead, through the
-posture this surface already has (objectui#7368): `formatMetadataError` on the shared
-`studio-package-list` sonner id, and recorded so the switcher reads `failed` rather
-than presenting a now-stale list as current. A real deletion navigates exactly as
-before — to the first surviving package, or `/home` when none is left.
-
-Still a `.catch` and still no retry: one 503 must not take the Studio down, and no
-retry policy has been ruled on.
diff --git a/.changeset/7823-objectview-chart-relay-whole-block.md b/.changeset/7823-objectview-chart-relay-whole-block.md
deleted file mode 100644
index 028d672f43..0000000000
--- a/.changeset/7823-objectview-chart-relay-whole-block.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Forward the authored `chart:` block WHOLE from the object-view relay to `ListView`
-(objectui#7823).
-
-`ObjectView`'s `renderListView` builds the list schema by spreading the object's list
-schema and then relaying the active view's visualization blocks under `options`. Every
-sibling block is forwarded whole; `chart` was a hand-listed projection of exactly six
-keys — `chartType`, `xAxisField`, `yAxisFields`, `aggregation`, `series`, `config` — the
-pre-ADR-0021 key set, frozen. The whole ADR-0021 (objectui#1890) authoring shape
-(`dataset` / `dimensions` / `values`) and the legacy `categoryField` / `valueField`
-spelling had no rung, so a view that declared them reached `ListView` with its binding
-stripped, and `ListView` could not tell that from a view that had declared nothing.
-
-**What changes for authors.** On the object-view route, a list view that declares an
-ADR-0021 chart block and whitelists `chart` in `appearance.allowedVisualizations` is now
-offered the Chart toggle and renders from the dataset it named. Before this fix the
-capability gate added by objectui#7544 was handed six `undefined` keys, correctly
-answered "nothing declared" about a view whose author had declared everything, and
-ADR-0047 filtered the author's own whitelist down to `['grid']` — no toggle, no
-diagnostic. The legacy `xAxisField` / `yAxisFields` spelling did survive the projection
-and did resolve, so the two authoring shapes behaved differently on this route for
-reasons that lived entirely in that one object literal.
-
-**Not a wider whitelist.** The projection is replaced by a pointer, not extended from
-six keys to nine: a hand-listed key set is a copy, and copies rot silently — three more
-keys would buy ADR-0021's correctness while re-arming the identical trap for the next
-block key, and nothing would fire then either, because the relay's view definition is
-`Record` and a missing rung is invisible to `tsc` (objectui#7559 owns that
-mechanism). Forwarding whole is safe because `ListView` reads this block by name at both
-of its readers — the capability gate's `resolveListChartBinding` and the `chart` render
-branch — and never spreads it.
-
-A view that declares no chart block, an empty block, or a block with no binding at all
-is still offered no Chart toggle: the relay now forwards `undefined` instead of the old
-permanently-truthy husk of six `undefined` keys.
diff --git a/.changeset/7828-calendar-gantt-ladder-harness.md b/.changeset/7828-calendar-gantt-ladder-harness.md
deleted file mode 100644
index 594f3d76b9..0000000000
--- a/.changeset/7828-calendar-gantt-ladder-harness.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
----
-
-Test-only change; no published behaviour changes.
-
-objectui#7828 ports the three harness guards from objectui#7527 (`c2fc261f5`),
-plus PR #7826's bound capture, from the timeline `colorField` ladder fixture to
-its two siblings — `ObjectCalendar.colorFieldLadder-7243.test.tsx` and
-`ObjectGantt.colorFieldLadder-7243.test.tsx`. Their readiness predicate now
-identifies WHICH render wrote instead of counting arity, each helper returns the
-array that predicate accepted rather than re-reading a module global, every
-render is unmounted in a `finally`, and no renderer may be alive when one is
-mounted.
-
-No runtime code is touched, and neither component's behaviour was changed or
-found wanting: the two-paint window the guards exist for was measured for and is
-absent on both (both gate their record query on the settled object schema), so
-this is a fixture that can no longer be broken by the ordinary next edit, not a
-fix for a failure.
diff --git a/.changeset/7837-vscode-export-react-side-effect-import.md b/.changeset/7837-vscode-export-react-side-effect-import.md
deleted file mode 100644
index a5e719c67c..0000000000
--- a/.changeset/7837-vscode-export-react-side-effect-import.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'object-ui': patch
----
-
-Fix the phantom import the VS Code extension's **Export to React** command wrote into
-every file it generated (objectui#7837).
-
-`generateReactComponent()` emitted a preamble that imported `registerDefaultRenderers`
-from `@object-ui/components` and then called it. That symbol is on **no export** of
-that package: its built `dist/index.d.ts` carries exactly one `register*` name,
-`registerPlaceholders`, and `registerDefaultRenderers` appears **0 times** in either
-`dist/index.d.ts` or `dist/index.js`. So every file the command produced failed to
-compile with `TS2305` naming a symbol the user never typed.
-
-`@object-ui/components` registers its renderers as an **import side effect** —
-`sideEffects: true` in its manifest, `import './renderers'` in its barrel under the
-comment `Register all ObjectUI renderers (side-effects)`, and **114 `register(` call
-sites** at module scope in the built `dist/index.js`. There is no registration function
-to call, so the generated preamble now imports the package for the side effect and says
-why. Same spelling the root README landed for objectui#7417.
-
-`packages/vscode-extension/DESIGN.md`, which documented the identical two lines, is
-corrected in the same commit so the design record does not freeze the defect.
-
-No public surface moved: no export added, no signature changed.
diff --git a/.changeset/7839-tree-arm-own-reference.md b/.changeset/7839-tree-arm-own-reference.md
deleted file mode 100644
index c91d9b08c5..0000000000
--- a/.changeset/7839-tree-arm-own-reference.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/plugin-tree': patch
----
-
-`ObjectTree`'s parent-pointer auto-detection accepts a `tree` field only when it is
-this object's own (objectui#7839, objectstack#14892 follow-up).
-
-A hierarchy is parent/child **within one object**, so `@objectstack/spec` refuses a
-`tree` field whose `reference` names any other object (`refuseForeignTreeReference`);
-`reference` stays optional on a `tree`, where it is a redundant self-annotation.
-`detectParentField` returned the first `type: 'tree'` field whatever its `reference`
-said, so a foreign-shaped one was silently picked as the parent pointer and the forest
-was grouped on a pointer into a table it does not point at. It now mirrors the spec's
-own kernel predicate `hasDetectableParentField` term for term — accept when `reference`
-is absent, or when it equals the bound `objectName` — and the `objectName` guard comes
-with it: an object whose name we do not know cannot be self-referenced.
-
-Skipping rather than returning also removes a masking bug the old early-return hid: a
-foreign `tree` declared before a self-referencing `lookup` used to win by position and
-the lookup never got its turn. The `lookup` / `master_detail` arm is otherwise
-unchanged.
-
-Reachability is narrow and stated rather than oversold: the refused shape does not
-survive `ObjectSchema.parse` on a spec carrying that rule, so it is unreachable from
-parsed metadata and reachable from a hand-built one — `getObjectSchema` is a required
-member of the published `DataSource` interface, so a third-party implementation reaches
-this reader raw. Note also that the renderer tightens **ahead** of the spec copy this
-repo installs: `@objectstack/spec@17.2.0` still accepts the foreign shape at parse
-(measured), so until that pin moves this function is the only door.
diff --git a/.changeset/7842-objectview-datasource-contract.md b/.changeset/7842-objectview-datasource-contract.md
deleted file mode 100644
index df6b32db5e..0000000000
--- a/.changeset/7842-objectview-datasource-contract.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
----
-
-Comment-only truthfulness fix in `@object-ui/plugin-view`, plus its pin.
-`ObjectViewProps.dataSource` promised "If not provided, falls back to
-SchemaRendererProvider context" one line above a declaration that has always
-been required, so one of the two had been wrong for the prop's whole life
-(objectui#7842). Measured: the TYPE is right. `ObjectView` holds no context read
-at all — no `useContext`, no `SchemaRendererContext`, and not
-`useElementDataSource`, the hook `@object-ui/react` publishes for exactly this
-fallback; the prop's value travels into `useSettledSchema` (no context read
-either) and into two effects that GUARD on it rather than resolve one.
-
-The sentence was misfiled rather than invented: `ObjectViewRenderer`, the
-renderer registered for the `object-view` and `view` schema tags, is what really
-reads `useContext(SchemaRendererContext)` and hands `ctx?.dataSource` down to
-this prop. So a schema-driven host does get the provider's adapter; a caller
-writing `` in TSX does not, and tsc refuses the omission at the
-call site. The JSDoc now says that, says what happens when the prop is absent
-anyway (every read site returns early, the value is forwarded verbatim to a
-child whose own `dataSource` is optional, and the view renders empty rather than
-throwing), and records that making the prop optional would widen a published
-accept set.
-
-The new pin keeps both halves: `ObjectView` under a real `SchemaRendererProvider`
-never touches the provider's adapter (behind a live control that proves the same
-probe DOES observe an adapter passed as the prop), and the registered renderer
-still resolves one from context. No published behaviour changes and no
-declaration moved.
diff --git a/.changeset/7849-unevaluated-diagnostic-lists-properties.md b/.changeset/7849-unevaluated-diagnostic-lists-properties.md
deleted file mode 100644
index c2e0864b36..0000000000
--- a/.changeset/7849-unevaluated-diagnostic-lists-properties.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-"@object-ui/react": patch
----
-
-The unevaluated-expression diagnostic now lists `properties` among the channels
-that evaluate and read back, matching the sibling `props`-bag diagnostic that
-tells authors to write their keys there.
-
-Both messages are dev-build diagnostics in `SchemaRenderer`, and one node can
-trip both. They disagreed: `propsBagDiagnostic` said *"`props` is NOT hoisted
-onto the node — only `properties.*` is … Write them under `properties`
-instead"*, while `unevaluatedExpression` enumerated *"channels that do evaluate
-and read back today"* as `content` or host-side resolution — omitting
-`properties`. An author who hit both was told to use a channel the other message
-said did not work.
-
-The enumeration was the wrong half, established by measurement rather than by
-reading the `COMPAT` label on the hoist. `properties` has no retirement on
-record: `@objectstack/spec@17.2.0` calls the vocabulary carried there *"ALIVE —
-this is not dead surface to retire under ADR-0049"*, keeps
-`PageComponentSchema.properties` as the open carrier on purpose, gates it at the
-authoring door, and tombstones no part of it via `retiredKey()`. In this repo
-`props`, not `properties`, is the spelling annotated as the legacy alias.
-
-Diagnostic text only — no evaluation, hoist or schema behaviour changed.
diff --git a/.changeset/7862-vscode-export-react-unused-import.md b/.changeset/7862-vscode-export-react-unused-import.md
deleted file mode 100644
index e75f1449ea..0000000000
--- a/.changeset/7862-vscode-export-react-unused-import.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'object-ui': patch
----
-
-Drop the unused `import React from 'react'` the VS Code extension's **Export to React**
-command wrote into every file it generated (objectui#7862).
-
-The generated file's only JSX is a single SchemaRenderer element, so under the automatic
-JSX runtime — `"jsx": "react-jsx"`, what a new Vite or Next project is configured with —
-the `React` identifier was never read. Measured on this branch against the built
-`dist/index.d.ts` of `@object-ui/react` and `@object-ui/components`, TypeScript 6.0.3:
-the emitted file compiled clean under `react-jsx` + `strict` (exit 0), and under the same
-config plus `noUnusedLocals: true` it failed with
-`TS6133: 'React' is declared but its value is never read` — so a consumer with that
-option on could not compile the file the command had just handed them.
-
-The preamble now says in a comment that it assumes the automatic runtime and that the
-import goes back only on the classic `"jsx": "react"` transform, which is the one
-configuration this costs: measured, that file reports one diagnostic about `React` being
-out of scope. Nothing in the extension emits or promises a `jsx` setting — the string
-does not occur anywhere in the package — and the published docs page for the command
-already showed the output without the import.
-
-A new pin, `src/__tests__/export-to-react-compiles.test.ts`, now extracts the template's
-PRODUCT and compiles it under `noUnusedLocals`, rather than matching substrings in the
-generator. The sibling objectui#7837 pin was green for the whole life of this line
-because it never named it; a compile closes the class instead of one member of it. Its
-positive control runs on every invocation: re-adding the import must report TS6133, so
-the harness cannot go quietly, permanently green.
-
-`packages/vscode-extension/DESIGN.md`, whose section 4 hand-mirrors this preamble, is
-corrected in the same commit so the design record does not freeze the defect — the
-spelling objectui#7837 landed for the same file. What binds the two copies together is
-still nothing, and objectui#7976 holds that question.
-
-No public surface moved: no export added, no signature changed.
diff --git a/.changeset/7874-timeline-relative-defaults-retired.md b/.changeset/7874-timeline-relative-defaults-retired.md
deleted file mode 100644
index c817d5fbe8..0000000000
--- a/.changeset/7874-timeline-relative-defaults-retired.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
----
-
-Internal residue removal, no published behaviour: the five `timeline.relative.*`
-rows are deleted from `TIMELINE_DEFAULT_TRANSLATIONS`
-(`packages/plugin-timeline/src/useTimelineTranslation.ts`), and a retirement pin
-replaces them.
-
-The rows, transcribed verbatim because that — not "it is in git history
-somewhere" — is what makes the deletion reversible:
-
-```ts
-'timeline.relative.today': 'Today',
-'timeline.relative.tomorrow': 'Tomorrow',
-'timeline.relative.yesterday': 'Yesterday',
-'timeline.relative.inDays': 'In {{n}} days',
-'timeline.relative.daysAgo': '{{n}} days ago',
-```
-
-Nothing releases because nothing could reach them. The `en` pack defines no
-`timeline.relative.*` leaf, so the provider path never served one; no call site
-anywhere in the tree — sources, tests, JSON, ignored build outputs — asks for the
-prefix `timeline.relative`, so the provider-less `fallbackT` never reached a row
-either; and neither `TIMELINE_DEFAULT_TRANSLATIONS` nor `translateTimelineDefault`
-is re-exported from the package entry, so no consumer of the tarball can name
-them. Day-granularity relative phrases are produced by `formatRelativeDate` in
-`@object-ui/core` through `Intl.RelativeTimeFormat`, which needs no copy row —
-that is what left these five behind (objectui#7874, found by objectui#7567's
-factory-default census printing its abstention count).
diff --git a/.changeset/7876-activity-map-docs-transcription-pin.md b/.changeset/7876-activity-map-docs-transcription-pin.md
deleted file mode 100644
index bbbfa8586d..0000000000
--- a/.changeset/7876-activity-map-docs-transcription-pin.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Pin the `sys_activity` mapping paragraph on `content/docs/plugins/plugin-detail.mdx`
-against `ACTIVITY_TYPE_TO_FEED_TYPE`, derived from the map in both directions
-(objectui#7876, ruling A). Test only; no package is released by this change, and the
-published behaviour of `@object-ui/plugin-detail` is unchanged.
diff --git a/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md b/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md
deleted file mode 100644
index c8806fadcd..0000000000
--- a/.changeset/7881-package-lookup-failure-is-not-an-empty-result.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio: a failed package lookup no longer opens the management sheet on nothing
-
-`fetchFullPackage` — the helper behind the switcher's "Package info & settings" —
-fetched `/api/v1/packages` and went straight to `res.json()`, never reading
-`res.ok`. The platform answers a failed read in the ADR-0112 envelope
-(`{ success: false, error: { code, message } }`), which parses cleanly through
-that reader: the error object is neither an array nor carries `packages`, so the
-list fell to `[]` and the lookup returned `null` without throwing. `openManage`'s
-`catch` therefore never ran and the two lines after it still fired, opening the
-management sheet over a null package — which renders nothing. During an outage
-the author clicked the menu item and got silence: no sheet, no toast, no
-explanation.
-
-The read now refuses a non-2xx, carrying the server's own `error.message` and
-`error.code` (in the 5xx band the platform withholds the producer's prose, so the
-code is the discriminating word) and naming the status when the body is not JSON
-at all. The failure is reported through this surface's existing posture —
-`formatMetadataError` on the shared `studio-package-list` sonner id, so one outage
-across this surface's four callers of that endpoint is one toast, not four.
-
-And the sheet no longer opens on a `null` package at all: a successful list that
-simply does not contain the package — deleted or uninstalled elsewhere — now says
-so instead of opening over nothing.
diff --git a/.changeset/7895-timeline-settled-schema-gate.md b/.changeset/7895-timeline-settled-schema-gate.md
deleted file mode 100644
index 9250f5c4b5..0000000000
--- a/.changeset/7895-timeline-settled-schema-gate.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/plugin-timeline': patch
----
-
-`ObjectTimeline` waits for the object definition instead of querying twice
-(objectui#7895).
-
-It was the last member of the set objectui#6482 converged on the shared
-settled-schema gate — `ObjectKanban`, `ObjectView`, `ObjectCalendar` and
-`ObjectTree` were named there, `ObjectGantt` was ask 2 of objectui#7225 — and
-nothing marked it a deliberate exclusion. It still held the object definition in
-a local `useState` fed by its own metadata effect, and listed that definition in
-the record-fetch effect's dependency array.
-
-**User-visible.** Every object-bound timeline load issued **two** `find` calls
-instead of one: the first before the definition landed, with `buildExpandFields`
-seeing no fields and therefore carrying no `$expand` at all, and a second one
-after. Whenever the metadata read is the slower of the two — the common case on
-a cold metadata cache — the second call is not merely a wasted round trip but a
-**three-step paint**: raw foreign-key ids, back to the loading skeleton (the
-effect's re-run calls `setLoading(true)` and `loading` is an early return), then
-the expanded rows. After this change the timeline paints once, from a query that
-already carries its expansion.
-
-Measured on the component with an instrumented renderer, one mount per hold,
-`getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with
-`ObjectCalendar` and `ObjectGantt` as positive controls in the same run: before,
-2 `find` calls with expand sets `[null, ['owner']]`, 1 paint at the readiness
-predicate and 3 late writes after it at every hold from +3 ms up; after, 1 `find`
-carrying `['owner']`, 1 paint, 0 late writes, and a first-paint time that tracks
-the hold (8 ms at +3, 30 ms at +25, 105 ms at +100) where before it was a flat
-3-7 ms at every hold. Both controls read 1 paint / 0 late writes before and
-after.
-
-The resolution half is now `useSettledSchema` from `@object-ui/react`, which
-settles on **every** exit — no source, no `getObjectSchema`, no object name, and
-a read that threw alike. That is what makes the gate safe: the replaced effect
-returned without settling on all four, which cost nothing while nothing waited on
-it and would have held a gated query open forever. Pinned by
-`ObjectTimeline.fetchGate-7895.test.tsx`, including a timeline whose adapter
-exposes no `getObjectSchema` and one whose definition read rejects — both still
-query, unexpanded.
-
-Unlike the two sibling conversions, the metadata read is **not** disabled for a
-timeline whose items were authored inline: this component also reads the
-definition's fields for option colours and field labels on that path, where no
-record query is issued at all.
diff --git a/.changeset/7903-gallery-settled-schema-gate.md b/.changeset/7903-gallery-settled-schema-gate.md
deleted file mode 100644
index 89eae6fc51..0000000000
--- a/.changeset/7903-gallery-settled-schema-gate.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-`ObjectGallery` waits for the object definition instead of querying twice
-(objectui#7903).
-
-It sat outside the set objectui#6482 converged on the shared settled-schema gate
-— `ObjectKanban`, `ObjectView`, `ObjectCalendar` and `ObjectTree` were named
-there, `ObjectGantt` was ask 2 of objectui#7225 and `ObjectTimeline` was
-objectui#7895 — and nothing marked it a deliberate exclusion. It still held the
-object definition in a local `useState` fed by its own metadata effect, and
-listed that definition in the record-fetch effect's dependency array.
-
-**User-visible.** Every object-bound gallery load issued **two** `find` calls
-instead of one: the first before the definition landed, with `buildExpandFields`
-seeing no fields and therefore carrying no `$expand` at all, and a second one
-after. The first paint was therefore a grid of cards rendered from raw
-foreign-key ids. After this change the gallery paints once, from a query that
-already carries its expansion.
-
-Measured on the component with an instrumented renderer, one mount per hold,
-`getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with
-`ObjectCalendar` as a positive control in the same run: before, 2 `find` calls
-with expand sets `[null, ['owner']]` at every hold, the issue order always
-`schema:issued, find(unexpanded), schema:settled, find(expanded)`, two distinct
-painted states, 3 late writes into the card grid after the first paint, and a
-first-paint time flat at 3-7 ms across the whole sweep; after, 1 `find` carrying
-`['owner']`, one painted state, 0 late writes, and a first paint that tracks the
-hold (9 ms at +3, 15 ms at +10, 35 ms at +25, 106 ms at +100). The control read
-1 `find` carrying `['owner']` and a hold-tracking first paint both before and
-after.
-
-The cost this component was paying is a **two**-step paint, not the three-step
-one `ObjectCalendar` and `ObjectTimeline` each measured: those make `loading` an
-unconditional early return, so their re-run drops back to a placeholder in
-between, while this component's early return is `loading && !items.length` — the
-raw ids were replaced in place. Measured here rather than inherited from the
-matching shape, which is objectui#6482's own per-component standard.
-
-The resolution half is now `useSettledSchema` from `@object-ui/react`, which
-settles on **every** exit — no source, no `getObjectSchema`, no object name, and
-a read that threw alike. That is what makes the gate safe: the replaced effect
-returned without settling on all four, which cost nothing while nothing waited on
-it and would have held a gated query open forever. Pinned by
-`ObjectGallery.fetchGate-7903.test.tsx`, including a gallery whose adapter
-exposes no `getObjectSchema` and one whose definition read rejects — both still
-query, unexpanded.
-
-Two departures, each judged for this component rather than copied. Like
-`ObjectTimeline` and unlike `ObjectCalendar` / `ObjectGantt`, the metadata read
-is **not** disabled for a gallery whose records were authored inline: this
-component reads the definition on every path, for cell semantics and for ADR-0079
-card titles, not only to expand a query. And the gate window now holds the
-loading placeholder rather than showing "No items to display", which the two
-siblings get from their initial `loading` state and this one did not.
diff --git a/.changeset/7907-manage-snapshot-refresh-reports.md b/.changeset/7907-manage-snapshot-refresh-reports.md
deleted file mode 100644
index ec71213f7f..0000000000
--- a/.changeset/7907-manage-snapshot-refresh-reports.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio: a failed managed-snapshot refresh no longer leaves the package sheet showing
-the pre-action record as current (objectui#7907).
-
-`onManageChanged` runs after every package lifecycle action fired from the detail sheet
-(disable / enable / duplicate / publish / publish-drafts / manifest edit). Its tail
-re-read the managed record so the change would show immediately, and swallowed any
-failure under a bare `catch {}` commented "keep the current snapshot" — a snapshot the
-action itself had just made stale. The author disabled a package, was told nothing, and
-went on reading `Status: Enabled`.
-
-The sheet derives its lifecycle verb from that record (`enabled` picks both the button's
-label and the endpoint it POSTs), so a stale snapshot did not merely display a stale
-badge — it re-armed the author with the verb they had just fired. The failure is now
-reported through this surface's existing posture (`formatMetadataError` on the shared
-`studio-package-list` sonner id, so one outage across the surface is still one toast)
-and the sheet closes rather than stay open on a record known to be pre-action. The
-Studio, the top bar and the package list are untouched: still a degradation, never a
-throw, and no navigation is inferred from a refresh that could not happen.
-
-The same tail dropped `fresh === null` — a successful read whose list no longer contains
-the package — just as quietly; it now reports that with the same sentence `openManage`
-already uses for it.
-
-Pre-existing, and objectui#7881 made it much easier to hit rather than causing it:
-before that fix `fetchFullPackage` never read `res.ok`, so this `catch` could only ever
-see a non-JSON body; now that the helper refuses a non-2xx, the same `catch` was also
-swallowing every 401 / 403 / 503 / 500.
diff --git a/.changeset/7918-zod-lazy-getter-identity.md b/.changeset/7918-zod-lazy-getter-identity.md
deleted file mode 100644
index aaba556a64..0000000000
--- a/.changeset/7918-zod-lazy-getter-identity.md
+++ /dev/null
@@ -1,54 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Two of the ten `z.lazy` exports in the zod node face now memoise their getter, so their
-public `.unwrap()` compares by identity (objectui#7918). The other eight are **deliberately
-unchanged** — measured, memoising them is a module-load `ReferenceError`.
-
-The card that found this did not claim the ten were wrong. It asked whether the spelling was
-buying a temporal-dead-zone dodge, and that check is what shipped. Each of the ten was
-rewritten in place to `const inner = ; z.lazy(() => inner)`, the package rebuilt, and
-the built barrel imported in a fresh process. **Eight refuse to load.** Seven name the very
-const being declared (`children: z.array(TreeNodeSchema)` sits inside `TreeNodeSchema`'s own
-initialiser); `SchemaNodeSchema` names `BaseSchemaCore`, which `base.zod.ts` declares below
-it. For those eight the `z.lazy` is load-bearing, so `ActionSchema`, `AppMenuItemSchema`,
-`FilterGroupSchema`, `MenuItemSchema`, `NavLinkSchema`, `NavigationMenuItemSchema`,
-`SchemaNodeSchema` and `TreeNodeSchema` keep the spelling they have. The two that loaded
-clean are memoised: `FilterBuilderConditionSchema` is not recursive at all, and
-`NavigationItemSchema` already defers its self-reference through an inner
-`z.lazy(() => NavigationItemSchema)` on `children`.
-
-⚠️ Two corrections to the finding, both measured, both worth more than the edit:
-
-**The recursion point was already identity-comparable, through the right handle.**
-`zod@4.4.3` caches a lazy's resolved inner type on `def._cachedInner` — its own comment says
-this preserves "identity for cycle detection on recursive schemas" — and `S._zod.innerType`
-reads that cache. It is stable for all ten, including the eight, and survives `.describe()`
-clones. What is *not* stable is `S.unwrap()`, because `ZodLazy` defines it as
-`() => inst._zod.def.getter()`, going around the cache (`ZodPromise` spells its own as a
-stored field). So a schema walker can recognise the recursion point today by reading
-`_zod.innerType`; the objectui#7581 false negative — `ActionSchema` reported "not exported by
-name" when it plainly is — was the wrong handle, not an unrecognisable schema. Memoising is
-still worth doing where it is free, because it makes the public `.unwrap()` honest.
-
-**The "rebuilt on every parse" cost does not exist.** The finding recorded, explicitly
-unmeasured, that a document with N nodes reconstructs the recursive sub-schema N times. It
-does not: the getter runs **once per lazy for the life of the process**, via the same
-`_cachedInner` — measured at one call during the first parse of a 13-node document and zero
-during the second. Wall clock agrees. `NavigationItemSchema` over a 73-node document,
-memoised versus not, medians of nine trials of 200 parses: 149,973 ns versus 140,293 ns per
-parse — ratio 0.94x, with the ranges overlapping. Those are shared-box seconds, so the
-absolutes are not idle-machine figures; the ratio is the reading, and the reading is "no
-difference". There is no parse-time win here, and anyone pricing the strict face
-(objectui#7935 / objectstack#5250) should strike this from the input list.
-
-No accept/reject behaviour moves — a memoised getter changes schema *identity*, not what is
-declared or admitted. The measurement, the eight `ReferenceError` messages, the identity
-matrix and an executable reproduction of both the TDZ mechanism and the once-per-process
-getter are pinned in `packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts`.
-
-⚠️ Also settled while locating the ten: `AppMenuItemSchema` has no declaration of its own —
-it is the barrel alias of `app.zod.ts`'s `MenuItemSchema`, while the barrel's own
-`MenuItemSchema` is `overlay.zod.ts`'s. Two different schemas, so the list really is ten
-entries and not nine.
diff --git a/.changeset/7923-schema-reference-named-list-view-keys.md b/.changeset/7923-schema-reference-named-list-view-keys.md
deleted file mode 100644
index 7fe632e9a5..0000000000
--- a/.changeset/7923-schema-reference-named-list-view-keys.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Documentation and test only. `content/docs/api/schema-reference.md`'s `object-view`
-example no longer authors `listViews["my-deals"].default` — a key `NamedListView`
-never declared and `ObjectView` never read — and now spells the node-level
-`defaultListView` the renderer actually reads. The new pin
-`packages/types/src/__tests__/schema-reference-named-list-view-keys-7923.test.ts`
-holds that example's `listViews` keys to `NamedListView`'s declared members.
-No published behaviour changes (objectui#7923).
diff --git a/.changeset/7947-named-list-view-figures.md b/.changeset/7947-named-list-view-figures.md
deleted file mode 100644
index 09da676eea..0000000000
--- a/.changeset/7947-named-list-view-figures.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
----
-
-Corrective, comment-and-figures only: no published behaviour changes, so this
-declares no bump.
-
-`NamedListView`'s member figures were quoted wrong in four places. The
-declaration has **47** top-level members, counted with the
-`namedListViewMemberCount()` regex that
-`packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts` uses for
-its own assertion — not "about 52", which was a hand figure sitting between two
-instruments (a looser count that also matches nested object-literal lines gives
-59 on the same declaration). The derived "unread" figure is **41**, not 45: the
-renderer reads seven keys off a named view, but only six of them (`label`,
-`type`, `columns`, `filter`, `sort`, `options`) are declared members. The
-seventh, `data`, is not declared on `NamedListView` at all — it reaches the
-renderer through an `as any` cast on the named-view config in
-`packages/plugin-view/src/ObjectView.tsx` — so the arithmetic is 47 − 6, not
-52 − 7.
-
-Corrected at all four sites: this changeset's sibling
-`.changeset/object-view-unmirrored-keys-7779.md` (still unconsumed, corrected in
-place so the wrong figure never reaches a published CHANGELOG), the
-`ObjectViewSchema` docblock in `packages/types/src/zod/objectql.zod.ts`, the
-`UnmirroredDeclared` note in
-`packages/types/src/__tests__/zod-mirror-parity.test.ts`, and the header of the
-pin test file.
-
-The pin that should have caught the drift did not bind: it asserted
-`toBeGreaterThanOrEqual(40)` against a true 47, which permitted the declaration
-to shed seven members — including a shrink toward the read set, the exact
-condition that re-opens the `listViews` value-type decision — and could not
-catch growth at all. It is now an exact `toBe(47)` whose failure message names
-the three sibling files whose figures must move with it, plus a comment
-recording how the count is taken and where the retired "about 52" came from.
-
-No schema, no type, and no assertion about what is accepted or refused changed.
diff --git a/.changeset/7952-dashboard-widgets-component-arm.md b/.changeset/7952-dashboard-widgets-component-arm.md
deleted file mode 100644
index 42b36e9a91..0000000000
--- a/.changeset/7952-dashboard-widgets-component-arm.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/types": minor
----
-
-`DashboardComponentSchema.widgets` gains the component-node arm on the TypeScript face (objectui#7952)
-
-`widgets` was `DashboardWidgetSchema[]` in TypeScript while the zod schema it mirrors has been a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the widget slot (`type: 'metric-card'`, body validated as passthrough `BaseSchema`) or a spec-family widget. So the shape `@object-ui/plugin-dashboard`'s README teaches in every `metric-card` example — and the shape the shipped `DashboardRenderer` renders — parsed green under `safeParse` and was refused by `tsc --strict` (`TS2561: 'value' does not exist in type 'DashboardWidgetSchema'`, six occurrences across the README's dashboard blocks at `fc32921`). There was no annotation an author could write for a document the platform accepts.
-
-**Accept-set change (Clause ②, TypeScript face only).** `widgets` is now `Array`, and `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` — is a new export of `@object-ui/types`. The zod schema is unchanged; `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue` (those are `MetricCard`'s registry inputs, not widget keys — the compiler's `Did you mean to write 'values'?` points at the repair both declarations forbid).
-
-**What still refuses.** A widget that names a spec-family `type` and carries an undeclared key (`{ type: 'bar', bogus: 1 }`) is still a `tsc` error: the literal is discriminated by `type`, so the passthrough arm never applies to it. A `type` outside both vocabularies is refused as before. The one corner the TypeScript union cannot discriminate — a legacy `component` envelope with NO `type` plus an undeclared key — compiles on the TypeScript face and is refused by name at validation, as every `BaseSchema` slot already behaves.
-
-**Consumers.** The new arm is assignable to `DashboardWidgetSchema`, so code that annotates a widget callback `(w: DashboardWidgetSchema)` keeps compiling unchanged. Code that reads a property off an unannotated element of `schema.widgets` now sees the union, and through `BaseSchema`'s index signature that read is `any` rather than the widget's declared type — annotate the parameter to keep the narrower type.
diff --git a/.changeset/7979-package-form-dialog-envelope-reader.md b/.changeset/7979-package-form-dialog-envelope-reader.md
deleted file mode 100644
index 87c38686d3..0000000000
--- a/.changeset/7979-package-form-dialog-envelope-reader.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`PackageFormDialog` reads the producer-marked `error.userMessage` (objectui#7979).
-
-The create / edit / view package dialog POSTs and PATCHes `/api/v1/packages` through its
-own `apiJson`, which held a fourth copy of the ADR-0112 failure-envelope ladder —
-character for character the one `PackagesPage` had before objectui#7959. It read the
-diagnostic `error.message` and stopped, so two things a refusal carries never reached the
-author: the producer's marked `error.userMessage` (present since objectstack#9934, emitted
-by both doors that serve these routes) and `error.code`.
-
-The read now comes from the one shared rule, `readEnvelopeFailureText`
-(`utils/apiErrorEnvelope.ts`), which prefers the mark over the diagnostic and appends the
-declared code behind whichever prose won. Create and edit are exactly where an author meets
-a refusal that names what to fix — a namespace rule, a version already published — and in
-the 5xx band the door substitutes the generic `Internal server error` into `message` while
-the mark rides through untouched, so on a marked 500/503 the dialog used to show the generic
-sentence and discard the specific one.
-
-The two rungs below the shared read stay: a bare-string `error` and a top-level `message`
-are older runtimes' shapes, not this envelope, and they are live for this call site alone.
-The dialog's two status-driven arms (409 → "already exists", 403 → the localized capability
-copy, objectstack#8270) are unchanged.
diff --git a/.changeset/8067-component-input-member-kind.md b/.changeset/8067-component-input-member-kind.md
deleted file mode 100644
index ddd9c49df8..0000000000
--- a/.changeset/8067-component-input-member-kind.md
+++ /dev/null
@@ -1,54 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/sdui-parser': minor
-'@object-ui/components': minor
-'@object-ui/plugin-detail': minor
-'@object-ui/plugin-grid': minor
----
-
-`ComponentInput.of` — the coarse kind of an input's MEMBERS, with readers on day one
-(objectui#8067).
-
-A registration's `type: 'array'` said a value was a list and stopped there, so a member
-that drifted from `@objectstack/spec` was invisible to every layer that reads a
-declaration. `page:header.actions` is the measured cost: the contract declares
-`z.array(z.string())` ("Action IDs"), the renderer read the members as `ActionDef`
-objects, and the repo-wide parity gate in
-`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` stayed green for the
-whole life of the drift because both sides carried the key and neither could say what
-was inside it. What settled it was a maintainer ruling, not a test — and even after the
-fix, "these are ids" survived only as English in the registration's `description`.
-
-**What is new.** `ComponentInput` gains an optional `of`, carrying the same coarse-kind
-vocabulary as `type` one level down: the ELEMENTS of an `array`, or the VALUES of an
-`object` used as a map. One kind, or an array of them for a member contract that is a
-union, with `type`'s semantics — a member passes when any declared arm accepts it. The
-manifest serializer forwards it, so `sdui.manifest.json` now carries seven keys per
-input instead of six.
-
-**Three readers ship with it**, which was the bar this slot had to clear (objectui#5905
-is the precedent: five `ComponentInput` keys declared and read by nothing). The
-repo-wide parity gate compares every declared `of` against the member kind
-`ComponentPropsMap[type]` actually accepts and fails on one the contract refuses;
-`sdui-parser`'s `validateTree` reports a member that fits no declared kind, as a new
-`member-type-mismatch` diagnostic naming the offending positions; and the generated
-`sdui-intrinsics.d.ts` narrows the authoring type — `page:header`'s `actions` is
-`string[]` where it used to be `unknown[]`.
-
-**Fifteen keys now declare one**, across ten blocks, each DERIVED rather than chosen:
-every container key's member position was probed with one value of each coarse kind and
-a declaration written only where exactly one kind was accepted. A member contract that
-admits several kinds — `record:highlights.fields` takes a field name or an inline field
-object — is deliberately left undeclared and pinned with its reason, because picking one
-arm there is a narrowing this repo leaves un-gated and picking all of them would
-advertise shapes only a per-block pin can vouch for.
-
-**The ceiling is unchanged.** `of` is a KIND and never a value domain, so the maintainer
-ruling of 2026-08-17 quoted on `ComponentInput.type` — the coarse arm plus `description`
-is the publication face's expression ceiling, and spec is the sole judge of values —
-stands exactly as written. `of: 'object'` says the members are objects; which keys they
-carry is still `description`'s job and `os validate`'s.
-
-**Nothing published before this changes.** An input that declares no `of` validates,
-serializes and types byte-identically: `validateTree` checks no member, the serializer
-emits no key, and the codegen emits the same `unknown[]`.
diff --git a/.changeset/8068-mandatory-per-block-member-pin.md b/.changeset/8068-mandatory-per-block-member-pin.md
deleted file mode 100644
index 11684e5252..0000000000
--- a/.changeset/8068-mandatory-per-block-member-pin.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
----
-
-Make a per-block member pin MANDATORY for every `array`/`object` typed
-registered input on a spec-carried block (objectui#8068).
-
-`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` compares
-top-level key names and delegates member shapes to per-block tests beside the
-renderers — but nothing required one. `page:header.actions` is the key that
-proves it: array-typed, member shape in `description` prose only, spec
-`z.array(z.string())` while the renderer read the members as objects, and every
-layer green for a full contract cycle until a human filed it.
-
-The new direction judges the same `covered` set as the rest of that file: every
-array/object-armed input must name a pin whose file exists, is collected by the
-runner, and names both the block and the key — or carry an explicit,
-issue-backed exemption. Measured first, as the card required: 77 such inputs, 19
-already pinned, 58 not, so the route is the transition this repo already uses —
-all 58 listed by name, self-deleting once a key acquires a pin, under a ceiling
-that may only ratchet down. `objectui#8071` owns converting them.
-
-Test only; no package is released by this change.
diff --git a/.changeset/8137-record-activity-calibration-repoint.md b/.changeset/8137-record-activity-calibration-repoint.md
deleted file mode 100644
index 48401d08a9..0000000000
--- a/.changeset/8137-record-activity-calibration-repoint.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Re-point the `record:activity.types` member calibration control in
-`registry-inputs-spec-parity` from `'Account'` to `''` (objectui#8137).
-`@objectstack/spec` 17.3.0 made that vocabulary open —
-`z.array(z.union([FeedItemType, z.string().min(1)]))`, objectstack#11658
-executing the maintainer's 2026-08-24 ruling on objectstack#11507 — so the old
-probe now accepts, while `''` is still refused for its CONTENT via `.min(1)`.
-Test only; no package is released by this change.
diff --git a/.changeset/8165-translatefn-one-authority.md b/.changeset/8165-translatefn-one-authority.md
deleted file mode 100644
index a172590962..0000000000
--- a/.changeset/8165-translatefn-one-authority.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Re-point `saveAdvisoryToast`'s `TranslateFn` at its one authority,
-`writeWarningToast` (objectui#8165), and shrink the `KNOWN_COLLISIONS` baseline
-in `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` by that
-site. Type-level only: the declaration was byte-identical to the one it now
-re-exports, the name is on no package's published face, and `export type { X }
-from '…'` erases at build — so no package is released by this change.
diff --git a/.changeset/8168-objectchart-absent-category-axis-refusal.md b/.changeset/8168-objectchart-absent-category-axis-refusal.md
deleted file mode 100644
index ccc98a7e6f..0000000000
--- a/.changeset/8168-objectchart-absent-category-axis-refusal.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/plugin-charts': minor
-'@object-ui/i18n': minor
----
-
-`ObjectChart` refuses an object-bound chart that declares no category axis (objectui#8168).
-
-An object-bound chart that named no field to group by used to be composed anyway.
-`runAggregate` passed `schema.aggregate` to `ds.aggregate(objectName, { field,
-function, groupBy, filter })` with no guard on `groupBy`, and the `ds.find` leg
-handed the same bag to `aggregateRecords`, which buckets every record on
-`record[groupBy] ?? 'Unknown'`. So one path asked a driver to group by `undefined`
-and the other collapsed the whole object into a single `'Unknown'` bar — and which
-of those a reader saw was decided by the data source, not by the renderer. The only
-loud states this component had were a fetch `error` (`chart-error`) and a generic
-"No data yet"; neither is a statement about an absent binding.
-
-`ObjectCalendar`, `ObjectGantt` and `ObjectTimeline` each already refuse a view that
-declares no axis. This is the fourth, following `ObjectTimeline`'s shape
-(objectui#7459): a `role="alert"` box, `data-testid="chart-missing-category-axis"`,
-naming the bindings the author can declare — `aggregate.groupBy`, `xAxisKey`,
-`xAxis.field` — rendered from the resolver's own vocabulary so the message cannot
-drift from what the resolver reads.
-
-**Breaking, deliberately — and this repo ships breaking as `minor`.** A chart that
-previously rendered an `'Unknown'`-bucketed bar (or whatever the driver did with
-`groupBy: undefined`) now renders the refusal instead. That is the intent: the
-picture it drew was not a picture of the data.
-
-It keys on the CATEGORY alone. A measure may legitimately be absent — `count` takes
-no field — so refusing on an absent measure would refuse `count` grouped by a
-declared category, a chart that renders correctly. Four shapes are deliberately
-untouched: an ADR-0021 `dataset` chart (which may declare no dimension), a chart
-carrying authored `data` or a `bind` scope (no field name is read to fetch those
-rows), a spec-shape `xAxis: { field }` with no `xAxisKey` (resolved through
-`normalizeChartSchema`, this package's one translation of the author-facing shape),
-and every schema the five in-repo producers compose today — all five floor their own
-category, so none of them can reach the refusal.
-
-This does **not** retire the six `'name'` / `'value'` floors at the three relay faces;
-that is the remainder of objectui#7547 and is mechanical only once this screen exists.
-
-New key `chart.unconfigured.noCategoryAxis` in all ten locale packs.
diff --git a/.changeset/8171-calendar-sort-input.md b/.changeset/8171-calendar-sort-input.md
deleted file mode 100644
index 48b7be711c..0000000000
--- a/.changeset/8171-calendar-sort-input.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-'@object-ui/plugin-calendar': minor
----
-
-Declare the `sort` input on both `object-calendar` registrations
-(objectui#8171) — the html tier stops reporting `unknown-prop` on a key the spec
-declares and `ObjectCalendar` already reads.
-
-objectui#7712's defect, one key over. `ObjectCalendar.tsx` lowers the authored
-key onto its own query as `$orderby: convertSortToQueryParams(schema.sort)`, and
-`@objectstack/spec`'s `ComponentPropsMap['object-calendar']` declares `sort`
-(measured on 17.2.0: `safeParse({ objectName, sort })` returns `success: true`,
-while the same strict schema on the same call refuses `bogusProp` by name — that
-control is what makes the acceptance a verdict). But neither of the two
-registrations that publish this renderer — `plugin-calendar:object-calendar` and
-`view:calendar` — listed `sort` in `inputs`, and `sdui-parser`'s `validateTree`
-reports `unknown-prop` for every key no `inputs` entry claims. So an author
-writing the one spelling that WORKS was told it was unknown — objectui#6678's
-shape, where a correct write draws the same diagnostic as a write that does
-nothing.
-
-ADR-0049 enforce-or-remove resolves toward **declare**, not remove: the key has
-live readers on both ends, so the registrations were the side that was wrong.
-Declared `type: 'array'`, matching the `filter` entry objectui#7712 put beside
-it and the `sort` that `object-grid`'s `GRID_QUERY_INPUTS` publishes — measured,
-that arm is the repo-wide convention for this key, all seven existing `sort`
-declarations write it.
-
-⛔ Not derived from the `ElementDataSourceMapping` sitting six lines above,
-which already asserts `sort: true`. That structure maps query keys for
-`ElementDataSourceGate`; it is not an authoring declaration, and objectui#7712
-measured why a mechanical derivation would be wrong — kanban's mapping also
-carries `limit`, which the spec refuses by name.
-
-⚠️ Carried forward from objectui#7712, because it stays true here:
-`check:react-blocks-declaration-parity` runs manifest → spec, one direction. A
-key the SPEC declares and the manifest omits is structurally outside what that
-ratchet measures, so declaring this key does **not** make the next omission
-loud. Making that ratchet bidirectional is objectui#8176.
-
-Rider carried by the same change, because objectui#8212 landed first: the
-console's `registry-inputs-spec-parity` ledger is updated in step with the
-declaration — the `object-calendar.sort` unpublished-key exemption is deleted
-(its cover expires the moment the key is declared), the shrink-only objectui#8176
-backlog ceiling drops 16 to 15, and `sort` gets a `MEMBER_PINS` entry rather than
-a member-pin exemption. `MEMBER_PIN_EXEMPTION_CEILING` is untouched at 62.
-
-⚠️ That member pin is deliberately NOT the identity-forwarding shape the two
-`filter` pins use. `ObjectCalendar.tsx` writes
-`$orderby: convertSortToQueryParams(schema.sort)`, which builds a new
-`field -> direction` map, so this key is lowered rather than passed through and
-`toBe` on `$orderby` is false about it. The pin asserts what is READ inside a
-member instead — `field`, `order`, an omitted `order` meaning ascending, and a
-member with no usable `field` dropped rather than invented.
-
-No version bump is declared for `@object-ui/console`: its only edit here is that
-test file's ledger data, which publishes nothing.
diff --git a/.changeset/8181-draft-envelope-read-decoration-strip.md b/.changeset/8181-draft-envelope-read-decoration-strip.md
deleted file mode 100644
index 42968d7ac0..0000000000
--- a/.changeset/8181-draft-envelope-read-decoration-strip.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/data-objectstack': patch
-'@object-ui/app-shell': patch
----
-
-fix(studio): one draft-envelope reader, and it strips the framework's read decorations
-
-`client.getDraft()` serves a DECORATED body — the draft branch stamps
-`_draft: true` and then `decorateMetadataItem` attaches `_diagnostics` for any
-type with a registered Zod schema. The spec names both READ-TIME decorations
-precisely because a served body "is NOT a valid input to the schema that
-produced it until these are removed" (`METADATA_READ_DECORATIONS`).
-
-objectui#7603 taught `ResourceEditPage` to strip them. It could only teach one
-site, because `extractDraftBody` existed **four times** — three verbatim copies
-plus a hand-rolled one in `ObjectHooksPanel` — and six more consumers unwrapped
-the envelope inline. Ten readers, one of which knew the rule.
-
-**The user-visible half.** The pending-changes sheet's per-entry diff compares
-the published body against the draft body key by key. Those two reads are
-decorated ASYMMETRICALLY — only the draft branch stamps `_draft` — so the sheet
-listed `_draft` under "Also changed:" on every entry that has a published
-counterpart, and `_diagnostics` alongside it whenever the two read-time verdicts
-differed. Framework-internal keys were being presented to the author as their
-own edits, on the screen where they decide whether to publish.
-
-**The rest.** Six sites merged a decorated body into a document they then wrote
-back through `save(..., { mode: 'draft' })` — the Studio app / page / object /
-flow surfaces, the package OWD panel, the object hooks panel, and the
-adapter's `updateView`. Today's server absorbs that (it strips read decorations
-on ingress, before its own schema gate), so nothing 400s; this is still a client
-emitting a body its own spec calls invalid, and the fix belongs at the producer.
-
-The cure is one function rather than ten strips: `extractDraftBody` is now
-exported from `@object-ui/data-objectstack`, beside the `getDraft` whose
-envelope it decodes. The key list is the spec's exported
-`stripReadDecorations` — never a second hand-maintained copy in this repo. The
-presence verdict still runs BEFORE the strip, so removing our own annotations
-can never turn a served draft into "nothing pending", and the ADR-0010
-protection envelope (`_lock`, `_provenance`, `_packageId`, `_packageVersion`)
-is deliberately untouched: those keys are declared by the closed schemas.
-
-No schema was loosened, and no gate was taught to tolerate `_diagnostics`.
diff --git a/.changeset/8193-objectview-canonical-groupbyfield.md b/.changeset/8193-objectview-canonical-groupbyfield.md
deleted file mode 100644
index 02972a93b4..0000000000
--- a/.changeset/8193-objectview-canonical-groupbyfield.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-list': minor
----
-
-Emit the spec-canonical kanban lane key from the object page, and teach the
-capability gate to recognize it (objectui#8193).
-
-`ObjectView` built the view-level kanban config it hands to `list-view` and wrote
-the deprecated alias `groupField` — never `groupByField`, the key
-`@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it
-already READ the canonical key first. Its sibling producer in the same package,
-`defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and
-left the reasoning next to itself ("that read-site now prefers the spec key, so
-one key is enough"); the twin was not carried along, so one producer surface
-spoke two vocabularies for one concept depending on which entry point you
-arrived through. `ObjectView` writes into `options.kanban`, which
-`normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing
-corrected it downstream and the legacy spelling was what drove the lanes.
-
-The expression is now the exported `kanbanViewOptions`, the fifth member of the
-`timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` /
-`galleryViewOptions` family it had been the odd one out of.
-
-**`ListView`'s kanban capability gate changed with it, and had to.** The gate
-consulted the `options.kanban` bag for the LEGACY spelling only, so a producer
-writing the spec key into the bag was invisible to it while still rendering
-correctly — the render branch merges the bag and resolves
-`groupByField || groupField`, so the gate recognized strictly less than what
-renders. Measured before and after: a bag of `{groupBy, groupField}` offered
-Kanban and `{groupBy, groupByField}` did not, so shipping the producer change
-alone would have removed the Kanban toggle from every object view. The gate now
-reads both spellings out of the bag — the same one-question-two-sites repair
-objectui#5042 made for `map` and objectui#7544 for `chart`.
-
-**No alias READ was removed, and the alias is not retired.** Stored metadata
-still authors `groupField`, and every read site still resolves it; `ListView`
-already preferring the canonical key is precisely why the sibling producer could
-drop the alias write. What changed is only what this one face WRITES, plus one
-added rung on the gate.
-
-**Migration.** Nothing authored has to change. If you read
-`options.kanban.groupField` off the schema `ObjectView` produces, read
-`groupByField` (or both) instead — that bag now carries the spec spelling.
-
-The view-level `groupBy` in the same bag is untouched. It is not a spec key
-either — measured against the strict `KanbanConfigSchema`, which refuses it by
-name — but `ListView`'s projection collectors read it, so retiring it needs its
-own producer census and is filed as objectui#8213.
diff --git a/.changeset/8194-fields-date-widget-convention.md b/.changeset/8194-fields-date-widget-convention.md
deleted file mode 100644
index 8fe0942574..0000000000
--- a/.changeset/8194-fields-date-widget-convention.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/fields': minor
----
-
-One home for the `date` display convention in the readonly field widgets
-(objectui#8194).
-
-Four readonly `date` faces in `@object-ui/fields` called
-`toLocaleDateString(locale)` with **no options bag at all** — `Intl`'s numeric
-default — so they never implemented the year-dropping decision the shared
-`formatDate` documents and every `date` CELL already follows. They now call
-`formatDate` (default style):
-
-- the readonly `DateField` (the form / detail face, and what `FieldEditWidget`
- renders in the grid and detail inline editors),
-- the sub-grid `GridField`'s readonly `date` column,
-- a `FormulaField` declaring `return_type: 'date'`,
-- the lookup picker's plain-text `$date` fallback (`lookupColumnDisplay`),
- which sits in the same function as the descriptor path that already rendered
- through `formatDate`.
-
-**Visible change**: every one of those faces changes shape in every locale, in
-every year — not only the year token. In `en-US` a date renders `Jul 4` this
-year and `Jul 4, 2024` for a past year, where it used to render `7/4/2026` and
-`7/4/2024`; in `de` `4. Juli` / `4. Juli 2024` for `4.7.2026` / `4.7.2024`; in
-`zh` and `ja` `7月4日` / `2024年7月4日` for `2026/7/4` / `2024/7/4`; in `ar`
-`4 يوليو` / `4 يوليو 2024`. Each now matches the `date` cell beside it. This is
-a larger move than the sibling change in `@object-ui/components`
-(objectui#7620), whose former face already asked for a short month and so only
-lost its year token — these four passed no bag whatsoever.
-
-A value the formatter cannot parse now reads `—` at three of the four sites
-instead of the literal `Invalid Date`. The sub-grid keeps showing the raw
-stored string for an unreadable value, unchanged (objectui#3569).
-
-Untouched: the `datetime` readonly faces (`DateTimeField`, the sub-grid's
-`datetime`/`time` branch). They are the same omission one type over, but their
-home is `formatDateTime`, whose named faces are a separate display-convention
-question; they are recorded on their own card rather than picked here.
-
-A surface that genuinely wants the year on every row is an explicit `format`
-style honoured by both paths, not a second option bag — the objectui#7620 /
-objectui#7443 / objectui#4576 lesson, one surface over.
diff --git a/.changeset/8198-unused-runtime-declarations-removed.md b/.changeset/8198-unused-runtime-declarations-removed.md
deleted file mode 100644
index 0749e35e3a..0000000000
--- a/.changeset/8198-unused-runtime-declarations-removed.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-'@object-ui/auth': minor
-'@object-ui/console': minor
-'@object-ui/core': minor
-'@object-ui/fields': minor
-'@object-ui/layout': minor
-'@object-ui/plugin-ai': minor
-'@object-ui/plugin-calendar': minor
-'@object-ui/plugin-chatbot': minor
-'@object-ui/plugin-dashboard': minor
-'@object-ui/plugin-designer': minor
-'@object-ui/plugin-editor': minor
-'@object-ui/plugin-map': minor
-'@object-ui/plugin-markdown': minor
-'@object-ui/plugin-report': minor
-'@object-ui/react': minor
-'@object-ui/runner': minor
----
-
-Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
-the direction so the next one cannot land (objectui#8198).
-
-`check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
-so a declaration could outlive its last consumer indefinitely. That is what happened to
-`recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
-was removed by hand on objectui#7625, and nothing would have reported the next one. The
-new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
-`optionalDependencies` of every released package.
-
-**Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
-changes: their Vite `external` predicates are path-based and never read `dependencies`,
-so no built artifact moves. What changes is the install graph — a project that imports
-one of the removed packages while depending only on the ObjectUI package that used to
-drag it in will no longer resolve it. Declare it directly; that is the correct
-dependency edge in either case. The removals, by package:
-
-- `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
-- `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
-- `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
-- `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
-- `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
-- `@object-ui/core`: `lodash`, `zod`
-- `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply
-- `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
-- `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
-- `@object-ui/fields`: `clsx`, `tailwind-merge`
-- `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
-- `@object-ui/auth`: `@object-ui/types`
-- `@object-ui/plugin-calendar`: `@object-ui/fields`
-- `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
-- `@object-ui/react`: `react-hook-form`
-
-Every one was verified by a whole-package grep before removal — the name appeared nowhere
-under the package but its own manifest and CHANGELOG — and the whole workspace builds,
-type-checks and tests green afterwards.
diff --git a/.changeset/8201-kanban-calendar-scalar-authoring-keys.md b/.changeset/8201-kanban-calendar-scalar-authoring-keys.md
deleted file mode 100644
index e98c998ec1..0000000000
--- a/.changeset/8201-kanban-calendar-scalar-authoring-keys.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-'@object-ui/plugin-kanban': patch
-'@object-ui/plugin-calendar': patch
----
-
-`object-kanban` / `view:kanban` and `object-calendar` / `view:calendar` now DECLARE seven
-spec-carried keys their renderers already honoured, so the html tier stops reporting working
-metadata as `unknown-prop` (objectui#8201, the backlog objectui#8176 exposed).
-
-Board: `groupBy`, `cardTitle`, `titleField`, `swimlaneField`, `coverImageField`.
-Calendar: `defaultView` (the spec's three-member enum) and `locale`.
-
-No renderer behaviour changes — every one of these keys was already read and already acted on;
-what changes is that authoring tools can now discover them and the save gate agrees with the
-validator. Both tags of each block now spread ONE shared `inputs` list, so the two published
-surfaces cannot drift apart by hand-copy.
diff --git a/.changeset/8204-refuseskind-invalid-type-decisive.md b/.changeset/8204-refuseskind-invalid-type-decisive.md
deleted file mode 100644
index 215d941eff..0000000000
--- a/.changeset/8204-refuseskind-invalid-type-decisive.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
----
-
-Test-only change: repair the `registry-inputs-spec-parity` kind classifier so a member
-whose contract is a `z.union` is judged by what the union actually refused. No published
-behaviour changes; nothing outside `apps/console/src/__tests__/` is touched.
-
-objectui#8204. `refusesKind` asked "is EVERY complaint at this node a kind complaint",
-and Zod keeps running a schema's checks after its type check has failed: for
-`z.string().min(1)` the probe `[]` comes back as both `invalid_type` AND `too_small`.
-Read by `every`, that pair is a CONTENT refusal — so the array probe that
-`@objectstack/spec` 17.3.0's `RecordActivityProps.types`
-(`z.array(z.union([FeedItemType, z.string().min(1)]))`) measurably refuses put `array`
-into that key's accepted kind set, and the derivation gate reported
-`record:activity.types → contract accepts {string,array}` for a key whose array members
-the contract rejects.
-
-`invalid_type` at a node is now decisive: a value cannot be simultaneously the right kind
-and the wrong one, so whatever else the node says about a value it has already refused
-for its kind is downstream of that refusal, not a second opinion. Measured against the
-installed 17.3.0 artifact, `record:activity.types` goes `{string,array}` to `{string}`
-and no other judged key moves; `record:highlights.fields`, a genuine member union, is
-still reported as `{string,object}`.
-
-The suspected mechanism "`refusesKind` does not recurse into `z.union`" was measured and
-is FALSE — the recursion runs and is what carries branch verdicts up. The per-branch
-reading underneath it was the defect.
diff --git a/.changeset/8218-studio-property-panel-de-developerize.md b/.changeset/8218-studio-property-panel-de-developerize.md
deleted file mode 100644
index a805a8b99d..0000000000
--- a/.changeset/8218-studio-property-panel-de-developerize.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-De-developerize the metadata-admin property form now that Studio's 「界面」 panel
-grafts it in front of an AI-build maker (objectui#8218).
-
-`SchemaForm` + `widgets.tsx` were written for an administrator editing metadata.
-Studio's interface panel renders the very same form for someone who has never
-seen a JSON Schema, so four developer habits landed inside an otherwise fully
-Chinese surface. Same cause, one pass:
-
-- **Machine-name tooltip.** The `title="Machine name"` on the identifier chip
- went through the engine string table. Note the chip's own predicate degrades
- under localization: it shows when the visible label does not spell the machine
- name, which a translated label never does — so in a Chinese panel it renders
- for *every* field. Reported, not changed here; whether a maker should see the
- identifier at all is a product call, not a rendering one.
-
-- **Master-detail column headers.** A column whose item schema carries no
- `title` printed the raw JSON Schema key (`actionUrl`, `actionType`) as if it
- were a column name — in *every* locale, English included. It now humanises the
- key, the same convention this tree already applies wherever a title is absent
- (`json-schema-to-fields.ts`'s `prop.title || humanizeKey`, and `SchemaForm`'s
- own grid repeater: `s.label || prettify(s.field)`). `title` is an OPTIONAL
- annotation, so its absence is not off-spec metadata and the renderer has to
- render something; this is not a lenient contract fallback. The LOCALIZED
- column name needs an upstream channel that does not exist yet — filed there.
-
-- **Numeric fields.** Both numeric renderers (`SchemaForm`'s field control and
- the master-detail cell) now gray the schema's `default` in as a placeholder,
- so an empty box reads "using the default" instead of "unknown", and forward
- `minimum` / `maximum` / `multipleOf` onto the control. Previously only
- `fieldSpec.min` / `.max` were read — which a spec-derived authoring form never
- declares — so a panel accepted a negative column count its own contract had
- already ruled out. The default stays a placeholder, never a written value, so
- "left on the default" remains distinguishable from "pinned to today's default"
- in the saved metadata.
-
-- **Untranslated strings.** A full sweep of both files: 45 user-visible literals
- (aria-labels, placeholders, empty-state prose, segmented-control labels,
- secret-field copy, the code editor's chrome) now resolve through the engine
- table in `en` and `zh`.
diff --git a/.changeset/8266-dashboard-count-aggregate-series-key.md b/.changeset/8266-dashboard-count-aggregate-series-key.md
deleted file mode 100644
index c4fc28fefb..0000000000
--- a/.changeset/8266-dashboard-count-aggregate-series-key.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-charts': patch
----
-
-Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing
-(objectui#8266).
-
-A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }`
-and no `field` — the normal way to author "how many records per status" — rendered an
-empty chart. No error, no empty state: a plot frame with the category ticks drawn and
-not one mark in it, which reads exactly like "this object has no rows yet".
-
-**Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each
-built the series binding as `aggregate?.field || (options.yField || 'value')`, which for
-a fieldless count resolves to `'value'`. The rows an object-bound fieldless count
-returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned
-since framework#3701. A `dataKey` naming a column no row carries plots nothing, and
-neither of the renderer's two guards fires on it: the rows DO carry the category key,
-and the series array is not empty.
-
-**Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to
-`chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of
-"the value column an object-bound aggregate produces". Both relays now consult it, and
-the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed
-through the same function, so the two halves of the question cannot drift again.
-
-**What moves on screen.** A chart that was blank now draws. Charts that already drew are
-unaffected: a field-bearing aggregate resolves to its raw field under both the old and
-the new reading, and a chart with no `aggregate` at all keeps the author's `yField`.
-One authored key changes meaning: a `yField` written on an object-bound chart that
-ALSO declares an aggregate no longer wins over the aggregate's own column — it named a
-record column that a grouped aggregate never returns, so it plotted nothing before.
-
-**Not fixed here, and out of scope.** The same widget with no `options.xField` is
-refused by the category-axis guard naming `name`, a key the author never wrote (they
-wrote `aggregate.groupBy`). That is the category half of the same relay gap and is
-filed separately.
diff --git a/.changeset/8269-dashboard-category-axis-groupby.md b/.changeset/8269-dashboard-category-axis-groupby.md
deleted file mode 100644
index d6e554dd06..0000000000
--- a/.changeset/8269-dashboard-category-axis-groupby.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-charts': patch
----
-
-Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being
-refused for lacking a `name` column (objectui#8269).
-
-A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }`
-and no `options.xField` rendered a refusal instead of a chart:
-
-> This chart cannot plot its category axis: no row has a `name` field.
-
-The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that
-had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent
-them to debug the wrong layer.
-
-**Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each
-floored the category binding on a literal — `options.xField || 'name'` — and handed it to
-the `object-chart` node without ever consulting the aggregate that decides it. An
-object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no
-row carried `name` and the category-axis guard (framework#4033) fired correctly on a
-binding that was already wrong when it arrived.
-
-**Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to
-`chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of
-"the category column an object-bound aggregate produces", and the published sibling of the
-`chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now
-consult it for the object-provider branch.
-
-**What moves on screen.** A widget that rendered a refusal now draws. Measured through
-`ChartRenderer` at 480x320 over the rows a fieldless count returns
-(`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from
-`xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`,
-1 series and 2 marks with the category ticks drawn.
-
-**Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows
-are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it
-returns a single row with no category column), and the authored-literal-rows branch — the
-`chart` node composed after the object-provider check fails — keeps its floor unchanged.
-One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField`
-written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the
-aggregate's own column — it named a record column a grouped aggregate never returns, so it
-produced the same refusal before.
diff --git a/.changeset/action-callback-retired-7068.md b/.changeset/action-callback-retired-7068.md
deleted file mode 100644
index 0ed6b08f8d..0000000000
--- a/.changeset/action-callback-retired-7068.md
+++ /dev/null
@@ -1,77 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Breaking for authored metadata:** the legacy `ActionSchema`'s Phase-2 callback
-pair — `onSuccess` / `onFailure`, each carrying an `ActionCallback` object — is
-RETIRED, and the `ActionCallback` type and its Zod mirror `ActionCallbackSchema`
-(with the inferred `ActionCallbackSchemaType`) are DELETED from `@object-ui/types`
-and `@object-ui/types/zod` (objectui#7068; maintainer ruling option 1 of
-2026-09-05, immediate, no deprecation window; ADR-0049 enforce-or-remove).
-
-**What an author who wrote the shape sees now.** A `{ type: 'action', … }`
-document authoring `onSuccess: { type: 'toast', message: '…' }` (or any
-`onFailure` callback) no longer validates: the parse fails loudly on the
-`onSuccess` / `onFailure` path (`invalid_type`, expected `never`) with the
-explanation and the migration in the message, and the TypeScript members are
-`?: never` tombstones so the same document is a `tsc` error at the authoring
-site. `import type { ActionCallback } from '@object-ui/types'` and
-`import { ActionCallbackSchema } from '@object-ui/types/zod'` fail to resolve.
-
-**What was measured, on this branch's base (`900f8d99`).** `ActionCallback`
-(`{ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' |
-'dialog', message?, url?, api?, method?, dialog?, handler? }`) was declared in
-`crud.ts`, mirrored in `zod/crud.zod.ts`, re-exported by both barrels, and
-carried on the legacy `ActionSchema` as `onSuccess?` / `onFailure?`. Producers:
-the package's own `phase2-schemas.test.ts` fixture and three `ts` fences in
-`content/docs/core/enhanced-actions.mdx` — nothing else (`git grep -l
-ActionCallback` over `packages content skills` hit the five `packages/types`
-files; positive control `SchemaNodeSchema` hit 22). Runtime readers: none —
-`ActionRunner` imports `UIActionSchema`, never this interface, and its own
-`ActionDef.onFailure` is a different (runner-native) meaning. It was the THIRD
-meaning of one key: objectui#5934 had already retired the runner's callback
-meaning of `onSuccess` and converged it on the spec's block.
-
-**Why authored JSON that passed publish is unaffected.** `@objectstack/spec`'s
-`ActionSchema` (installed pin 17.2.0) already refused the callback shape at
-publish — `invalid_type` at `onSuccess.navigate` plus `unrecognized_keys` on the
-`onSuccess` block, and `onFailure` refused as an unrecognized key on the action —
-so no published or saved metadata could carry it. Only TypeScript code that
-typed a callback against the legacy interface, or JSON validated solely through
-`@object-ui/types/zod`, meets the new refusal.
-
-**Where the live meaning lives.** Post-success navigation is the spec's
-`onSuccess` block, `{ navigate, openIn }`, declared on `UIActionSchema`
-(`ui-action.ts`) and forwarded to the runner (objectui#5934). A success or
-failure notice is `successMessage` / `errorMessage` — adjacent keys on the same
-legacy `ActionSchema`, NOT retired, and still accepted on both faces.
-
-**Two published faces, one retirement — tombstone on the keys, deletion of the
-type.** `BaseSchema` is `.passthrough()` on the mirror and carries an index
-signature on the interface, so DELETING the two keys would have ADMITTED an
-authored callback unchecked on both faces; they stay declared as `?: never` /
-`retirementTombstone()` (the PR #7761 / #7769 shape) and the base-vs-extended
-contrast is pinned. The standalone `ActionCallback` / `ActionCallbackSchema` have
-no such escape hatch and are deleted outright, the route objectui#7664 / PR #7743
-took for the `DeclarativeKanban*` trio; the parity ledger drops the pair
-(`EXPECTED_MIRROR_PAIRS` 159 → 158) and the absence is pinned in
-`action-callback-retired-7068.test.ts`.
-
-**Docs, same change.** `content/docs/core/enhanced-actions.mdx` — the three
-`onSuccess` / `onFailure` fences author `successMessage` / `errorMessage`
-instead, and the "Callbacks" section is a "Post-success behaviour" note pointing
-at the spec block (no fence: the legacy type carries no spec-derived block).
-`content/docs/guide/schema-overview.md` — the fragment line, the feature bullet
-and the checklist row are rewritten to the truth (the ✅ claim is now a
-retirement note).
-
-**Migration:** delete `onSuccess` / `onFailure` from any legacy `ActionSchema`
-document or fixture; write `successMessage` / `errorMessage` for notices, put
-follow-up work in `chain`, and author post-success navigation as the spec's
-`onSuccess: { navigate, openIn }` block on `UIActionSchema`.
-
-Graded `minor`, not `patch`: this narrows the accepted input set on both faces
-and removes two exports, which is breaking for any consumer who wrote the shape.
-It is not `major` per this repo's fixed-group convention (objectui's own breaking
-changes ship as `minor`; the group's major tracks `@objectstack` — AGENTS.md
-版本号策略, mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/action-designer-param-vocabulary.md b/.changeset/action-designer-param-vocabulary.md
deleted file mode 100644
index 9b82c424a0..0000000000
--- a/.changeset/action-designer-param-vocabulary.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-"@object-ui/app-shell": patch
----
-
-fix(app-shell): the Action designer's preview draws what the runtime dialog will draw
-
-`ActionDefaultInspector` offers eight param `type` spellings; `ActionPreview`'s
-dialog mock switched on five of them over a private table, so three of the eight
-previewed as a control `ActionParamDialog` does not render — `datetime` and a
-targeted `lookup` as plain text boxes, and a `select` whose options were not
-authored yet as a text box as well. A `text` param that happened to carry
-`options` previewed as a select the runtime never draws, for the same reason in
-the other direction.
-
-The mock now resolves each param through `paramToField`'s
-`resolveParamWidgetType` / `paramDegradesWithoutTarget` — the same adapter the
-dialog itself renders through — so the two panes cannot disagree about a
-spelling again. `datetime` draws a date/time control, a `lookup` with a declared
-`reference` draws a record picker, a targetless one draws the record-id text box
-the dialog degrades to and says why, and a `select` always draws a picker.
-
-The per-param editor also gains an `options` control for `select` params. It had
-none, and `params` is hidden from the collapsed "More fields" form, so the panel
-that offered the type had nowhere to author the choices the type needs.
diff --git a/.changeset/action-keys-warning-text-5642.md b/.changeset/action-keys-warning-text-5642.md
deleted file mode 100644
index d4fdb94a12..0000000000
--- a/.changeset/action-keys-warning-text-5642.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/core': patch
----
-
-The dev-mode unknown-key warning stops stating a fact that was retired, and
-sends the author to the file that actually declares the interface
-(objectui#5642).
-
-Both halves of the message's tail had outlived the change they described. It
-told the author the key was warned about rather than rejected because
-`ActionDef` "still carries `[key: string]: any`" — objectstack#4075 step 3
-deleted that index signature, and `actionKeys.pin.test.ts` pins the deletion in
-the opposite direction (`{ ActionDef: false, ActionContext: true }`), while
-`actionDef-closed-surface.test.ts` pins that `tsc` now rejects exactly such a
-key at the construction site. And it prescribed promoting the key to an explicit
-field on `ActionDef` "(packages/core/src/actions/actionKeys.ts)" — that file
-holds the INVENTORY (`ACTION_DEF_KEYS`); the interface is in `ActionRunner.ts`.
-The wrong pointer had teeth: an author who followed it edited the inventory
-alone, which is precisely the half-change the pin test reddens on, since it
-re-derives the inventory from the interface's AST.
-
-The tail now carries the reason the module's own header already gives for why
-this warning survived step 3 — the two mechanisms cover disjoint populations.
-`tsc` sees action literals authored in code; the warning sees actions that
-arrive as data, from stored rows that are rehydrated unparsed and that no
-compiler ever looked at (objectstack#3903). The prescription names
-`ActionRunner.ts` for the field and `ACTION_DEF_KEYS` as the same-commit second
-edit, saying why.
-
-No behaviour change: the classification logic, the key inventory and its
-derivation are untouched, and the warning fires on exactly the same actions as
-before. Two comments in the same file and two in the pin test that described the
-pre-step-3 world were refreshed in the same pass, and the message text is now
-pinned — the printed interface path is resolved off the message and read, so a
-move or rename reddens by name instead of shipping a second dead prescription.
diff --git a/.changeset/action-onsuccess-forward-5493.md b/.changeset/action-onsuccess-forward-5493.md
deleted file mode 100644
index b74a69cef5..0000000000
--- a/.changeset/action-onsuccess-forward-5493.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`action:button`, `action:icon`, `action:group` and `action:menu` now forward the
-declared `onSuccess` block, so the post-success navigation an author writes in metadata
-actually happens on those four surfaces (objectui#5493).
-
-`onSuccess` — spec's closed strict `{ navigate, openIn }` object — became authorable on
-`ActionSchema` with the `@objectstack/spec` 17.1.0 pin bump (objectui#5328), and the
-runner has read it off the **forwarded** def since objectui#5221:
-`ActionRunner.handlePostExecution` → `readOnSuccessNavigation` → `navigateOnSuccess`,
-which hops through the app's own `navigationHandler` (a real SPA route change, immune to
-popup blocking). Between those two halves sat these four forward whitelists, which never
-carried the key. The action succeeded, the toast said so, and the declared hop silently
-did not happen — the same "shipped green while dropped one hop before the runner" class
-as `bodyExtra` (objectstack#6837), `bodyShape` (objectstack#6938) and `resultDialog`
-(objectui#3646).
-
-Reachability before this change was a function of which host rendered the action: the
-full-def-spread hosts (`DeclaredActionsBar`, `ObjectGrid.onActionDef`,
-`RelatedRecordActionsBridge`, `useNavActionDispatch`) already carried it through, so the
-same declaration hopped there and did nothing here — and on an `action:bar`, whether an
-action lands inline (`action:button`) or in the overflow (`action:menu`) is decided by
-`maxVisible`, 3 desktop / 1 mobile. The declared navigation therefore depended on
-viewport width.
-
-Pinned end to end in
-`packages/components/src/renderers/action/__tests__/action-onSuccess-forward.test.tsx`:
-one row per surface drives the real renderer through the real runner and asserts the
-`${result.*}`-interpolated url reaches `onNavigate`, with `openIn` exercised on both
-branches. Each row asserts the action executed first, so a zero-navigation reading
-cannot be a harness that did nothing.
-
-The four matching `KNOWN_GAPS` entries in `scripts/check-action-forward-parity.mjs` are
-deleted — that ledger is ratcheted, so a stale entry excusing a key that is now
-forwarded fails the gate. `element:button` is untouched and stays correct: `onSuccess`
-is not on spec's `InlineActionSchema` pick list, so that surface never owed it.
diff --git a/.changeset/action-param-degrade-predicate-5654.md b/.changeset/action-param-degrade-predicate-5654.md
deleted file mode 100644
index 2fc5978fe4..0000000000
--- a/.changeset/action-param-degrade-predicate-5654.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-**Behaviour change:** a `master_detail` action param with no reference target now
-receives the same "paste a record id" placeholder and help text a targetless
-`lookup` param has had since objectui#3405. Previously it degraded to a plain
-text input exactly like `lookup` did, but silently — the user was shown an
-unexplained empty box that wanted a bare record id.
-
-The cause was two hand-maintained answers to one question. `paramToField()`
-performs the degradation over RESOLVED widget keys (`lookup`, `master_detail`),
-while `ActionParamDialog` decided who gets the hints with its own literal over
-RAW param spellings (`'lookup' || 'reference'`). Neither set contained the
-other: `master_detail` degraded with no hints, and `reference` was a copy of an
-alias-table row the adapter folds to `lookup` before it tests membership.
-
-`paramToField` now exports `paramDegradesWithoutTarget(param)` and both the
-adapter's own fallback branch and the dialog's two hint readers ask it, so "who
-degrades" and "who gets hints after degrading" are one member set answered once
-(objectui#5654; same convergence shape as objectui#4770 / #4790 / #4815).
-
-Params that already had the hints keep them, including the `reference` spelling;
-a param that renders as text without having degraded (an unknown type) still
-gets no picker hints.
diff --git a/.changeset/action-runtime-same-origin-only-5702.md b/.changeset/action-runtime-same-origin-only-5702.md
deleted file mode 100644
index 1340b71436..0000000000
--- a/.changeset/action-runtime-same-origin-only-5702.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-**Behaviour change:** the console action runtime (`useConsoleActionRuntime`) now
-builds its authenticated fetch with `sameOriginOnly: true`, matching the
-`provider: 'api'` data-source lane (`ConsoleShell`). A metadata `type: 'api'`
-action whose resolved target is a different origin than the page is fetched
-through the bare global fetch: the platform Bearer token, `X-Tenant-ID`, and
-`Accept-Language` are no longer attached (objectui#5702, maintainer ruling
-2026-08-22). Previously the Bearer rode to any off-origin target whose URL
-contained `/api/`, and `X-Tenant-ID` rode to every off-origin target
-unconditionally.
-
-Same-origin actions — including every relative target in a same-origin
-deployment — are unchanged, and off-origin requests still execute (pass-through,
-not a refusal). An off-origin integration that legitimately needs the platform
-bearer declares itself explicitly or proxies same-origin.
-
-Note for split-host setups: `apiHandler` prefixes relative action targets with
-`VITE_SERVER_URL`. When that is set to an origin different from the page's,
-those action requests are off-origin and no longer carry credentials. The
-committed dev and starter configurations are unaffected: since objectui#5745
-they ship `VITE_SERVER_URL` empty and reach a split-host backend through the
-Vite `/api` dev proxy, so their action requests are same-origin.
diff --git a/.changeset/active-org-clear-removal-failure-5731.md b/.changeset/active-org-clear-removal-failure-5731.md
deleted file mode 100644
index aacb03bc76..0000000000
--- a/.changeset/active-org-clear-removal-failure-5731.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-`ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually
-gone and, when it is not, both reports the failure and stops `get()` answering from
-the surviving value — instead of swallowing the failed removal (objectui#5731).
-
-`clear()` nulled `_memoryValue` and then removed the persisted key inside a
-`try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a
-NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two
-halves of `clear()` were not equally strong: nulling memory always sticks, while a
-removal that did not stick left the key readable and the read order preferred it.
-Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does
-not stick", and it was silent — the cleared organization went back on the wire as
-`X-Tenant-ID` on every subsequent request.
-
-The removal is now judged by a READ-BACK rather than by catching the throw, which is
-both narrower and wider in the right directions. Wider: a wrapped or proxied
-`localStorage` whose `removeItem` is a silent no-op never throws and leaves identical
-residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where
-every operation throws have nothing readable to resurrect, were already safe, and are
-not reported as failures.
-
-A key whose removal could not be verified is quarantined in memory for the rest of the
-page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`,
-which `clear()` has just nulled and which a later `set()` refills with the value that
-write was meant to persist. The quarantine is released as soon as a removal on that key
-sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract
-(objectui#5279); a re-stamped signed-out organization is not.
-
-The failure is not thrown and not returned. All five call sites — sign-out's
-`purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`,
-`leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive
-after the transition they follow up on has already happened, and none can act on a
-storage failure; a `boolean` every caller ignores would read as handled when it is not.
-So the invariant is restored inside `clear()` and the failure is reported to the
-console.
-
-A working `localStorage` behaves exactly as before: the removal sticks, nothing is
-quarantined, nothing is reported, and a non-null persisted read is still authoritative.
-`set()`'s swallowed write failure is deliberately untouched — that swallow is
-objectui#5703's memory fallback, and it is the correct kind, because the memory copy
-upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s.
diff --git a/.changeset/active-org-storage-memory-fallback-5703.md b/.changeset/active-org-storage-memory-fallback-5703.md
deleted file mode 100644
index eef9655d93..0000000000
--- a/.changeset/active-org-storage-memory-fallback-5703.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-`ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls
-back to the in-memory value otherwise, instead of returning the `localStorage` read
-unconditionally — so the fallback is reachable in the browser state it was written for
-(objectui#5703).
-
-`set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but
-`get()` only consulted `_memoryValue` when the READ itself threw. There is a real
-browser state where the read does not throw and the fallback is nonetheless the only
-copy: `localStorage` present and readable but rejecting writes — Safari private
-browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`.
-In that state the active org was stored and could not be read back, measured as
-`_memoryValue = org-42` alongside `get() = null`.
-
-The cost was silent and lasted the whole session rather than the documented first-boot
-window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on
-any request. Per the edge contract documented on objectui#5279 that header is a routing
-hint a reader falls through on — the framework scopes from the session — so no row
-visibility rode on this; what was missing is the tenant-routing input, on every request.
-`switchOrganization` also appeared to succeed while the client-side stamp never
-followed.
-
-Sign-out is unaffected, and that is the half worth stating: the new fallback fires
-exactly when the `localStorage` read is null, which is the state `clear()` leaves
-behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That
-property is now pinned by test rather than relied upon, so a future `clear()` that only
-removed the persisted key fails a test instead of quietly re-stamping a cleared org.
-
-A non-null persisted read still wins over the memory value, so a working `localStorage`
-behaves exactly as before.
diff --git a/.changeset/activity-map-single-reading-5878.md b/.changeset/activity-map-single-reading-5878.md
deleted file mode 100644
index b04790a791..0000000000
--- a/.changeset/activity-map-single-reading-5878.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-detail': minor
----
-
-The console record page and the `record:activity` block read ONE `sys_activity`
-type table, so a scheduled meeting no longer appears on one and vanishes on the
-other.
-
-`RecordDetailView`'s `sys_activity` merge carried a hand-written copy of the
-table that `record:activity` exports as `ACTIVITY_TYPE_TO_FEED_TYPE`. Neither
-file imported the other and nothing compared them, so the two could drift
-silently — and they had. objectui#5840 added `scheduled` -> `event` to the
-exported table, because a shipped producer (HotCRM's `schedule_meeting`) writes
-that value and the row was being dropped before any filter ran. The copy here
-was left untouched, so the same row rendered on a hand-authored record page and
-was dropped on the console record page: same record, same row, two answers.
-
-`RecordDetailView` now imports the exported table, and the copy is gone. The
-table is re-exported from `@object-ui/plugin-detail`'s entry point, which is
-what makes a single reading possible at all — it was previously reachable only
-from inside the plugin. No module enters the eager closure: the module holding
-it was already pulled in by the renderer beside it.
-
-Adding an activity type is now one edit, in one place, that both surfaces see.
-A re-fork is caught rather than merely discouraged: the new pins spy on the
-shared object and inject a member into it at runtime, so a private copy holding
-today's members exactly — the failure this change removes — fails, where a
-value comparison would pass on the defect.
-
-Only the table converges. The row-to-`FeedItem` construction around it is still
-written twice, so an unmapped activity type is still dropped silently on the
-console surface where the block warns once; that mirror is filed separately.
diff --git a/.changeset/adapter-advisory-sink-pin.md b/.changeset/adapter-advisory-sink-pin.md
deleted file mode 100644
index 7a9a2c668e..0000000000
--- a/.changeset/adapter-advisory-sink-pin.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: pins that a metadata-save advisory emitted by the `ObjectStackAdapter` actually reaches the toast sink `AdapterProvider` wires to it. No published behaviour changes.
diff --git a/.changeset/adapter-meta-type-singular-4940.md b/.changeset/adapter-meta-type-singular-4940.md
deleted file mode 100644
index a96ab64517..0000000000
--- a/.changeset/adapter-meta-type-singular-4940.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/data-objectstack': patch
----
-
-`ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata
-types in the singular, matching the other twelve `client.meta.*` call sites in this
-file (objectui#4940).
-
-`getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed
-the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued
-its singular spelling was deliberate — the plural site was silent. Both plural sites
-resolved today only because the server folds plural → singular
-(`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`,
-confirmed by reading both the mapping and the by-name route handler that calls it), so
-this is consistency restoration rather than a behavior change — nothing a user hits was
-broken, and nothing a user hits changes.
-
-`appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended
-with two new cases asserting `getApp`/`getPage` pass the singular type to
-`client.meta.getItem`, so a future revert to the plural spelling fails a test instead of
-depending on the server-side fold staying in place.
diff --git a/.changeset/adr-0057-d10-citation-attribution-5202.md b/.changeset/adr-0057-d10-citation-attribution-5202.md
deleted file mode 100644
index e30834a070..0000000000
--- a/.changeset/adr-0057-d10-citation-attribution-5202.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
----
-
-Traceability only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. The change is comment text at seven live source sites; no
-executable line moves, and `git diff -U0` carries zero non-comment added or removed lines.
-
-`server enforces, client is courtesy` was cited at those seven sites as a bare
-**`ADR-0057 D10`**. The substantive claim is correct and is unchanged here; what was missing
-is the **framework qualifier**. In this repository the bare string resolves to
-`docs/adr/0057-console-ai-chat-one-conversation-docked.md` — a document about console AI chat
-docking, which contains no `D10` at all and is the one a reader greps first. The intended
-anchor is the *framework's* ADR-0057, whose D10 decides *"Setup-nav surfacing follows the
-capability (ADR-0029 K2); the object stays open"*.
-
-Each site now carries the disambiguation already shipped by the two authorities in this
-repository — `docs/adr/0036-field-conditional-rules.md:91` and
-`packages/core/src/evaluator/fieldRules.ts:38` — rather than a third phrasing:
-`the framework's ADR-0057 D10 — framework numbering; this repo's own ADR-0057 is an
-unrelated document`.
-
-Three sites are deliberately left byte-untouched, all three already correct:
-`packages/data-objectstack/src/appAccessProbe.test.ts:25` (a verbatim quotation of the
-objectstack#8013 ruling, and about the capability/nav gate — the one family D10 really does
-decide), plus the two authorities above.
diff --git a/.changeset/ai-chat-public-share-base-4482.md b/.changeset/ai-chat-public-share-base-4482.md
deleted file mode 100644
index 3ad06d0251..0000000000
--- a/.changeset/ai-chat-public-share-base-4482.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`AiChatPage`'s public share-link base now resolves through the one console-mount
-resolver instead of a private copy of it (objectui#4482).
-
-The page built `publicShareBase` itself — read the injected ` `, take its
-pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third
-independent implementation of the mount resolution `resolveConsoleUrl` centralizes.
-objectui#4472 had just deleted the other two on that rule; this was the surviving
-sibling. Its output was correct, so nothing a user hits was broken and nothing a user
-hits changes: measured over the base-href matrix, the deleted builder and
-`resolveConsoleUrl('s')` return identical URLs for every shape the console is served in
-— `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable
-builds, nested mounts, and no ` ` at all.
-
-The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`,
-which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM
-it returns `undefined` rather than a URL built from an origin that does not exist, so
-`ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument —
-the mount is only ever carried by the injected ` `, and a resolver with no
-other input cannot be pinned by a test that steers something production never reads.
-
-`resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected
-` ` element for each deployment shape, plus a structural case asserting no other
-app-shell file reads the ` ` tag — so a fourth copy fails a test rather than
-waiting for mount semantics to change under it.
diff --git a/.changeset/ai-quota-ledger-vocabulary-3804.md b/.changeset/ai-quota-ledger-vocabulary-3804.md
deleted file mode 100644
index 61ec9e5047..0000000000
--- a/.changeset/ai-quota-ledger-vocabulary-3804.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
----
-
-Recognize the landed AI quota ledger vocabulary in the chat error path
-
-`parseAiQuotaError` now accepts the three SCREAMING_SNAKE ledger codes the cloud
-token guardrail emits (`AI_ALLOWANCE_EXHAUSTED`, `AI_DESIGN_QUOTA_EXHAUSTED`,
-`AI_DATA_CHAT_TRIAL_EXHAUSTED`) alongside the legacy lowercase trio, which stays
-readable for producers that have not converged yet. The companion fields
-(`messageEn` / `upgrade` / `topUp` / `resetsTonight`) are now read from the
-declared envelope's `error.details` as well as their legacy top-level position,
-with the declared position winning.
-
-A quota-exhausted user gets the upgrade / top-up CTA again instead of the
-generic "Response failed" banner. The per-turn message cap's generic
-`QUOTA_EXCEEDED` deliberately keeps its existing rate-limit path — it has no
-upgrade or top-up next step.
diff --git a/.changeset/ai-studio-accessor-5577.md b/.changeset/ai-studio-accessor-5577.md
deleted file mode 100644
index dca9479328..0000000000
--- a/.changeset/ai-studio-accessor-5577.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead
-of being spelled inline at two call sites (objectui#5577).
-
-`features.marketplace` already had a documented accessor whose docblock is where the
-fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating
-`/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*,
-plus the "never infer this from the shape of a failure" warning. `features.aiStudio`
-had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false`
-and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine
-had two spellings and neither reader could cite it.
-
-The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and
-against a runtime-config snapshot whose `features` is absent that read is a TypeError
-rather than a fail-open — the exact shape that crashed 29 tests across four suites in
-PR #5575 before it was corrected. Measured here: no live path can currently deliver
-such a snapshot to `ChatDock` (the module's singleton constructs `features` on every
-write and exports no setter, and no suite mounts the dock's default body under a
-partial stand-in), so this closes a reachable-by-construction crash rather than a live
-one — and it closes it at the source by leaving no inline read to get wrong.
-
-`isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`:
-neither is re-exported from `src/index.ts`, so the package's published `exports` surface
-is unchanged.
diff --git a/.changeset/alert-dialog-fixture-premise-pin-7693.md b/.changeset/alert-dialog-fixture-premise-pin-7693.md
deleted file mode 100644
index 82d6073136..0000000000
--- a/.changeset/alert-dialog-fixture-premise-pin-7693.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change in `@object-ui/types`: the pin that recorded objectui#7693's filed premise (the four alert-dialog schema-catalog fixtures authoring an `actions` array) is re-derived onto the read dialect now that the card has landed. No published behaviour changes — `overlay.ts` and `zod/overlay.zod.ts` are untouched, and the only file edited under `src/` is a test.
diff --git a/.changeset/alias-closure-relative-miss-5386.md b/.changeset/alias-closure-relative-miss-5386.md
deleted file mode 100644
index f67ab4d329..0000000000
--- a/.changeset/alias-closure-relative-miss-5386.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
----
-
-Tests only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` is touched; the only file changed is
-`examples/console-starter/test/vite-alias-closure.test.ts`, and that example is private.
-
-Makes the alias-closure walker record an unresolvable **relative** import instead of
-dropping it.
-
-`computeClosure()` walks two kinds of specifier. The bare-specifier branch pushed a miss
-onto `unresolvable`; the relative branch dropped one with no record at all. So
-`expect(closure.unresolvable).toEqual([])` was not a weak assertion for relative imports
-— it was a structurally empty one. It could not fail no matter how many relative
-specifiers the walk failed to follow, and the `filesWalked` floor was the only signal
-that anything had gone wrong.
-
-That is how two earlier conversions to explicit extensions (objectui#4538, objectui#5214)
-each truncated this walk while landing green: the floor had enough slack to absorb both,
-and only went red once app-shell — the largest package — converted and took the count
-under 500. The direct symptom was being swallowed one branch away the whole time.
-
-Measured on `main` at the time of this change, with the resolver ablated to its
-pre-objectui#5357 behaviour to reproduce that regression class: 275 relative specifiers
-dropped, `filesWalked` 1245 to 402, packages reached 29 to 22 — and `unresolvable` still
-reporting `[]`. With this change the same ablation fails the suite with all 275 named,
-each alongside the file that imports it.
-
-The miss is recorded only for specifiers that are *meant* to be modules — no extension,
-or one of the JS/TS emitted extensions. `ts.preProcessFile` also reports `./styles.css`,
-`./data.json` and `./logo.svg`, which `resolveModule` cannot resolve by design, so
-recording those identically would fail the suite for a reason that is not a defect.
-Assets and Vite resource specifiers (`?raw`, `?url`, `?inline`) are skipped into a
-separate `nonModuleSkipped` list — explicitly, and observably, rather than by accident.
-
-Four fixtures pin the class against the real walker so a future edit cannot silently
-re-blind the branch: `./Foo.js` resolving through to the `Foo.tsx` on disk, planted
-unresolvable modules of both spellings being named, assets staying out of `unresolvable`
-while still being accounted for, and the classifier's boundary cases.
diff --git a/.changeset/analytics-coded-400-rejected.md b/.changeset/analytics-coded-400-rejected.md
deleted file mode 100644
index 1b0272bfff..0000000000
--- a/.changeset/analytics-coded-400-rejected.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/data-objectstack': minor
----
-
-`classifyAnalyticsFailure` now reads a 400 as a refusal of the query body we
-sent regardless of which ADR-0112 `code` it carries, so `aggregate()` no
-longer answers a rejected filter with client-side numbers from a different
-door (objectui#7755).
-
-Before this fix, only 400 `VALIDATION_FAILED` (and a code-less 400) threw
-`AnalyticsQueryRejectedError`. Any OTHER coded 400 — `service-analytics` ships
-its own 400 `INVALID_FILTER` on a filter shape it refuses — matched none of
-`classifyAnalyticsFailure`'s branches and fell through to `unknown`, which
-`aggregate()`'s catch has no arm for, so it silently degraded to
-`aggregateViaFind`: a re-read through `find()`'s `$filter` query-string
-contract, which accepts array shapes the analytics request body does not. A
-filter the analytics route refused could still be answered — with a
-plausible, wrong number, and no sign the request had a defect.
-
-The fix is a floor UNDER the existing code branches, not a replacement for
-them: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` still win `not-installed`,
-`VALIDATION_FAILED` / `UNAUTHENTICATED` / `CUBE_NOT_FOUND` still win their own
-outcomes first (objectui#5721). Only a 400 that none of those four already
-claimed now falls to the new floor instead of past it. An unmatched NON-400
-coded error (e.g. a coded 5xx) is unaffected and keeps degrading exactly as
-before — this fix is scoped to the 400 case only.
diff --git a/.changeset/analytics-error-branch-mapping-5663.md b/.changeset/analytics-error-branch-mapping-5663.md
deleted file mode 100644
index e0809a7683..0000000000
--- a/.changeset/analytics-error-branch-mapping-5663.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/data-objectstack': patch
----
-
-`ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's
-ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an
-unauthenticated session stop being reported as a missing analytics capability
-(objectui#5663).
-
-Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`:
-the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and
-the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The
-mapping tested `res.status === 501 || res.status === 404` and called all of it
-"the analytics capability is not installed", so every unknown dataset produced a
-banner telling the operator to install `@objectstack/service-analytics` and mount
-`AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on
-four HotCRM Executive Overview widgets while the analytics service was installed
-and answering — the real condition was an installed `app.objectstack.hotcrm` at
-1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of
-the system from the remedy the banner named.
-
-Three conditions now get three answers, each keyed on the code the framework
-declares for it:
-
-- `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and
- `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing
- `AnalyticsNotInstalledError` and its copy — one remedy, one message.
-- `NOT_FOUND` (404, unknown `datasetName`) throws the new
- `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the
- dataset and pointing at the installed app's version rather than at the server.
-- `UNAUTHENTICATED` (401, `enforceAuth`) throws the new
- `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the
- request was refused before it ran and therefore says nothing about the
- capability.
-
-The banner also used to print the server's own message in a parenthetical while
-contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not
-found.` under a headline claiming a missing capability. That is now structurally
-impossible rather than merely fixed: the headline is a pure function of `code` and
-the parenthetical is a verbatim quote of `message`, both read off the same
-response, and a test walks every branch asserting each message carries its own
-headline and none of the others'.
-
-Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its
-constructor signature (it gains an optional third `serverCode` argument and a
-`serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including
-the metadata-admin dataset preview — are unaffected. A 404 carrying a code this
-client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now
-keeps its server detail instead of being relabelled as a missing capability; a 404
-or 501 carrying no code at all is still read as the capability being absent, since
-the route's own `NOT_FOUND` always ships a code.
diff --git a/.changeset/analytics-failure-code-first.md b/.changeset/analytics-failure-code-first.md
deleted file mode 100644
index 8b9043df2c..0000000000
--- a/.changeset/analytics-failure-code-first.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-'@object-ui/data-objectstack': minor
----
-
-Classify `/analytics/query` failures by their ADR-0112 `code` rather than their HTTP status, so a chart is no longer answered from a different code path behind the wrong explanation.
-
-`classifyAnalyticsFailure` tested `status === 404 || status === 501` before the code operands on the same line, so the status short-circuited every one of them: any 404 on this face was classified "the analytics capability is not installed" whatever code it carried, and `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the conditions they name. Three unrelated conditions answer 404 on this url, so the status cannot tell them apart — the `code` is the contract.
-
-Two conditions change behaviour:
-
-- **404 `CUBE_NOT_FOUND`** (a misspelled or unregistered cube — an authoring mistake) now **throws** the server's own error verbatim, keeping `code` and the producer's repair instructions. It previously warned "install `@objectstack/service-analytics`" and silently degraded to `find()` + client-side aggregation — which cannot answer it anyway, because the fallback re-reads the same name through `/data`, where an unregistered object is a 404 `OBJECT_NOT_FOUND`.
-- **401 `UNAUTHENTICATED`** (an anonymous or lapsed session) now **throws** `AnalyticsUnauthenticatedError` instead of degrading silently behind a `find()` that is about to be refused the same way.
-
-Unchanged: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` and code-less 404/501 answers still degrade loudly to the client-side fallback, 400 `VALIDATION_FAILED` still throws, and 5xx / network failures still degrade silently.
diff --git a/.changeset/anon-seed-scope-pin-5828.md b/.changeset/anon-seed-scope-pin-5828.md
deleted file mode 100644
index 12f0afa2b7..0000000000
--- a/.changeset/anon-seed-scope-pin-5828.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Test-only: lands the objectui#5746 enumeration harness as a regression pin under
-objectui#5828. It mounts the real console boot path (real `AuthProvider`, real
-`ConnectedShell` session gate, real `MetadataProvider`; only the auth and
-metadata servers are doubled) and asserts what is true today — including that
-the guest and `previewMode` boots write `objectui:metadata:app:@none:@anon`,
-which is the open observation objectui#5828 carries, not a change made here. The
-pin that can fail is S6: it goes red if objectui#5198's principal-scoped cache
-key stops discriminating principals. No published behaviour changes.
diff --git a/.changeset/anonymous-expression-user-id-6534.md b/.changeset/anonymous-expression-user-id-6534.md
deleted file mode 100644
index c3c8459aae..0000000000
--- a/.changeset/anonymous-expression-user-id-6534.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The predicate identity bound for a signed-out visitor now carries `id: null`, so a
-`ctx.user.id` visibility gate BITES instead of failing open (objectui#6534).
-
-`buildExpressionUser` has two branches, and only the signed-in one carried `id`.
-So `'id' in buildExpressionUser(null)` was `false`, and an absent key is not
-`false`: a CEL predicate naming `ctx.user.id` / `current_user.id` / `os.user.id`
-hit an unbound key for a signed-out visitor and FAULTED. A faulting visibility
-predicate fails OPEN (`evaluateVisibility`), so the gated field or action rendered
-for exactly the principal it was written to exclude, with nothing on screen to say
-the gate had not bitten — silently, for every signed-out visitor.
-
-Because the defect was in the shared normaliser rather than at a mount site, it
-reached EVERY mount site, including `AppContent` and the console's
-`InternalFormRoute`, both of which have always called the normaliser correctly.
-This is the same fault-open mechanism objectui#6515 fixed one level up, where
-`RecordFormPage` hand-rolled a descriptor missing `id` and `isPlatformAdmin`.
-
-`null` rather than `undefined`, and rather than leaving the key absent, is settled
-by precedent on this exact object rather than chosen here. objectui#5424 removed
-`roles` from it because a present-and-always-`undefined` key "is the shape that
-teaches the wrong thing" — the context answers rather than being plainly absent,
-and the answer is silently wrong; `undefined` here would reproduce that defect one
-key over. Leaving it absent IS the defect. `null` is a VALUE a CEL author can
-compare against, so `ctx.user.id == '…'` resolves to a clean FALSE. Measured, not
-assumed: at a real mount site with `authState.user = null` and a field gated on
-`ctx.user.id == 'u_admin'`, the field is now filtered OUT of the schema handed to
-`ObjectForm`, where before it was present.
-
-This also closes the last asymmetry between the two branches. Both now advertise
-the same six keys, which is the symmetry objectui#5424 was closing when it removed
-`roles` — and the shape pin now asserts the key sets are equal, so a future edit
-that adds a key to one branch and forgets the other fails whichever branch it
-forgets.
-
-NOT CHANGED, deliberately: fail-open on a predicate that DOES fault. That is
-shipped permission-boundary policy (objectui#6443 / #6487 / #6445) and remains
-exactly as it was — an unevaluable `visible` still renders. This change removes a
-REASON to fault; it does not touch what happens once a predicate has. No accept set
-is widened, no gate is relaxed and no fallback is added: the only behavioural
-movement is that an id-gated surface which used to render for anonymous visitors
-now hides from them.
diff --git a/.changeset/app-hidden-catalogue-flag-7542.md b/.changeset/app-hidden-catalogue-flag-7542.md
deleted file mode 100644
index 547d0f353f..0000000000
--- a/.changeset/app-hidden-catalogue-flag-7542.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**`AppComponentSchema.hidden` is now declared `boolean` — the spec's app-catalogue flag — instead of the `boolean | ExpressionWire` hide predicate it inherited from `BaseSchema`.** On the `app` node two keys collide in name and differ in meaning (objectui#7542): the zod mirror takes `@objectstack/spec/ui` `AppSchema.hidden` ("Hide from the App Switcher") by reference through `SpecAppFields`, where the spec's `z.boolean().optional()` lands after the base's key and overrides it, so the validator has always refused a predicate string or a CEL envelope object at path `hidden` on an `app` document — through `AppComponentSchema.safeParse` and through `safeValidateSchema` alike — while the published TypeScript interface, restating nothing, invited exactly that spelling. This is direction 1 of the card: the declaration is pulled back to what the validator enforces. Direction 2 — giving the catalogue flag its own name upstream in `@objectstack/spec` so the app node can inherit the renderer's predicate again — stays open as the alternative and is a protocol change, not taken here.
-
-Breaking on the TypeScript face only: a predicate string or envelope on an `app` node no longer type-checks, and an `app` node cannot use the predicate spelling every other node accepts. Authored JSON metadata is unaffected — the validator never admitted it — and the in-repo reader (`filterActiveApps` in `@object-ui/app-shell`) already treats the key as the boolean `hidden !== true`, never evaluating it. The renderer's `hidden` predicate on every other node is unchanged, `BaseSchema.hidden` is untouched, and the `KnownDrift` row objectui#7455 seeded for this pair is removed because the drift it recorded is gone.
diff --git a/.changeset/app-shell-nodenext-pin-5440.md b/.changeset/app-shell-nodenext-pin-5440.md
deleted file mode 100644
index f6674a1eb9..0000000000
--- a/.changeset/app-shell-nodenext-pin-5440.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Pin `module` / `moduleResolution` to `nodenext` in `@object-ui/app-shell`'s build config, matching the pins `@object-ui/react`, `@object-ui/fields` and five other packages already carry. This package builds with a bare `tsc`, which never rewrites import specifiers, so what the source writes is exactly what `dist` ships; under `nodenext` a missing relative extension is a compile error, so the extensionless-specifier defect that made published entries unloadable under plain Node cannot come back silently here.
-
-The lazy `@monaco-editor/react` imports in the metadata designer's source editors now read the package's named `Editor` export instead of its default. `@monaco-editor/react@4.7.0` is CommonJS and ships no `exports` map, so under `nodenext` the default resolves to the module namespace rather than to the component. The two names are one declaration in that package's own typings and the same object at runtime in both its CommonJS and ESM builds, so the editor that renders is unchanged.
diff --git a/.changeset/app-shell-page-header-subtitle-4761.md b/.changeset/app-shell-page-header-subtitle-4761.md
deleted file mode 100644
index 01333ab79c..0000000000
--- a/.changeset/app-shell-page-header-subtitle-4761.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The console's `` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761).
-
-This repository has two components named `PageHeader`. `@object-ui/layout`'s is
-the renderer for the authored `page:header` / `page-header` node and converged
-on `subtitle` in objectui#3789, because `subtitle` is the key
-`@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s —
-the console's own title row, drawn by `ObjectView` and `ObjectDataPage` —
-spelled the very same concept `description` and had no `subtitle` at all. Both
-rendered correctly; the defect was one concept carrying two key names one
-package apart, the objectstack#4115 shape moved up a layer. An author reading
-one component to learn the other was being taught a key the contract does not
-have.
-
-**Not a breaking change, measured rather than assumed.** The convergence is a
-plain rename with no alias, because this component is not on the published
-surface:
-
-| gauge | result |
-|---|---|
-| exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) |
-| `exports` map | declares exactly `.` and `./styles.css` |
-| Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves |
-| in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) |
-| emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) |
-
-No supported specifier reaches the prop, so there was nothing to keep
-compatible, and a renderer-side `description` alias would have been exactly the
-second dialect AGENTS.md #0.1 forbids — the layout side had just finished
-retiring one. Out-of-repo consumers cannot be enumerated from this repository;
-what can be, and is, is the set of import paths through which one could have
-reached this component, which is empty.
-
-Rendered output is unchanged: same element, same classes, same position. The
-patch tier is a declaration that the tarball moved, not a claim that a consumer
-must act.
-
-`packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the
-pin the card asked for. It asserts the subtitle on the DOM a reader gets (a
-``, in the title block, after the `
`), that `description` now draws
-nothing and is rejected by the compiler, and — the assertion that actually goes
-red if either side drifts again — that both packages' `PageHeaderComponentProps`
-declare `subtitle`.
diff --git a/.changeset/app-shell-provision-envelope-shape-6707.md b/.changeset/app-shell-provision-envelope-shape-6707.md
deleted file mode 100644
index fdf65fd3cc..0000000000
--- a/.changeset/app-shell-provision-envelope-shape-6707.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-`provisionProductionEnvironment` now REFUSES a success payload whose `data`
-carries no `environment` row, instead of resolving best-effort (objectui#6707).
-
-objectui#6629 fixed this consumer to read the created environment from the
-nested `environment` key, but deliberately left the envelope check alone: it
-catches a **missing** `data` and says nothing about `data`'s **shape**. So a
-producer that regressed to a flat payload would once again resolve successfully
-with `id` and `hostname` both `undefined` — the same silent outcome #6629 had
-just fixed, reachable again by a producer change alone.
-
-A flat payload is a producer contract violation, not a second dialect to be
-tolerated, and tolerating it is how the original defect stayed invisible. The
-call refuses it now, which routes a producer regression to this call's already
-documented failure path rather than a successful-looking no-op: the sole caller
-(`CreateWorkspaceDialog`) already wraps the call in `try`/`catch`, logs a
-warning, and lets the onboarding gate re-provision lazily on first navigation.
-Workspace creation itself is unaffected — the caller does not re-throw, and it
-never read this call's return value.
-
-The refusal carries its own diagnostic, distinct from the missing-envelope one,
-so a logged warning still distinguishes "the control plane did not wrap the
-payload" from "it did not put the row where it says it does".
-
-The wire shape this is written against is confirmed producer-side rather than
-inferred from this consumer — the distinction is load-bearing, because before
-#6629 the only in-repo artifact pinning this payload was a hand-written mock
-pinning the bug shape. Both sources are recorded on the function's docblock.
diff --git a/.changeset/approvals-inbox-cell-remount-5348.md b/.changeset/approvals-inbox-cell-remount-5348.md
deleted file mode 100644
index 4a11753a5b..0000000000
--- a/.changeset/approvals-inbox-cell-remount-5348.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The Approvals Inbox stops remounting every row on every render.
-
-`ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions`
-inside its own component body. React identifies a component by the identity of
-its function, so each render produced three brand-new component *types* and React
-unmounted and remounted every row's subtree instead of updating it — and the page
-holds its clock in state and ticks it every 60s, so this fired on a timer whether
-or not anyone was touching the page (objectui#5348).
-
-Two consequences were reproduced against `origin/main` before the fix, in
-`apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`:
-
-- **Transient subtree state is discarded.** Focus placed on a row's Approve
- button moved to `` on the next clock tick.
-- **Input is silently swallowed.** A pointer sequence that spans a re-render —
- press, tick, release — left the confirmation dialog unopened: the captured node
- had been replaced, so React's delegated listener never saw the click. This is
- the failure objectui#5211 hit and worked around at its call site
- (`Unable to find role="alertdialog"`).
-
-The three cells are now at module scope beside `StatusBadge`, which was moved
-there for the same reason and already carries the explanation. Everything they
-closed over is passed in: `RequestCell` and `InlineActions` take the page's
-scoped translator, and `RecordCell` takes `href: string | null` — one prop rather
-than two, so the objectui#5211 readable/unreadable decision and the URL cannot be
-handed in disagreeing with each other.
-
-The verification asserts the consequence, not the placement. A test that checks
-the three functions now sit at module scope stays green for a refactor that moves
-them and introduces a fourth inline component beside them; these cases compare
-DOM-node identity for all three cells across a clock tick, which no remount can
-pass, and re-drive the swallowed click.
-
-That guard is load-bearing because lint cannot supply one here.
-`react-hooks/static-components` exists for exactly this class and is `error` in
-this repo via the plugin's recommended set, yet it reports nothing on this page:
-measured on `origin/main`, an arrow-form inner component injected into
-`ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same
-shape in a ten-line file produced two. The rule's analysis bails out on this
-component, which is how three of them shipped.
diff --git a/.changeset/approvals-queue-hidden-amount-6020.md b/.changeset/approvals-queue-hidden-amount-6020.md
deleted file mode 100644
index 3c0b81e920..0000000000
--- a/.changeset/approvals-queue-hidden-amount-6020.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-"@object-ui/console": patch
----
-
-Approvals inbox: the queue rows and the amount sort now honour each request
-object's own `hidden: true` field declaration (objectui#6020).
-
-The `hidden: true` trim added for the drawer summary card reached only the
-drawer — the desktop queue row, the mobile card and the amount comparator
-still read the field, so an amount an app author declared hidden rendered
-inline in the queue and ordered the list, which leaked its relative magnitude
-even to a viewer who never saw the figure.
-
-The queue spans many objects, so the trim is a per-object lookup and every row
-is answered about its own object; a row left with no renderable amount now
-sorts with the other amount-less rows. `hidden` stays a UI contract
-(objectstack#10749) and the filter still fails open: an unanswered or failed
-metadata read renders today's figure.
diff --git a/.changeset/approvals-raw-payload-gate-5553.md b/.changeset/approvals-raw-payload-gate-5553.md
deleted file mode 100644
index 3cd1ed2642..0000000000
--- a/.changeset/approvals-raw-payload-gate-5553.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The Approvals Inbox no longer shows a business approver the submitted record's raw
-row JSON.
-
-The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no
-principal check of any kind — so every approver could expand (and one-click copy) the
-complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`,
-`organization_id`, bare lookup ids, and **the fields the object's metadata declares
-`hidden: true`**. Reported from a live EHR deployment on 17.1.0
-(objectstack-ai/objectstack#10734), where that declaration is a patient-data control.
-The app author had no legitimate lever to remove the panel — field `hidden`, view
-columns, app navigation, permission sets and env vars are all ineffective against it —
-so the remedies available in the field were patching the shipped bundle or injecting
-CSS.
-
-The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's
-`studioEntry` module: `studio.access` is a declared platform-scope capability that a
-tenant org owner does not hold by design, and it already reaches the browser in
-`systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served,
-computed or made authorable — no new config key, no new i18n copy, and the panel is
-byte-for-byte unchanged for the platform operator it was written for. A business
-approver keeps the structured record summary, the approval chain, the activity feed
-and the decision actions; only the raw snapshot is gone.
-
-The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from
-`usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a
-holder's button while the server still refuses the write is the worse outcome for an
-action. This panel has the opposite stake, since the measured defect is a non-holder
-seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend
-predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200`
-with no `systemPermissions` at all, and a reported empty array. A deployment whose
-permission layer just failed must not be the one that leaks the snapshot.
-
-`ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the
-acceptance condition is that something does *not* render — which an empty render
-reproduces perfectly — every denial case also asserts the drawer it denies inside, and
-the `studio.access` case drives the same fixture through the same helper and finds the
-panel. `created_by` and `organization_id` are the witnesses: both are in the page's
-`PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can
-reach the DOM only through the raw panel. Ablating the gate (restoring the bare
-`payload != null` condition) turns the three denial cases red on exactly that
-assertion and leaves the holder case green.
-
-Out of scope, tracked separately: trimming the summary by object metadata, and the
-server-side residual that sends the unfiltered snapshot to the client at all.
diff --git a/.changeset/approvals-step-progress-vertical-5554.md b/.changeset/approvals-step-progress-vertical-5554.md
deleted file mode 100644
index 8bccea37ef..0000000000
--- a/.changeset/approvals-step-progress-vertical-5554.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'@object-ui/console': patch
-'@object-ui/app-shell': patch
----
-
-The approval step progress bar is a vertical stepper, so long flows stop
-clipping their tail steps.
-
-Both occurrences were a single non-wrapping flex row whose steps were each
-`shrink-0`. A flex row's min-content width is the sum of its non-shrinkable
-items, so the bar's intrinsic width grew without bound with step count and
-label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK
-step names measured **1070px inside a 527px container** (objectui#5554).
-
-The two hosts failed differently, and neither failure was recoverable by the
-reader:
-
-- **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not
- scrollable, so the nearest scroller was the drawer *panel*. Reaching steps
- 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the
- record card, the activity timeline and the action buttons off-screen and left
- a near-blank panel.
-- **`RecordApprovalsPanel`** (the record page's approvals panel) — this one
- carried `overflow-x-auto`, so it scrolled itself rather than its container.
- Better, but the tail steps still sat behind a scroll gesture with no visible
- affordance.
-
-In both, readers took the clipped bar for the end of the data; the reporting
-customer acceptance tester said so verbatim. Widening the window does not help:
-the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000.
-
-Both now render as a column: one row per step, a badge-and-rail gutter, and a
-label that may wrap. Width is capped by the container at every step count and
-every label length, which also suits both hosts' tall-and-narrow aspect. The
-rail segment below each step keeps the tint rule the horizontal connector used
-— it is coloured by the step it leads *into*.
-
-**Always vertical, with no step-count or measured-width threshold**, because
-the overflow is driven by intrinsic content width (labels x count), not by
-count alone: three 16-character CJK labels already crowd a 527px drawer, so any
-count threshold picks a cutoff that is wrong for some real flow, and a measured
-one reintroduces a viewport-dependent branch. The card's requirement is a fix
-that cannot break at an untested viewport or flow length, and a layout with no
-breakpoint and no measurement is the form that satisfies it. Horizontal-with-
-scroll was ruled out for both occurrences: it leaves steps behind a gesture.
-
-Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and
-`RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is
-green against the broken code too — every step was always in the DOM, and the
-clipping was layout — so the suites assert the property the defect names
-instead: no row is `shrink-0`, every label is `min-w-0` and none is
-`whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no
-axis, overflow or width-pinning class carries a breakpoint prefix (so there is
-no viewport with untested behaviour). The reported failing regime is exercised
-directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins
-that the layout classes are byte-identical across all four, so no count
-threshold can put some other flow length back on the old path.
-
-The two steppers are kept identical by hand rather than extracted to a shared
-component: they live in different packages, and deduplicating them is a
-refactor with its own surface. Filed separately.
diff --git a/.changeset/approver-display-name-5414.md b/.changeset/approver-display-name-5414.md
deleted file mode 100644
index f441983851..0000000000
--- a/.changeset/approver-display-name-5414.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-The approval panel identifies the pending approver by name, not by a truncated raw id.
-
-A record waiting on a position rendered its approver as `positi…ager` — the
-engine reference `position:sales_manager`, 22 characters, past the identity
-formatter's 14-character truncation arm and middle-truncated to fit its chip. The
-step names beside it were human prose; the one line answering *who is holding
-this record* was an internal identifier, and not even a complete one. The same
-reference reached the admin-override confirm dialog un-truncated, so a paragraph
-of plain governance prose ended `— position:sales_manager` (objectui#5414).
-
-Both surfaces now resolve the reference before rendering, in three tiers, most
-authoritative first. The server's own `pending_approver_names` wins whenever it
-answers, and a backend that resolves its own slate costs the record page no extra
-request. Otherwise the console reads the directory row the spec's approver
-binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and,
-for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no
-adapter and no row, the machine name still prettifies into prose rather than
-truncating. The raw reference stays on hover, which is where an internal
-identifier belongs.
-
-An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)`
-is actionable where `positi…ager` is not, and it is the motivating rescue case
-for the admin-override path. Staffing is deliberately tri-state — a
-`sys_user_position` read the viewer is not permitted to make leaves the seat's
-staffing UNKNOWN and says nothing, because "I could not look" is a different
-claim from "nobody holds it" and only one of them is safe to print on a
-governance surface.
-
-Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed`
-and `approvalsInbox.approverNameSeparator`. The separator is a translated
-punctuation key rather than `Intl.ListFormat`, which was measured on this tree
-joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with
-no separator, reading as one person's name.
-
-The directory-backed kinds and their value columns are read from
-`@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new
-approver type is covered the day the spec publishes it. Id-valued kinds
-(`user` / `team` / `department`) keep the existing middle-truncation: a row id
-has no prose to recover, and that arm is objectui#3461's answer, not this card's
-defect.
diff --git a/.changeset/approver-identities-read-positions-5424.md b/.changeset/approver-identities-read-positions-5424.md
deleted file mode 100644
index 2137b2b8c2..0000000000
--- a/.changeset/approver-identities-read-positions-5424.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424)
-
-Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no
-deprecation window, and the protocol-17 session face emits no `roles` key at
-all. Three client sites still read it:
-
-- **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's
- Approvals tab and Home's To-do card. It read nothing else, so it sent **no
- `role:` identity at all**: an approval addressed to a position rather than to
- a person matched nothing and vanished from all three surfaces, silently.
-- **`approvalsApi.buildApproverIdentities`** — "My Pending" and the
- Approve/Reject enablement. It also splits the scalar `user.role`, so it
- degraded rather than dying: it still yielded `role:user` while dropping every
- business position name (`manager`, `finance_approver`, …).
-- **`AppContent`'s expression user** — forwarded a `roles` key that was always
- `undefined` into every CEL predicate context. Removed; `positions` and
- `isPlatformAdmin` were already forwarded correctly beside it.
-
-The retired spelling is **not** kept as a fallback — pairing the two is what
-ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the
-declaration.
-
-`AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately
-untouched: it is a semantics decision, not a rename, and is deferred to a
-maintainer ruling.
diff --git a/.changeset/appsidebar-deprecated-5720.md b/.changeset/appsidebar-deprecated-5720.md
deleted file mode 100644
index 404094e329..0000000000
--- a/.changeset/appsidebar-deprecated-5720.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`AppSidebar` is now `@deprecated` — use `UnifiedSidebar` instead.
-
-A census (objectui#5720) found `AppSidebar` has no in-repo mount point
-(`ConsoleLayout` renders `UnifiedSidebar`, not this component) and no
-downstream consumer visible anywhere across this org's GitHub-visible
-repositories. It stays exported — from the package barrel and the published
-`dist/index.d.ts` — because `@object-ui/app-shell` is a public npm package
-(`publishConfig.access: "public"`) and an external consumer outside this org
-is structurally invisible to that census; that is why it is deprecated
-rather than deleted outright. No behavior change in this release — the
-component still renders exactly as before. Its admin nav cluster is a
-near-duplicate of `UnifiedSidebar`'s and has already drifted from it (it
-gates only `sys-marketplace` on the workspace-admin flag, where
-`UnifiedSidebar` gates the whole cluster); that divergence is not being
-reconciled, since the component is scheduled for removal rather than kept
-in parity — see objectui#5817 for the removal plan.
-
-Migration: replace any `AppSidebar` usage with `UnifiedSidebar` from the same
-package.
diff --git a/.changeset/audit-field-def-reference-narrow.md b/.changeset/audit-field-def-reference-narrow.md
deleted file mode 100644
index 6a32b5c227..0000000000
--- a/.changeset/audit-field-def-reference-narrow.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Narrow `AuditFieldDef`'s relationship-target read in `app-shell`'s record History
-diff helpers to the spec spelling `reference`, carried as a bare `string`.
-
-No release: nothing published changes. `AuditFieldDef` and `auditHistoryDisplay`
-are package-internal — neither is re-exported from `src/index.ts`, and the
-package's `exports` map exposes only `.` and `./styles.css`, so no consumer can
-reach either. Runtime behaviour is unchanged on every document that can reach the
-helper: `normalizeSchemaReferenceKeys` stamps both snake_case spellings at the
-metadata ingestion choke point, and the removed `string[]` carrier was already
-refused at runtime by the `typeof` narrowing that stayed.
diff --git a/.changeset/authguard-positions-5424.md b/.changeset/authguard-positions-5424.md
deleted file mode 100644
index 438aa5d16a..0000000000
--- a/.changeset/authguard-positions-5424.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@object-ui/auth": minor
----
-
-`AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424.
diff --git a/.changeset/block-config-schema-parity-8216.md b/.changeset/block-config-schema-parity-8216.md
deleted file mode 100644
index a1fb9131d0..0000000000
--- a/.changeset/block-config-schema-parity-8216.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only: a parity gate pinning every `BLOCK_CONFIG` designer field name to the schema its block is judged by (`@objectstack/spec/ui`'s `ComponentPropsMap`, or the `@object-ui/types/zod` node arm). No published behaviour changes — no runtime source was touched.
diff --git a/.changeset/block-schema-docs-truth-4895.md b/.changeset/block-schema-docs-truth-4895.md
deleted file mode 100644
index 3019d9ed00..0000000000
--- a/.changeset/block-schema-docs-truth-4895.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
----
-
-Docs and a gate ledger only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared. No package `src/` is touched: the two files
-changed are `content/docs/blocks/block-schema.mdx` and
-`scripts/check-doc-component-types.mjs`.
-
-Corrects `content/docs/blocks/block-schema.mdx` to the vocabulary the repository actually
-declares, per the maintainer ruling recorded on objectui#4895 (Option B: docs-truth fix,
-the block family re-scoped as type-level).
-
-The page taught a `{ type: 'slot', name: 'content' }` node inside
-`BlockSchema.template`. `template` is typed `SchemaNode | SchemaNode[]`, so that snippet
-sat on the render path — and `slot` is registered nowhere. Measured against the
-`check-doc-component-types.mjs` derivation of the registered universe (659 keys), the only
-keys matching `/block|slot/` are `blockquote` and `ui:blockquote`. A reader who copied the
-snippet got the renderer's `OBJUI-001` "Unknown component type" panel. That node is
-deleted, and the page now teaches `slotContent`, the key `BlockSchema` and
-`BlockInstanceSchema` actually declare.
-
-**`slotContent` is documented as declared-but-not-yet-consumed, not as the working path**,
-because that is what it measures as. Outside `packages/types` — the interface, its Zod
-mirror, and that package's own parse test — nothing in the repository reads `slotContent`,
-`slots` or `template`. Rewriting one phantom into a second phantom is the failure this card
-exists to close, so the page states the status plainly instead of implying a runtime that
-does not exist. For the same reason the component-schema framing is dropped from
-`block-library` / `block-editor` / `block-instance`: none of them appears in `AnySchema`,
-the runtime node union in `packages/types/src/index.ts`.
-
-Two further measurements are written into the page because they are what a confused reader
-needs. The four `` demos it embeds are ordinary registered component trees
-(`card`, `flex`, `stack`, `text`, `icon`, `button`, `badge`) — none carries `type: 'block'`,
-`slots` or `slotContent`, so nothing on the page was ever exercising the block vocabulary.
-And the slot system that *is* wired end to end is a different family entirely: slotted
-record pages (`kind: "slotted"`, `page.slots`), consumed by `usePageAssignment`,
-`PageBlockCanvas` and `PageBlockInspector`. The page now links there rather than leaving the
-two `slots` spellings to be conflated.
-
-The `slot` entry in `DOC_TYPE_EXEMPTIONS` pointed at this card and is dropped, as the ruling
-requires. With the phantom node gone the entry would itself fail as `stale-exemption`, so it
-is deleted rather than re-pointed; the three terse family reasons are re-pointed at the
-ruling's framing in the same pass.
-
-Option A (build renderers for the family) is rejected on zero pull and Option C (retire the
-family) is deferred; neither is implemented here.
diff --git a/.changeset/built-moment-transition-5799.md b/.changeset/built-moment-transition-5799.md
deleted file mode 100644
index b429463265..0000000000
--- a/.changeset/built-moment-transition-5799.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio//interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app::build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio.
diff --git a/.changeset/built-transition-published-5799.md b/.changeset/built-transition-published-5799.md
deleted file mode 100644
index 69bd9c0cfa..0000000000
--- a/.changeset/built-transition-published-5799.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
-'@object-ui/app-shell': patch
----
-
-The built-moment transition (#5799) now fires on auto-publish environments too: `detectBuiltAppPackage` reads the raw build envelope (`status:'drafted'` OR `'published'`, packageId + an `app` item), because an auto-publish posture rewrites apply_blueprint's envelope to `published` and the drafted-only `draftReview` lift never fired there — measured live on staging, where reopening a built conversation stayed on the full page.
diff --git a/.changeset/button-container-exception-reason-6804.md b/.changeset/button-container-exception-reason-6804.md
deleted file mode 100644
index 5dda128717..0000000000
--- a/.changeset/button-container-exception-reason-6804.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
----
-
-Makes the `button` entry in `scripts/container-declaration-baseline.json` a **permanent,
-reasoned exception** instead of a provisional one, executing objectui#6804's maintainer
-ruling (2026-08-30). objectui#6779 had excluded `button` from the ratchet-to-zero list
-"pending a separate card"; that card has now been ruled, and the exclusion stands: `button`
-does not declare `isContainer`.
-
-The entry's `reason` now carries the ruling's ground rather than a forward reference —
-`isContainer` means layout containment, not "this tag renders children", and `button` reads
-`schema.children` only as a fallback for `schema.label`, so declaring it would make one
-predicate mean two things and would delete the `Button` identifier from the JSX scope of
-every `kind:'react'` page, against zero measured pull the other way. Its owning `issue`
-moves from `objectui#6779` (which deferred the question) to `objectui#6804` (which answered
-it), with the provenance kept in the reason.
-
-This matters because an exception with no recorded ground is indistinguishable from a
-missed one, and that indistinguishability is the mechanism behind this defect class's three
-independent rediscoveries (objectui#3900 / objectui#6740 / objectui#6764).
-
-Also records a measurement the note previously implied away: `button` is the only public
-tag among the 45 violations listed, but not the only public tag in the containment story —
-ADR-0080's `PUBLIC_BLOCKS` carries `badge` and `alert` as bare keys too, so of the 14 tags
-the ruling covers, three are published contract. The 11 bare `sidebar-*` keys are not (the
-public sidebar is the namespaced `page:sidebar`, which already declares `isContainer`).
-
-Ledger and test prose only. No published behaviour changes, no registration's metadata is
-altered, and every assertion that keeps the exception honest is unchanged.
diff --git a/.changeset/calendar-explain-network-escape-5438.md b/.changeset/calendar-explain-network-escape-5438.md
deleted file mode 100644
index 18359f993f..0000000000
--- a/.changeset/calendar-explain-network-escape-5438.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
----
-
-Tests only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` is touched; the only file changed is
-`packages/plugin-calendar/src/object-calendar-renderer.propsContract.test.tsx`.
-
-Stops `object-calendar-renderer.propsContract.test.tsx` from making four real outbound
-connections to `127.0.0.1:3000` while it stays green.
-
-`happy-dom`'s default document origin is `http://localhost:3000/`, so a relative-URL
-`fetch` that nothing intercepts resolves against that origin and leaves the process for
-real. The escaping request is `@object-ui/plugin-detail`'s record-level explain probe
-(`useRecordEditable`, `POST /api/v1/security/explain`): the file's one DEFAULT-navigation
-`onEventClick` case opens `ObjectCalendar`'s overlay drawer, which renders
-`RecordDetailDrawer` → `DetailView`, which gates its Edit/Delete CTAs on a per-record
-write verdict. With no `apiFetch` on the `SchemaRendererProvider` this file wraps every
-case in, the hook's `apiFetch ?? fetch` fallback reaches the bare global `fetch` — and the
-hook fails open on the resulting connection failure, so the escape was invisible to every
-existing assertion.
-
-Same defect class as objectui#3339 (`plugin-detail`, closed by PR #4105), objectui#5225
-(`plugin-report`) and objectui#5280 (`plugin-dashboard` `DatasetWidget`) — a new consumer
-of an already-diagnosed hook, not a new root cause. Fixed the way #4105 settled it:
-`installExplainDouble()` answers the probe from a recording double instead of the
-network, installed for every case in the file (not only the one that reaches it, since
-the probe is `DetailView`'s own wiring and invisible from this file's schema authoring).
-`visible: true` reproduces the pre-fix fail-open behaviour exactly, so no existing
-assertion changes meaning.
-
-A new pinned test — the file's counter-probe — opens the DEFAULT-navigation drawer and
-asserts the double is actually reached, twice (`update` and `delete`), with the expected
-URL and request body. Confirmed load-bearing by reverse verification: with the double
-temporarily disabled, the same case brings back all four `ECONNREFUSED` and fails that
-new assertion (`explainCalls` stays empty instead of reaching length 2).
diff --git a/.changeset/calendar-readme-schema-keys-5045.md b/.changeset/calendar-readme-schema-keys-5045.md
deleted file mode 100644
index eaf0ed0831..0000000000
--- a/.changeset/calendar-readme-schema-keys-5045.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
----
-
-`README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema`
-that does not exist. Measured against the interface itself
-(`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's
-only required key besides `type` — was published as `events?`, so a reader
-following the README omits it and TypeScript rejects the node; `defaultDate` was
-`string` where the schema says `string | Date`; and `onDateClick` was listed as a
-schema key when it is a `CalendarViewProps` **component** prop, sending readers
-to a different package's surface for a key `calendar-view` does not have (the
-schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys
-with nothing saying it was a summary (objectui#5045).
-
-The block now carries the requiredness the schema declares, names itself a
-partial summary of `CalendarViewSchema`, and adds the author-facing
-`defaultView` / `view` / `views` / `editable` / `date`. It also states plainly
-what the registered `calendar-view` renderer actually reads — it builds events
-from the node's `data` array and drops an authored `events` key (objectui#4433) —
-so the corrected requiredness does not itself become a new wrong instruction.
-
-This is a documentation fix to a file `plugin-calendar` publishes to npm, which
-is why it carries a version: the npm landing page only picks up the correction
-on a release. No behaviour, export, type, or `dist` byte changes. The pin test
-added alongside it publishes nothing.
diff --git a/.changeset/calendar-unscheduled-area.md b/.changeset/calendar-unscheduled-area.md
deleted file mode 100644
index 71ab42df47..0000000000
--- a/.changeset/calendar-unscheduled-area.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
-'@object-ui/i18n': patch
----
-
-fix(plugin-calendar): a record with no date is no longer placed on today
-
-`ObjectCalendar` mapped a record whose declared `startDateField` carried no
-value to `new Date()` — the current moment — so it rendered on today's cell as
-an ordinary event, indistinguishable from a real one. The `isNaN` guard six
-lines below could not catch it by construction: a no-argument `new Date()` is
-always valid, so the absent-value case became a well-formed lie *before* the
-check that would have caught it.
-
-The fabricating arm is deleted. Such records now leave the grid entirely and
-appear in a collapsed "Unscheduled (N)" area below the calendar — a visible
-count and an expandable list, with no invented date and no scheduling UI. The
-`isNaN` filter keeps its original job for values that are present but
-unparseable: absent and malformed stay two distinguishable outcomes.
-`allDay: !endDate` now applies only to records that have a start, so a record
-with no dates at all is unscheduled rather than silently all-day; a record with
-a start and no end still renders all-day exactly as before.
-
-Adds `calendar.unscheduled` to all ten locale packs.
diff --git a/.changeset/calendar-view-event-rename-5044.md b/.changeset/calendar-view-event-rename-5044.md
deleted file mode 100644
index 0ba8d30bf3..0000000000
--- a/.changeset/calendar-view-event-rename-5044.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/plugin-calendar': patch
----
-
-`@object-ui/plugin-calendar` now exports the `CalendarView` component's runtime event
-type as **`CalendarViewEvent`**, and keeps `CalendarEvent` as a **`@deprecated` alias**
-of it. **Non-breaking:** the alias is a working re-export denoting the same type, so
-code importing `CalendarEvent` from this package keeps compiling unchanged — nothing is
-removed and no behaviour changes.
-
-Why: `@object-ui/types` exports its own `CalendarEvent`, the AUTHORING event
-(`id: string`, `start` / `end` accept ISO strings with `end` required, plus
-`description`), while this package's was the runtime event (`id: string | number`,
-`start: Date`, `end?: Date`). Neither is assignable to the other, and IDE auto-import
-chose between the two identical names essentially at random — the wrong pick surfaced as
-a remote `TS2322` about `Date` rather than as a wrong import, which is how this package's
-own README example stayed uncompilable through an earlier import-path fix. The authoring
-type keeps the canonical `CalendarEvent` name; the runtime type gets the self-describing
-one (objectui#5044, following the `ObjectCalendarProps` -> `ObjectCalendarComponentProps`
-rename in objectui#4650).
-
-Write `CalendarViewEvent` in new code.
diff --git a/.changeset/calendar-view-mode-agenda-retired-5740.md b/.changeset/calendar-view-mode-agenda-retired-5740.md
deleted file mode 100644
index f50f849b41..0000000000
--- a/.changeset/calendar-view-mode-agenda-retired-5740.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema`
-(objectui#5740 — the value-level residue of objectui#5667's key-level
-convergence of `CalendarViewSchema` on the registered `calendar-view`
-renderer's measured read set; ADR-0049 enforce-or-remove).
-
-The union declared a value nothing enforced: the registered renderer's `view`
-input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves
-any off-enum value — `'agenda'` included — to `undefined` (the component's
-`'month'` default), and `CalendarView` renders no agenda view. An author
-writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with
-no error or warning. No in-repo, example, or catalog app authors
-`view: 'agenda'` (measured during objectui#5667's sweep and re-measured for
-this change, including the objectstack tree).
-
-**This narrows the accept set — unlike #5667's key retirements, which created
-no new rejections.** `view` is a declared key, and declared keys are validated
-even under `.passthrough()`, so `view: 'agenda'` is now a **validation error
-that previously parsed green** (an `invalid_value` issue on the `view` path,
-offering `month`/`week`/`day`). Undeclared keys still pass through unchanged.
-Breaking on the published zod surface; ships as `minor` per this repo's
-version-alignment policy (majors track `@objectstack`).
-
-The runtime boundary is unchanged: an off-union `view` in raw metadata still
-falls back to the component's `'month'` default at the renderer, and the
-registry input already declared the three-value enum. Docblocks, the schema
-reference table, and the zod `describe` no longer teach an `'agenda'`
-fallback.
diff --git a/.changeset/calendar-view-schema-converge.md b/.changeset/calendar-view-schema-converge.md
deleted file mode 100644
index 61f9a3c1d7..0000000000
--- a/.changeset/calendar-view-schema-converge.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-calendar': patch
----
-
-`CalendarViewSchema` (TS interface and zod mirror) converges on the registered
-`calendar-view` renderer's measured read set (objectui#5667, maintainer ruling
-option A — the renderer is authoritative).
-
-**Breaking for consumers of the published type** (deliberate; per-repo policy
-breaking changes ship as `minor` — the fixed group's `major` tracks
-`@objectstack`):
-
-- Nine inert keys are retired: `events` (the interface's only required key,
- which the renderer deliberately drops — objectui#4433), `defaultView`,
- `defaultDate`, `date`, `views`, `editable`, `onEventCreate`,
- `onEventUpdate`, `onDateChange`. None had a read site on the authored-node
- path and no measured app authors them (ADR-0049 enforce-or-remove).
-- The type now declares what the renderer actually reads: `data`, `titleField`,
- `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`,
- `currentDate`, `allowCreate`, `className`, plus the two host-only function
- hatches it forwards (`onEventClick`, `onViewChange`).
-- Practical radius, measured: `BaseSchema` carries an index signature and the
- zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys
- neither fail to compile nor get rejected at validation — they are simply no
- longer declared, documented, or type-checked. The material accept change is
- that zod no longer **requires** `events`: a `{ "type": "calendar-view" }`
- node without it now validates (previously the one key validation demanded
- was the one key guaranteed to do nothing).
-
-Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README
-and `content/docs/api/schema-reference.md` are repaired to the converged
-surface in the same change, so no copy of the old contradiction survives.
diff --git a/.changeset/canvas-design-run-5800.md b/.changeset/canvas-design-run-5800.md
deleted file mode 100644
index 18e7c49388..0000000000
--- a/.changeset/canvas-design-run-5800.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one.
diff --git a/.changeset/catalog-layout-props-sweep-4891-4890.md b/.changeset/catalog-layout-props-sweep-4891-4890.md
deleted file mode 100644
index 92f13f1872..0000000000
--- a/.changeset/catalog-layout-props-sweep-4891-4890.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@object-ui/components': minor
----
-
-`stack` now reads its spacing from `gap` and nothing else — the undeclared
-`spacing` key it also accepted is gone (objectui#4890).
-
-`StackSchema extends Omit`, whose spacing key is `gap`.
-`spacing` was declared by nothing: not the TypeScript interface, not the zod
-mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway,
-as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole
-story, since it existed to get past the type system saying the key was not there.
-A lenient consumer leg does not stay in the consumer: it becomes a second
-de-facto contract that producers write to, and 135 nodes across 39 files of the
-shipped schema catalog did exactly that. Every one of them rendered correctly, so
-nothing ever pointed at it, while the examples went on teaching the key to every
-author who copied them.
-
-The trap it was one edit away from springing: `flex` — semantically a `stack`
-with a `direction` — never read `spacing`, so re-typing any of those nodes would
-have dropped the spacing to the default silently. Fixed at the producer
-(AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the
-alias is deleted rather than legalised into `StackSchema`, where it would only
-have been a second name for `gap`.
-
-**If you author `spacing` on a `stack`**, rename it to `gap`; the value and the
-rendering are unchanged. A `stack` still carrying `spacing` now renders the
-default gap, exactly as a `flex` always did.
-
-Also in the same sweep, and visible only in the published example catalog rather
-than in any package API: 140 catalog nodes that were already `flex` / `stack` /
-`container` stopped hand-writing their own declared props in `className`
-(`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`,
-`flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all
-(objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in
-`className`, because the props are not responsive. Both facts are ratcheted in
-`examples/schema-catalog/test/layout-props-conversion.test.tsx`.
diff --git a/.changeset/chart-height-chain-5451.md b/.changeset/chart-height-chain-5451.md
deleted file mode 100644
index 3ab5836770..0000000000
--- a/.changeset/chart-height-chain-5451.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-`ObjectChart`'s wrapper div now carries `h-full`, keeping the height chain intact from a dashboard grid cell's declared height down to the element Recharts measures. Previously the chain died at the plain auto-height wrapper: `height: 100%` on the chart container computed to `auto`, Recharts measured a permanent zero, and only the `CHART_MIN_HEIGHT` floor (#5503) kept dashboard charts visible — at a fixed floor height instead of filling the cell (#5451). Under auto-height parents `h-full` resolves to `auto`, so non-dashboard hosts are unchanged.
diff --git a/.changeset/chat-transport-memo-4187.md b/.changeset/chat-transport-memo-4187.md
deleted file mode 100644
index 93978f70c6..0000000000
--- a/.changeset/chat-transport-memo-4187.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
----
-
-`useObjectChat` no longer rebuilds its `DefaultChatTransport` on every render
-(objectui#4187).
-
-The transport `useMemo` listed the caller's `body` and `headers` in its dep list.
-Both are object props and every caller passes a fresh literal each render — the AI
-page's chat pane builds its `body.context` inline — so the memo never hit and a
-transport was constructed on every render of every chat surface, which during a
-streaming turn is once per token batch.
-
-`body` and `headers` are now read through refs inside
-`prepareSendMessagesRequest`, the idiom this hook already uses for the live model
-(`modelRef`) and the handoff conversation id (`parentConvRef`), and they are gone
-from the dep list. Unlike memoizing at each call site, a future caller cannot
-undo it.
-
-No user-visible behaviour changes: `@ai-sdk/react` keeps the transport in a ref
-and re-keys its `Chat` only on `chat`/`id` (verified against the installed
-4.0.68), which `useObjectChat` passes neither of, so the message thread was never
-at risk — the rebuild was pure waste. The one real difference is *when* the two
-values are sampled: a send now reads them at send time, so it observes the values
-of the most recent render instead of those of the last render that happened to
-rebuild the transport. That is never staler than before, and it is pinned by
-`useObjectChat.transportIdentity.test.tsx`.
diff --git a/.changeset/check-schema-positive-marker-and-skip-count.md b/.changeset/check-schema-positive-marker-and-skip-count.md
deleted file mode 100644
index 84f3ac9f6b..0000000000
--- a/.changeset/check-schema-positive-marker-and-skip-count.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/cli': minor
----
-
-`objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge.
-
-A root `type` was treated as a component key wherever it appeared. `type` heads at
-least seven unrelated JSON vocabularies, and the most common of them is
-`package.json`'s `"type": "module"` — so the first line a user saw running
-`objectui check` in their own project was a warning about their own package
-manifest. Measured at this repository's root: 46 warnings, 45 of them
-`package.json` (objectui#5127).
-
-A file now enters type judgement only when its root carries a structural key
-declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`,
-`style`, the `visible`/`hidden`/`disabled` predicate family, `testId`,
-`ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an
-`.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply
-never judged. The key set is read out of the node contract rather than invented,
-and it is closed: it grows only when `BaseSchema` grows.
-
-A list of filenames to exclude was the alternative and was rejected: it is a
-second hand-maintained list of the shape objectui#5115 had just finished
-deleting, and it can only ever enumerate the foreign vocabularies someone already
-thought of. This is a positive marker instead.
-
-Because the marker narrows what is checked, the command now also reports the
-count of files that had a root `type` and no marker, together with the marker
-keys that opt one back in. That number is the coverage this gate gives up until
-schema files are recognisable, and printing it is what keeps the loss visible
-rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was
-never type-judged, before this change or after it. Exit codes are untouched: a
-JSON parse failure remains the only thing that fails the run.
-
-No public `$schema` URL is introduced. An earlier revision also admitted a file
-whose root `$schema` had an `objectui.org` host; the maintainer ruled against
-minting that identifier (2026-08-20, objectui#5127), so the structural key is the
-only marker. Because the matching was host-based rather than literal, that arm
-can be added later without invalidating a single file.
diff --git a/.changeset/chilled-donkeys-shave.md b/.changeset/chilled-donkeys-shave.md
deleted file mode 100644
index 745cbf50eb..0000000000
--- a/.changeset/chilled-donkeys-shave.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change to the console's public-block binding-reach probe: its `sections` fixture sample is now spec-valid (an array of section objects, not a bare string), its `formType` sample is a real form variant, and the crash guard runs ahead of the branch split so it covers every candidate. No published behaviour changes — the renderers are untouched.
diff --git a/.changeset/cli-app-generator-explicit-branch-cases.md b/.changeset/cli-app-generator-explicit-branch-cases.md
deleted file mode 100644
index c40bb23468..0000000000
--- a/.changeset/cli-app-generator-explicit-branch-cases.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change to `@object-ui/cli`'s app-generator suite: two added cases pin the
-generator's IN-WORKSPACE and STANDALONE context branches explicitly, instead of
-inheriting whichever one the ambient cwd happens to select (objectui#7807). No
-published behaviour changes.
diff --git a/.changeset/cli-validate-field-widget-namespace-5449.md b/.changeset/cli-validate-field-widget-namespace-5449.md
deleted file mode 100644
index 2984a2ef41..0000000000
--- a/.changeset/cli-validate-field-widget-namespace-5449.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/types': patch
-'@object-ui/cli': patch
----
-
-`objectui validate` now refuses a form field whose widget id names a namespace
-other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema`
-has given since objectui#5375 (objectui#5449).
-
-The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema
-declared `type` and `widget` as bare optional strings — so a field typed
-`ui:password` validated clean while the runtime validator rejected the same
-document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an
-author actually runs before shipping, so it was the one handing out the false
-green: an author did exactly the diligence objectui#5375 asks for and still
-shipped metadata that renders a secret into a plain text box.
-
-A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's
-precedence (`widget` before `type`), the key it blames, its error code and its
-message verbatim, so the two entry points cannot describe one defect two ways.
-
-**This rejects documents that previously validated.** Only colon-qualified
-field widget ids outside the `field:` namespace are affected — `field:`-prefixed
-ids and bare names such as `password` still pass, registered or not. A field
-carrying, say, `type: 'ui:password'` must be rewritten as `password` or
-`field:password`; it never rendered as a password box in any case.
-
-Which of the repo's authoring-time validators is canonical remains open
-(objectui#4631) — this states the rule on the zod side rather than unifying
-them.
diff --git a/.changeset/closure-claims-bounded-or-derivable.md b/.changeset/closure-claims-bounded-or-derivable.md
deleted file mode 100644
index ccf0743e1b..0000000000
--- a/.changeset/closure-claims-bounded-or-derivable.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Documentation and gate-only change (objectui#6186). The `thresholds` closure claim
-carried by `@object-ui/sdui-parser`'s unconsumed-widget-option census was written
-twice — in the census header and in the plugin-dashboard docs page — and is now
-single-sourced onto the census header, with a new gate re-deriving it from source
-on every test run. The block-schema page's whole-tree negative is bounded to the
-block family's own keys.
-
-The only edit to published source is the census module's comment header: no
-exported symbol, runtime behaviour or emitted diagnostic changes, so this releases
-nothing.
diff --git a/.changeset/cloud-connection-bind-failure-i18n-5054.md b/.changeset/cloud-connection-bind-failure-i18n-5054.md
deleted file mode 100644
index 68a88aac2c..0000000000
--- a/.changeset/cloud-connection-bind-failure-i18n-5054.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/i18n': patch
----
-
-A cloud-connection bind failure now reads in the user's language whichever clock
-noticed it (objectui#5054).
-
-One abandoned device approval could be noticed by either of two clocks, and the
-Cloud Connection panel had a different answer for each. When the panel's own
-`expires_in` deadline fired first it rendered `cloudConnection.errors.expired` —
-translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered
-HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson`
-threw a bare `Error` carrying only that sentence, and the catch rendered it
-verbatim. Same user, same failure, two languages, decided by which clock got
-there first — visible on a zh console as the same abandoned approval reading
-Chinese or English depending on whether the tab sat open past `expires_in`.
-
-`getJson` now carries the envelope's `declaredCode` and `code` across its throw,
-and a single closed map turns the two RFC 8628 outcomes a user can actually cause
-into console copy: `expired_token` → the existing `cloudConnection.errors.expired`,
-`access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten
-locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream
-spelling there — `code` is `DEVICE_CODE_FAILED` for both.
-
-Every other code is unchanged: `invalid_grant`, and anything upstream invents
-next, still render the wire `message`, which stays the single source of truth for
-failures this console has no copy for. No API, export or resolver was widened.
diff --git a/.changeset/clusterradius-unit-doc-5020.md b/.changeset/clusterradius-unit-doc-5020.md
deleted file mode 100644
index 00f5731ef8..0000000000
--- a/.changeset/clusterradius-unit-doc-5020.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/plugin-map': patch
----
-
-`ObjectMapProps.clusterRadius`'s JSDoc said "in pixels"; `clusterMarkers` has
-always used it as a coordinate-degree grid cell edge (`radius / 2 ** zoom`,
-divided into the marker's `[lng, lat]` degrees), not a screen-space radius —
-a host tuning clustering granularity by the documented unit would get a
-completely different result than intended (objectui#5020).
-
-No behavior, default, or name changes: clustering, the >100-visible-marker
-auto-threshold, and tap-through zoom are unaffected, and `clusterRadius` has
-no call sites outside `plugin-map/src` today (re-confirmed repo-wide,
-including `apps/`, `examples/`, and the `objectstack` spec/server repo — the
-default of `50` is what runs everywhere). This is a doc-comment correction
-only, bringing the JSDoc in line with the README's already-correct wording
-(post objectui#5002).
-
-The latitude-anisotropy trade-off (a degree grid distorts east-west as
-latitude rises) is a known design trade-off, not part of this fix.
diff --git a/.changeset/component-docs-disabled-inherited-7239.md b/.changeset/component-docs-disabled-inherited-7239.md
deleted file mode 100644
index a09347042f..0000000000
--- a/.changeset/component-docs-disabled-inherited-7239.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Docs-only: 13 `content/docs/components/**` pages spelled an inherited
-`disabled?: boolean` in their illustrative interfaces, which went stale when
-objectui#7087 removed the 18 narrowings and left the member inherited from
-`BaseSchema` as `boolean | string`. The pages now spell the shipped union
-(objectui#7239), and a new test-only pin
-(`packages/types/src/__tests__/component-docs-disabled-inherited-7239.test.ts`)
-holds all 14 inherited rows to it while keeping the 8 independent item/option
-rows at plain `boolean`. No published package behaviour changes.
diff --git a/.changeset/component-meta-converge-and-deprecate-alias-5893.md b/.changeset/component-meta-converge-and-deprecate-alias-5893.md
deleted file mode 100644
index 657412bdca..0000000000
--- a/.changeset/component-meta-converge-and-deprecate-alias-5893.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`ComponentMeta` is now declared once and re-exported, and `PluginComponentMeta` is
-deprecated in favour of `ComponentMeta` (objectui#5893).
-
-## The convergence
-
-`@object-ui/types` published `ComponentMeta` twice, from two different declarations:
-`base.ts` and `plugin-scope.ts` (the latter published as `PluginComponentMeta`). They
-were structural copies, not an alias pair. `plugin-scope.ts`' `ComponentMeta` is now
-`export type { ComponentMeta } from './base.js'` — the disposition objectui#4580 ruled
-for the identical shape, *a structural copy would reproduce the defect the moment either
-side moved*, and the same move objectui#5671 made for the sibling type `ComponentInput`
-in the same file.
-
-Either side had already moved. `base.ts` declared eleven keys; the plugin-scoped copy
-declared nine — the same nine, **minus `tags` and `description`**. So a plugin author
-typing against the plugin-facing declaration could not write two keys the main surface
-advertises, and which the runtime validator already accepted: `ComponentMetaSchema` in
-`zod/base.zod.ts` declares all eleven, so two of the three authorities agreed and the
-plugin-facing one did not. `resizeConstraints`' six members were identical in both, so
-the delta was exactly those two keys.
-
-What changes for a consumer: `tags` and `description` become writable on the
-plugin-facing type. Nothing narrows — no key is removed and no key's type changes, so no
-existing registration stops compiling. The convergence buys **acceptance** of two keys;
-it buys no rejection of anything. `ComponentMetaSchema` is a plain `z.object` with no
-`.strict()`, so it strips unknown keys rather than refusing them, and that is unchanged
-here.
-
-## The alias deprecation, sequenced after it
-
-`PluginComponentMeta` — the published alias for the plugin-scoped declaration — is now
-`@deprecated` in favour of `ComponentMeta`. **`PluginComponentMeta` is the name to search
-for** if you import it; replace it with `ComponentMeta` from the same entry point.
-
-This is stage 1 of objectui#5674's two-stage retirement (maintainer ruling, 2026-08-22:
-deprecate for a release, then remove). Nothing is removed here — the export still exists
-and still names the same type.
-
-The ordering is deliberate and is why the two halves ship together. Until the convergence
-above, the alias named a genuinely different nine-key interface; deprecating it then
-would have warned consumers about a name that was still about to change meaning. It is
-deprecated now, at its final meaning.
-
-**Why a deprecation window rather than a deletion.** The measurement that licenses
-deleting an export from a published package is *"no importer"*, and what can be measured
-from inside this repository is only *"no importer here"*. In-repo, `PluginComponentMeta`
-has exactly one occurrence — its own export line — searched across every root
-(`packages/`, `apps/`, `content/`, `docs/`, `skills/`, `examples/`, `e2e/`, `scripts/`,
-`eslint-rules/`, `public/`, `.changeset/` and the root docs) plus the sibling
-`objectstack` framework checkout, with controls searched identically so a broken search
-could not read as a clean one. What no search here can see is a consumer on npm. **That
-external caveat is unchanged from objectui#5674 and is not being dropped:** the window
-converts a silent break into a warned one before stage 2 lands. Stage 2 removes the alias
-and the now-dead re-export in `plugin-scope.ts` that exists only to feed it, and ships as
-a `minor` under this repo's policy that its own breaking changes never declare `major`.
-
-## Pinned by identity, not by member set
-
-A new test asserts that `plugin-scope.ts` re-exports the declaration and declares no
-`ComponentMeta` of its own. A member-set assertion cannot do this job: TypeScript is
-structurally typed, so a local re-declaration carrying the same eleven keys is mutually
-assignable with the imported one and passes every type-level check. A member-identical
-structural copy is exactly what objectui#4580 predicted would drift and exactly the state
-this card recorded — this copy started identical and acquired its two-key delta later.
-The member-set checks are kept alongside the identity pin, labelled as the control that
-shows what it cannot see.
diff --git a/.changeset/componentinput-reexport-4972.md b/.changeset/componentinput-reexport-4972.md
deleted file mode 100644
index 910e0a7348..0000000000
--- a/.changeset/componentinput-reexport-4972.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/types': minor
----
-
-`ComponentInput` is now declared once and re-exported, instead of restated in three
-places (objectui#4972).
-
-`@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`'
-plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`)
-were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now
-re-exports of that one declaration, which is the disposition objectui#4580 ruled for the
-identical shape — *a structural copy would reproduce the defect the moment either side
-moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`.
-
-Either side had already moved. `base.ts` declared thirteen keys; both copies declared
-nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every
-component registration actually imports**. Those four keys were unwritable at any real
-registration — a plain TypeScript error at the call site — while `ComponentInputSchema`
-(the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face
-advertised four keys the authoring face rejected. Measured over the repository, no
-registration had tried to write one yet, so nothing a user hits was broken today; what
-changes is that the four keys become writable, and there is no longer a second
-declaration for the next widening to miss.
-
-`ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single
-declaration imported by all three sites (objectui#3832); this converges the rest of the
-interface.
-
-Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is
-byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported
-here only with its control — a probe that added a *required* key to `ComponentInput` left
-the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of
-`export *` lines that names `ComponentInput` zero times. The gauge that can actually fail
-is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does
-`@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted
-declarations that change in either package.
-
-`WidgetInput`'s union-arm capability is deliberately untouched — a different gate path
-and a separate judgment.
diff --git a/.changeset/componentrendererprops-reexport-4594.md b/.changeset/componentrendererprops-reexport-4594.md
deleted file mode 100644
index 52cad5745a..0000000000
--- a/.changeset/componentrendererprops-reexport-4594.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/core': minor
----
-
-`ComponentRendererProps` is now declared once and re-exported, instead of
-hand-declared a second time in `@object-ui/core` (objectui#4594).
-
-`@object-ui/core`'s `ComponentRendererProps` (`src/types/index.ts`) was a
-non-generic interface typing `schema` as `SchemaNode`, while
-`@object-ui/types`' declaration of the same name is generic —
-`ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` with
-`schema: TSchema`. Same name, both exported from their package entry, from two
-packages the same consumers import together: which declaration a call site got
-depended on which package it reached for, and the two disagree about whether a
-primitive node is admissible. Core's is now a re-export of types', which is the
-disposition objectui#4580 ruled for `SchemaNode` two lines above it in the same
-file, and objectui#4972 for `ComponentInput` — *a structural copy would
-reproduce the defect the moment either side moved*.
-
-**Published-surface effect, and the reason it is not neutral.** Resolved
-through the TypeScript checker from `core/dist/index.d.ts` over a clean rebuild
-of both legs, `ComponentRendererProps` as reached through `@object-ui/core`
-moves from non-generic with
-`schema: BaseSchema | string | number | boolean | null | undefined` to
-`ComponentRendererProps` with `schema: TSchema`, defaulting to
-`BaseSchema`. `schema` therefore **narrows** back to the object form — core's
-copy had silently widened when objectui#4608 made core's `SchemaNode` a
-re-export of types' union — and the type gains a parameter. **Nothing imported
-it**, on either side, re-verified repo-wide on the merged ref, so no call site
-can observe either move; the narrowing is recorded here because it is a change
-to a published type, not because a consumer is affected.
-
-A compile-time pin now holds the reconciliation from
-`@object-ui/react` — the only position that resolves both packages through
-`node_modules` — alongside the existing `SchemaNode` one. It is a test-only
-addition and emits nothing, so `@object-ui/react` takes no bump of its own.
diff --git a/.changeset/components-react-page-published-dts-text-5666.md b/.changeset/components-react-page-published-dts-text-5666.md
deleted file mode 100644
index 6b021a41f0..0000000000
--- a/.changeset/components-react-page-published-dts-text-5666.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/components': patch
----
-
-Published declaration text for the react page renderer changed: `kind:'react'` page `source` styling is now stated in the shipped `.d.ts`
-
-`dist/renderers/layout/react-page.d.ts` is in this package's npm tarball, and the
-declaration emitter reproduces that module's file-header documentation into it
-verbatim. Two things a consumer reads — on hover over `ReactKindPage` in an editor,
-or straight out of the tarball — now say something different:
-
-- The injected-scope note no longer tells authors that layout is left to "plain
- HTML + Tailwind". It says plain HTML.
-- A new paragraph states the styling contract for `kind:'react'` page `source`:
- `source` is runtime metadata, not build input. Style with inline `style` objects
- using `hsl(var(--token))` theme colors, and render overlays through `ObjectForm`
- with `formType` `"drawer"` or `"modal"` rather than a hand-rolled `fixed inset-0`
- backdrop. Do **not** author Tailwind utility classes in page `source`: the
- console's Tailwind is compiled at build time by scanning the console's own `src`
- and there is no safelist, so an authored utility class silently produces no CSS
- and no error anywhere. `os validate` reports it as
- `page-source-className-tailwind`. (ADR-0065; ADR-0080's 2026-06-30 amendment;
- see `content/docs/guide/react-pages.md`.)
-
-That is guidance an author can act on, and it contradicts what this package's
-declarations previously told them, so it is a change to the published surface
-rather than an internal edit. No runtime behaviour changed and no export moved.
-
-This entry is corrective. The text landed under objectui#5461 with a changeset that
-declared only `@object-ui/types`, on the stated grounds that the edits "do not
-project into any `.d.ts`". Rebuilding the package shows that they do — the reasoning
-and the measurement are recorded in `scripts/check-changeset-presence.mjs`'s header,
-where the next author will meet them.
diff --git a/.changeset/confirm-card-terminal-5695.md b/.changeset/confirm-card-terminal-5695.md
deleted file mode 100644
index 7d3d67a83e..0000000000
--- a/.changeset/confirm-card-terminal-5695.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/plugin-chatbot': minor
-'@object-ui/app-shell': patch
-'@object-ui/i18n': patch
----
-
-The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs.
diff --git a/.changeset/confirm-handler-published-type-5835.md b/.changeset/confirm-handler-published-type-5835.md
deleted file mode 100644
index d141b7648e..0000000000
--- a/.changeset/confirm-handler-published-type-5835.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Type `RecordDetailView`'s confirm handler as the published `ConfirmationHandler` instead of an inline re-spelling of the same shape, and pin that both of `app-shell`'s confirm runtimes hand `ActionConfirmDialog` the same field set. Releases nothing: the annotation is erased at build time (the emitted JS for `RecordDetailView.tsx` is byte-identical before and after), the handler is not exported, and no published surface changes.
diff --git a/.changeset/confirm-runtime-close-parity-6034.md b/.changeset/confirm-runtime-close-parity-6034.md
deleted file mode 100644
index 94766b7b4a..0000000000
--- a/.changeset/confirm-runtime-close-parity-6034.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Console action runtime: closing an action confirm dialog now keeps the dialog's
-text instead of blanking it mid-fade.
-
-`useConsoleActionRuntime` reset its confirm state by replacing the whole object
-(`{ open: false, message: '' }`), which cleared `message` and dropped `options`.
-Radix keeps `AlertDialogContent` mounted through its exit animation, so the
-dialog's description went blank and its title and button labels reverted to
-their defaults while it was still fading out. It now flips only `open` and keeps
-every field, matching `RecordDetailView`'s second confirm runtime, which already
-closed this way. Both runtimes feed one `ActionConfirmDialog`; the parity pin now
-covers the close path as well as the open path.
diff --git a/.changeset/console-action-dispatch-envelope-5611.md b/.changeset/console-action-dispatch-envelope-5611.md
deleted file mode 100644
index a7e8555f25..0000000000
--- a/.changeset/console-action-dispatch-envelope-5611.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
----
-
-Internal: the console's admin-override notice now travels on a declared dispatch
-type instead of a cast (objectui#5611). No published API and no runtime
-behaviour changes, so this changeset declares "no release" rather than a bump —
-`@object-ui/app-shell` source moved, nothing it exports did.
-
-`overrideNotice` is the safety copy shown once, ahead of a privileged admin
-override that finalises an approval step over approvers who have not acted. Its
-producer (`DeclaredActionsBar`) reached its reader (`useConsoleActionRuntime`'s
-param-collection dialog) through a `dispatch as ActionDef` cast on one side and
-`action?: any` on the other, so nothing declared the key anywhere and the two
-could drift apart in silence — rename it on either side and the notice stops
-appearing with every test still green, because each side's suite spells the key
-itself.
-
-Both ends now share one declaration: `ConsoleActionDispatch`
-(`ActionDef & { overrideNotice?: string }`), a HOST-composed envelope that lives
-at the seam, in the one package where producer and reader both live. The cast is
-gone and both param-collection handlers narrow off `any` — which is what puts
-those functions under the compiler at all.
-
-The published `ActionDef` deliberately does NOT declare the key (maintainer
-ruling 2026-08-22): it is the authored-metadata mirror, and `overrideNotice` is
-the first key no author supplies, so declaring it there would make an unenforced
-key legally writable in metadata. That prohibition still holds exactly as
-written — `ActionDef` and `ACTION_DEF_KEYS` are unchanged.
-
-`@object-ui/core` is NOT untouched, and the reason is a separate declaration:
-the same ruling's item 4 adds `HOST_DISPATCH_ACTION_KEYS` to the key inventory
-so the dev-mode warning stops calling the host-composed key unknown. That change
-carries its own changeset (`host-dispatch-action-keys-5611.md`) and its own
-patch bump; this one remains the app-shell half, which publishes nothing.
diff --git a/.changeset/console-boot-request-dedup-5544.md b/.changeset/console-boot-request-dedup-5544.md
deleted file mode 100644
index 44270d6e83..0000000000
--- a/.changeset/console-boot-request-dedup-5544.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/types': patch
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-The console's cold load no longer asks `/api/v1/runtime/config` or
-`/auth/me/localization` twice (objectui#5544).
-
-Two pairs of boot callers were racing each other for the same URL, with no shared
-provider between them, so no guard inside either component could see the other:
-
-- `GET /api/v1/runtime/config` — the pre-React branding script inlined in
- `apps/console/index.html` (it runs during HTML parse so the tab title and
- favicon are the operator's before the bundle is fetched) and
- `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the
- expensive one: the console `await`s `initRuntimeConfig()` before
- `createRoot().render()`, so the duplicate sat on the critical path to first
- paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed
- boot concurrency further past the server's pool knee.
-- `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true
- first visit and `LocalizationFetchProvider` on every boot. The seed keeps
- running past its 500 ms race by design and the provider mounts the moment that
- race resolves, so on a first visit the two overlap. Measured ×2 on staging.
-
-`@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET
-while one is already in flight join that request instead of starting another. It
-shares the in-flight promise and nothing else — the entry is deleted the instant
-the request settles, so there is no cache, no TTL and no stale window, and a
-caller arriving after settle fetches fresh exactly as before. Rejections fan out
-to every sharer with the status intact (`LocalizationFetchProvider`'s retry
-policy still sees its own 503), each caller receives its own copy of the parsed
-body, and only GETs are eligible — a non-GET is refused rather than quietly
-rewritten.
-
-Requests that differ in credentials mode or headers keep separate identities, so
-the console's two deliberate `auth/get-session` calls — one Bearer-only with the
-cookie omitted to detect a stale token, then one through the cookie — stay two
-requests. Collapsing those would have destroyed the signal the first one exists
-to read.
-
-No component receives anything different: same payloads, same errors, one fewer
-round trip.
diff --git a/.changeset/console-boot-splash.md b/.changeset/console-boot-splash.md
deleted file mode 100644
index 7849076e82..0000000000
--- a/.changeset/console-boot-splash.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/console': patch
----
-
-fix(console): paint a boot indicator during the pre-React white frame
-
-The console's `index.html` shipped an empty ``, so a hard
-navigation showed a pure-white page until the module graph had downloaded,
-evaluated and resolved the two round trips `main.tsx` awaits before
-`createRoot().render()`. Measured on this repository's production build with
-Playwright + CDP screencast (frames classified pixel-by-pixel, corroborated by
-the Paint Timing API): 1224-2016 ms of pure white on an unthrottled localhost,
-2289-2297 ms at 20 Mbps/80 ms RTT, and 5808-6167 ms on a Fast-4G profile.
-
-The document now carries the indicator itself — an inline style block and the
-same gradient tile `LoadingScreen` opens with — so it paints from the HTML
-parser, before the first chunk is requested. It is removed on React's first
-commit into `#root`, so it never doubles up with `LoadingScreen`, which
-continues to own the init screen and its ten-language copy.
diff --git a/.changeset/console-form-container-specs-one-declaration-5596.md b/.changeset/console-form-container-specs-one-declaration-5596.md
deleted file mode 100644
index 6f199b8c8d..0000000000
--- a/.changeset/console-form-container-specs-one-declaration-5596.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-The two form CONTAINER contracts now have ONE declaration each, derived from
-`@objectstack/spec`, and the console reads them instead of its own copies.
-
-objectui#5542 converged the LEAF of this contract — the field spec — and left the
-two containers above it untouched, because converging them was a bigger call than a
-mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared
-twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once
-in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean
-subset — these two had **already drifted, in both directions**, so neither copy was a
-subset of the other and there were two live answers to "what may an author write":
-
-- `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`;
- the console declared none of them. The console's `columns` admitted the string arm
- (`'1' | '2' | '3' | '4'`); app-shell's took numbers only.
-- `FormViewSpec` — the console declared `label` / `groups` / `sharing` /
- `submitBehavior`; app-shell stopped at `type` plus `sections`.
-
-The drift is decided by asking the **contract**, not by picking a side. `columns`
-does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])`
-with the four numeric literals, folded to a number by its own transform), so
-app-shell's numbers-only declaration was rejecting metadata the platform accepts —
-objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is
-the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured
-against the installed `@objectstack/spec` 17.0.0), because a form config is titled,
-not labelled. The value that read actually finds is the VIEW's identity label, which
-arrives on the `ExpandedViewItem` envelope or beside the config on a flattened
-runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to
-the body it unwraps, rather than smuggled onto the form contract.
-
-Both types are therefore **derived from the spec's own `FormSection` / `FormView`
-with named narrowings** — the repo's sanctioned form for a spec-shaped local type
-(`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the
-two layers agree on comes from the spec and cannot fall behind it; the four positions
-where this layer is deliberately narrower are each named in an `Omit` list and
-restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving
-it would silently re-open #5542), and `label` / `description` / `visibleWhen` /
-`visibleOn` keep the shapes this repo's renderers and evaluators actually consume
-rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s
-`submitBehavior` union — previously hand-written under the comment "Mirrors the spec
-FormView.submitBehavior union" — is now read back off the shared type, making the
-mirror structural. `@object-ui/app-shell` re-exports both names from its package root
-(type-only, erased at build — nothing is added to the bundle), because a type that
-cannot be imported is a type that gets retyped.
-
-The pins are what make future drift loud, and each half is pinned on both sides.
-`form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the
-non-narrowed half of each type against the spec's own symbol, so re-hand-writing
-either declaration fails `type-check` the day the spec moves rather than years later
-when someone reads two files side by side — and the console's pins read both types
-back out of the **exported** `buildSections` signature rather than naming them, so a
-re-inlined local copy fails even if it agrees on every key on the day it is written.
-Their liveness controls are what stop them being phantom checks: the removed copies
-are pinned NOT equal to the shared types (proving the `Equal` helper still
-discriminates), the renderer's honoured `RenderableSection` is pinned not equal
-either (so the authored-document and honoured-row types cannot be collapsed again),
-and an undeclared key is still rejected (so the derivation smuggled in no index
-signature or `any`). Every narrowing carries a matching negative pin, so "derived"
-cannot quietly become "widened to whatever the spec says".
-
-Behaviour is unchanged — the runtime always accepted these keys. The vitest halves
-prove it: a section spelling its column count as the string `'3'` lays out identically
-to the numeric `3` on both sides, and a section carrying the keys only one side used
-to declare builds the same rows.
diff --git a/.changeset/console-form-field-spec-one-declaration-5542.md b/.changeset/console-form-field-spec-one-declaration-5542.md
deleted file mode 100644
index 59b1a9fdc5..0000000000
--- a/.changeset/console-form-field-spec-one-declaration-5542.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-The form-field authoring contract now has ONE declaration, and the console reads it
-instead of its own copy.
-
-objectui#5040 was not a missing key. It was that **two hand-written descriptions of
-one contract drifted**, and nothing could notice, because each was only ever checked
-against itself. PR #5537 converged the two app-shell descriptions into
-`views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`:
-`FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same
-name, in a different package — so the same failure mode stayed fully available.
-
-Measured key by key before choosing a route, because the two honest outcomes are
-"same contract, import it" and "genuinely narrower layer, rename it and pin the
-subset". The console's copy was a strict subset — 9 of the shared type's 26 keys,
-every one identical in type, none console-only — and it sat in a position that
-describes an **authored document**: `FormSectionSpec.fields`, read straight off the
-`/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both
-files even spell the same six-member `type` union and call the element type
-`FormFieldSpec`). The narrow, renderer-honoured shape is a different type that
-already exists in that file, `RenderableField`. So this was one contract described
-twice, and the console's description was wrong about the document: legal metadata —
-`visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`,
-and ten more keys — was undeclared there. That is #5040's own symptom, "the type
-rejects the configuration the runtime accepts", which no runtime test can see.
-
-`@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root
-(type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx`
-imports it and deletes the local declaration. Reachability is the load-bearing half:
-a type that cannot be imported is a type that gets retyped, and retyped copies drift.
-`form-spec.ts` itself is untouched.
-
-`FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the
-field-spec type back out of the **exported** `buildSections` signature rather than
-naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even
-if the copy agrees on every key on the day it is written — which is exactly what did
-not happen to the copy this change removes. Its liveness controls are what stop it
-being a phantom check: the removed nine-key shape is pinned NOT equal to the shared
-type (so the `Equal` helper is proven to still discriminate), `RenderableField` is
-pinned not equal to it either (so the honoured-row and authored-document types cannot
-be collapsed again), and an undeclared key is still rejected (so the import did not
-smuggle in an index signature). Behaviour is unchanged: the runtime always accepted
-these keys, and the vitest half proves the same rows are built.
diff --git a/.changeset/console-form-marked-refusal-5210.md b/.changeset/console-form-marked-refusal-5210.md
deleted file mode 100644
index 3c95052807..0000000000
--- a/.changeset/console-form-marked-refusal-5210.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/react': minor
-'@object-ui/components': patch
----
-
-The console form now renders a refusal message the producer explicitly marked
-as user-facing, instead of always substituting a generic string
-(objectui#5210).
-
-An application's hook guards could not talk to their users. When a hook refused
-a write with 403, the form replaced the server's text with
-`form.noPermissionToSave` unconditionally — the recorded objectstack#3821 fix,
-which exists because a raw refusal body puts untranslated platform diagnostics
-(`FORBIDDEN: insufficient privileges to update showcase_private_note
-pi-TgoJ4_DM55Fqz`) in front of end users. The external report behind this change
-had 11 guards whose deliberate, localized guidance — which role owns the action,
-whom to ask — never reached anyone, and named the incentive that creates:
-returning 400 instead of 403 for permission failures, degrading the status
-semantics logs, monitoring and API consumers depend on.
-
-The maintainer ruling (2026-08-19) was a producer-side opt-in rather than a
-chattier 403 branch, and the platform half shipped as objectstack#9934: a hook
-marks its refusal text with `userMessage` at throw time. This is the consumer
-half.
-
-- `@object-ui/react` gains `declaredUserMessage(err)` — the one "is this
- marked?" read. It answers the marking verbatim, from the two places the
- adapter boundary parks the envelope (the error itself, where
- `@objectstack/client` lifts it, and `details`), and `null` for everything
- else.
-- The form prefers a marked message over both its generic strings, on ANY
- status — the marking is status-agnostic; 403 is where this was reported, not
- a fence the contract draws.
-
-**Unmarked refusals are unchanged**: a 403 with no marking still shows the
-generic `form.noPermissionToSave`, and the raw text still goes to the browser
-console only. objectstack#3821's protection is preserved by construction, not
-by re-guessing which 403 bodies are presentable — the mark and the marked text
-are one field, so no boundary that rewraps or substitutes `message` can promote
-platform prose into the user-facing channel, and platform code never sets it.
diff --git a/.changeset/console-formpage-runtime-default-seed-5727.md b/.changeset/console-formpage-runtime-default-seed-5727.md
deleted file mode 100644
index d69e013e00..0000000000
--- a/.changeset/console-formpage-runtime-default-seed-5727.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The console's form routes no longer seed a RUNTIME `defaultValue` into the
-control, so the server-side resolution the declaration asks for actually happens
-(objectui#5727).
-
-`readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared
-default unconditionally. A `defaultValue` may be a literal, or an *instruction*
-the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`,
-`current_user`) or a CEL Expression envelope. Seeding one of those literally put
-the text `NOW()` into a datetime input on both `/forms/:name` and the public
-`/f/:slug` route, and submitting it sent that string as the field's value —
-which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved
-the declared default and the column stored the token text instead of a timestamp.
-
-The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same
-published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its
-seeding with, and the one this renderer already reads once removed (through
-`isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default
-leaves the key ABSENT rather than empty, because absent is precisely the case the
-engine resolves.
-
-Nothing else about the prefill precedence moves: a literal default still seeds, a
-stored record value still wins over a default, and an explicit `prefill_
=`
-param still wins over both — including for a field whose default is a runtime
-token, since a value a producer supplies is not a declaration awaiting
-resolution.
diff --git a/.changeset/console-formpage-visible-predicates-5594.md b/.changeset/console-formpage-visible-predicates-5594.md
deleted file mode 100644
index 60c95e1a1c..0000000000
--- a/.changeset/console-formpage-visible-predicates-5594.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The console's standalone form renderer now evaluates conditional field visibility.
-
-`apps/console/src/components/FormPage.tsx` is a **second, independent form renderer**
-— its own `buildSections`, its own JSX — and it serves both the public
-`/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of
-the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` /
-`visibleOn` *read* inside that file returned zero. So a field an author conditioned on
-`record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec`
-normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both
-authors and honours — rendered unconditionally on both routes. Fail-open and silent:
-the author saw the field always, with no diagnostic.
-
-objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different
-chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form`
-`sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx`
-is on that chain at no point, and #2212's regression pin lives with the chain it fixed,
-so nothing in the suite could see this copy. One contract, two implementations, each
-only ever checked against itself.
-
-The wiring is **#2212's ruling applied verbatim** rather than a second predicate
-semantics invented for this renderer, because two form renderers disagreeing about what
-`visibleWhen` *means* would be a worse defect than one renderer ignoring it. The
-predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`,
-`evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and
-`{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*`
-= the stored record an edit form started from), and the fail-open-but-loud behaviour on
-an unevaluable predicate are the shared ones by construction. Resolution is
-canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers:
-`sectionFields.ts` and app-shell's `readVisibility`.
-
-Two things deliberately did **not** change. A field hidden by its predicate still
-submits its value — conditional visibility is a rendering rule in both renderers, and
-making it a submit-payload rule would be a new contract decided once for both, not
-invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain:
-the second-renderer question is real, but it belongs with the #5596 convergence track,
-not with a predicate that is dead today.
-
-`FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this*
-renderer on purpose — a pin that cannot see the second copy is how the first gap
-survived. With the fix reverted and the pin in place the suite reports
-`11 failed | 1 passed (12)`; the one green is the control that has to be green (a field
-with no predicate still renders), without which every "the field is absent" assertion
-would be equally satisfied by a renderer that draws nothing at all.
diff --git a/.changeset/console-lazy-docs-portal-5467.md b/.changeset/console-lazy-docs-portal-5467.md
deleted file mode 100644
index 9b44325e88..0000000000
--- a/.changeset/console-lazy-docs-portal-5467.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to.
-
-`AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the
-app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same
-three statically for the platform portal at `/docs` (ADR-0046 section 6), so all
-of them sat in the eager graph regardless and the `import()` moved nothing —
-three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build`
-(objectui#5467). A static import on either side silently defeats the split for
-both, and the only signal is a build warning that fails nothing.
-
-`App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`,
-matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even
-though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot,
-so nothing imported `DocsIndex` dynamically, but left static it alone would keep
-`DocShell`, `use-book-data` and `book-nav` eager and the portal would only
-half-leave the closure.
-
-Measured on this branch with the `dist/eager-closure.json` gauge added by
-objectui#5324, both builds exiting 0:
-
-| | before | after |
-|---|---|---|
-| `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 |
-| eager closure, gzipped | 3,881,609 B | 3,870,058 B |
-| eager chunks | 58 | 52 |
-
-Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz),
-`CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B),
-`componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown
-folds into the entry chunk rather than dropping — which is why the entry chunk
-grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by
-11,551 B. The entry stays far under that budget's 350 KB line, and the eager
-closure is the number a page load actually pays.
-
-What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this
-looked like a bigger win than it is. Three eager chunks import it statically,
-and only one of them was this portal: `plugin-chatbot` reaches it directly, and
-`packages/fields`' `MarkdownContent` — lazy in source — is folded into the
-eagerly imported `ui-components` chunk by the `advancedChunks` group that claims
-every `packages/fields` module. That is objectui#5325's mechanism, not this
-card's, and it is why the saving here is 0.30% rather than 4%.
diff --git a/.changeset/console-preboot-branding-origin-5660.md b/.changeset/console-preboot-branding-origin-5660.md
deleted file mode 100644
index d642b3dd34..0000000000
--- a/.changeset/console-preboot-branding-origin-5660.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The console's pre-boot branding script now resolves its server origin from
-`VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of
-`window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set
-(objectui#5660).
-
-The two callers of `GET /api/v1/runtime/config` share one in-flight request, and
-that sharing keys on the FULL URL. So the origin was half of the contract: the
-inline script in `index.html` read one spelling and `src/main.tsx` read another, and
-whenever they disagreed the two never found each other. A same-origin production
-build hid it completely — both spellings collapse to `''` — so the split surfaced
-only in a dev pointing the console at a separate server, where it cost two requests
-to two different servers and let pre-boot branding paint from the wrong one.
-
-The pre-boot fetch is kept, not deleted: it is the request the module side JOINS.
-`sharedGetJson()` can only hand an earlier request to a later caller, and this
-script is the earlier one by construction — it runs during HTML parse, before the
-bundle is fetched, while `initRuntimeConfig()` is awaited before
-`createRoot().render()`. Deleting it would not remove a request; it would move the
-single remaining one later, onto the critical path to first paint, and leave the
-page's empty `` and favicon unbranded until React mounts.
-
-Vite substitutes its HTML env token only when the variable is set and leaves it
-verbatim otherwise, so the unset case is read as same-origin `''` rather than
-allowed to reach the URL as a path segment.
diff --git a/.changeset/console-studio-builder-decorative-lazy-5486.md b/.changeset/console-studio-builder-decorative-lazy-5486.md
deleted file mode 100644
index e557f79f0b..0000000000
--- a/.changeset/console-studio-builder-decorative-lazy-5486.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/console': patch
----
-
-`registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing.
-
-The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a
-`Suspense` fallback — naming the same barrel the line above it imports
-statically for `registerAppComponent`, and the same barrel `App.tsx` pulls
-`BuilderLanding` from to render the standalone `/studio` landing full-screen.
-Either reason alone makes the `import()` unable to move a module into another
-chunk (objectui#5486).
-
-**This moves no modules and is not a bundle improvement.** `BuilderLanding` was
-already in the eager graph via `App.tsx` and still is. Measured on
-`dist/eager-closure.json`, both builds exiting 0: the eager closure holds the
-same 52 chunks with the same names, and the only difference is 130 B gzipped
-(413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback
-text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure,
-because nothing could.
-
-What it does fix is honesty. The old code told every reader the builder was
-deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every
-console build — the console's count of those drops from 44 to 43, with the 43
-remaining ones all belonging to the `packages/fields` barrel (objectui#5325).
-A permanent warning that fails nothing is how a team learns to skim past build
-warnings, and a decorative `lazy()` is how the next reader learns something
-false about the chunk graph.
-
-The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx`
-lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in
-statically, and is genuinely deferred; it is untouched. Making the *builder*
-genuinely lazy would mean taking `App.tsx` off the static import too, changing
-how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind
-the barrel — a separate measured card, not folded in here.
diff --git a/.changeset/console-user-preference-request-budget.md b/.changeset/console-user-preference-request-budget.md
deleted file mode 100644
index a629873f79..0000000000
--- a/.changeset/console-user-preference-request-budget.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Tests only: pin the console `sys_user_preference` request budget across one
-mount (objectui#5544). Three distinct preference keys, one read each, and every
-consumer still receives its own row. No runtime code changes, nothing to release.
diff --git a/.changeset/container-declaration-ratchet-6779.md b/.changeset/container-declaration-ratchet-6779.md
deleted file mode 100644
index b9c378806f..0000000000
--- a/.changeset/container-declaration-ratchet-6779.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change: a universal runtime census pin that fails when any registration
-renders an authored `children` list while its registry meta omits `isContainer`,
-plus `scripts/container-declaration-baseline.json` — a ratchet-to-zero list of the
-44 existing violations (objectui#6779). No published behaviour changes; no
-registration's metadata is altered by this change.
diff --git a/.changeset/core-datascope-field-and-comparison-guards-7751.md b/.changeset/core-datascope-field-and-comparison-guards-7751.md
deleted file mode 100644
index 482715ad71..0000000000
--- a/.changeset/core-datascope-field-and-comparison-guards-7751.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@object-ui/core': minor
----
-
-`DataScopeManager` now **denies** a row when a scope rule names something that is not the record's own data, and when an ordered comparison would only succeed by coercing one of its two sides. It used to **admit** those rows.
-
-Two fail-opens on a row-level permission boundary, both measured against the previous release's source, both the same silent direction as objectui#7378 — a result set that is too large, with no error and no console line, which looks exactly like a correctly configured permissive scope.
-
-**The field a rule names is now read as an own member of the record, or not read at all.** `{ field: 'constructor', operator: 'ne', value: anything }` returned the ENTIRE dataset: the name resolved on the prototype chain, `Function !== 'x'` is true, and every row passed the rule that existed to hide it. The read now has three cases instead of one. A name in the refused list (`__proto__`, `constructor`, `prototype`) denies. An own member is read as before. A name that is not an own member but still resolves on the record's prototype chain — `toString`, `valueOf`, `hasOwnProperty`, or a field inherited from an `Object.create` parent — denies, because the value exists but is not this record's data. A name that resolves nowhere is a genuinely absent field and still reads as `undefined`, so the ordinary "this row has no `status`" rules keep every verdict they have always had.
-
-That third case is deliberately stricter than `evaluateCondition` in `@object-ui/permissions`, which this card was filed to converge with. Reading with `hasOwnProperty` alone — the sibling's shape — collapses "inherited" into "absent", and absent ADMITS on a negative operator, so the sibling still returns `true` for `{ field: 'toString', operator: 'neq' }` on every record (filed as objectui#8044). Converging on the sibling's exact lines would have closed three spellings and left the class open, and would itself have widened one case: an inherited field value flips from denied to admitted under `ne`. Distinguishing inherited from absent closes the class and keeps the change a narrowing everywhere.
-
-**Ordered comparisons (`gt` / `gte` / `lt` / `lte`) now require both sides to be the same comparable kind.** `{ field: 'age', operator: 'gte', value: 0 }` admitted records whose `age` was `null`, `'10'`, `true`, `false`, `''` or `[]` — every one of them through a coercion to a number that the rule's author never wrote. Both sides must now be numbers, or both strings, or both `Date`s.
-
-Same KIND, not "both numbers". The sibling requires `typeof === 'number'` on both sides; copying that predicate would have denied every row for `{ field: 'created', operator: 'gte', value: '2023-01-01' }`, since ISO date strings, plain string ranges and `Date` objects all order correctly on this evaluator today and none of those comparisons coerces anything. The hazard is cross-kind comparison, so cross-kind is what is refused.
-
-**`contains` now requires its rule value to be a string** rather than calling `String()` on it, so `{ operator: 'contains', value: 1 }` no longer matches the record value `'10'`. Same unwritten coercion as the ordered arms; the sibling already refused it.
-
-**The narrowing, named plainly for anyone upgrading with rules already stored.** A legitimate rule loses rows in exactly three shapes. A numeric rule (`age gte 18`) over a dataset where numbers arrive as strings — from JSON, a CSV import, an unparsed form field — stops matching those records; `'20'` was admitted by coercion and is now denied, and the fix is to parse the field at the producer rather than to widen the rule. A rule reading a field that records inherit from a shared prototype rather than own stops matching. And a `contains` rule written with a non-string value stops matching. Measured over a 2772-case differential matrix of value kinds, operators and record shapes: 352 verdicts narrowed, **zero widened**, and zero change to the genuinely-absent-field family.
-
-Operator SPELLING is untouched, deliberately: `ne` / `nin` here versus `neq` / `not_in` in the sibling, and the sibling's `is_null` / `is_not_null` which this evaluator does not implement, remain exactly as they were. That divergence is objectui#7750's question.
-
-Graded `minor` because a release reader can observe the narrowing on stored data; no declared type changed and the set of inputs the evaluator accepts has not widened.
diff --git a/.changeset/core-datascope-unknown-operator-denies-7378.md b/.changeset/core-datascope-unknown-operator-denies-7378.md
deleted file mode 100644
index b2f09e03f8..0000000000
--- a/.changeset/core-datascope-unknown-operator-denies-7378.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@object-ui/core': minor
----
-
-`DataScopeManager` now **denies** a row when a row-level scope rule carries an operator its evaluator does not implement. It used to **admit** the row.
-
-Behaviour change on a permission boundary, stated plainly. `evaluateFilter` implements nine operator spellings — `eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `in`, `nin`, `contains` — and its `default` arm returned `true`, so a stored `RowLevelFilter` carrying any other spelling passed every record the rule existed to hide, silently: no error, no console line, only a result set that was too large, which looks exactly like a correctly configured permissive scope. The arm now returns `false`, the answer `evaluateCondition` in `@object-ui/permissions` already gives from its own `default` arm. Because `applyFilters` ANDs a scope's rules, one unrecognised rule now denies every row in that scope.
-
-Who this reaches, measured on this release's base rather than assumed. The `RowLevelFilter['operator']` union is closed, so no TypeScript caller can write an unimplemented spelling, and no code in this repository constructs a `RowLevelFilter` outside the evaluator's own test. The path that changes is scope configuration read back from stored or hand-written JSON and handed to `setFilters` / `registerScopeWithConfig`, where the operator arrives as a plain string the type never checked. A deployment holding such a rule with a spelling outside the nine — including the spec's canonical `equals` / `not_equals` / `greater_than` / `starts_with` and the null-ness family `is_null` / `is_not_null`, none of which have an arm — sees fewer rows from that scope after upgrading, never more. Those spellings are not implemented here; they are refused instead of admitted. Whether to canonicalise them through the spec's `canonicalAstOperator` is left open on objectui#7378.
-
-Graded `minor` because a release reader can observe the narrowing on stored data; the declared type is unchanged and the set of spellings the evaluator accepts has not widened.
diff --git a/.changeset/create-plugin-in-repo-scaffold-note-7524.md b/.changeset/create-plugin-in-repo-scaffold-note-7524.md
deleted file mode 100644
index 466ad2c770..0000000000
--- a/.changeset/create-plugin-in-repo-scaffold-note-7524.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Comment-only change to `@object-ui/create-plugin`: `src/templates.ts` gains a docblock
-note recording that scaffolding into this monorepo is already refused by named pins
-(objectui#7524), and that the emitted per-package `test` block stays correct for the
-generator's actual audience. No generated file changes, so nothing is published.
diff --git a/.changeset/crud-guide-declarative-view-binding-5446.md b/.changeset/crud-guide-declarative-view-binding-5446.md
deleted file mode 100644
index 936ad20c97..0000000000
--- a/.changeset/crud-guide-declarative-view-binding-5446.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change: extended `packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx`
-to measure the `content/docs/guide/building-crud-app.md` rewrite for objectui#5446 (a
-docs-only fix — the guide now teaches the declarative `dataSource: { object, view }`
-binding instead of the `view` / `data.queryParams` keys `ObjectGrid` never read). No
-published package behaviour changes.
diff --git a/.changeset/current-user-default-prefill-5683.md b/.changeset/current-user-default-prefill-5683.md
deleted file mode 100644
index b519b0fb64..0000000000
--- a/.changeset/current-user-default-prefill-5683.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@object-ui/permissions': minor
-'@object-ui/plugin-form': minor
----
-
-Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned.
diff --git a/.changeset/cyan-donkeys-tickle.md b/.changeset/cyan-donkeys-tickle.md
deleted file mode 100644
index acd39b5025..0000000000
--- a/.changeset/cyan-donkeys-tickle.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Tooling only, no package released: port `scripts/check-entry-guard.mjs` from objectstack and wire it into `Lint`, so a `scripts/**` entry guard hand-typed with `process.argv[1]` cannot land. The 29 guards this tree already carries are baselined SHRINK-ONLY and converted separately (objectui#6092).
diff --git a/.changeset/dashboard-aria-member-retired-5830.md b/.changeset/dashboard-aria-member-retired-5830.md
deleted file mode 100644
index a104aca90f..0000000000
--- a/.changeset/dashboard-aria-member-retired-5830.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares
-the `aria` member (`{ ariaLabel?, ariaDescribedBy?, role? }`). Its doc comment
-claimed alignment with `@objectstack/spec AriaPropsSchema`, but the spec removed
-`dashboard.aria` at the #3896 audit close-out — `DashboardSchema.shape.aria` is
-a tombstone that refuses any value and tells authors to delete the key — and no
-dashboard renderer ever read `schema.aria` (objectui#5830).
-
-What an author loses is the **type-level suggestion** only: the key was already
-refused at parse (the Zod twin inherits the spec tombstone by reference), and
-`BaseSchema`'s index signature means an existing `aria:` line still compiles.
-There is **no runtime behaviour change** — the key never rendered, and stored
-documents carrying it already failed validation before this release.
diff --git a/.changeset/dashboard-config-aria-retired-5852.md b/.changeset/dashboard-config-aria-retired-5852.md
deleted file mode 100644
index 9ce2a42a1a..0000000000
--- a/.changeset/dashboard-config-aria-retired-5852.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Retired the designer-surface dashboard `aria` pair — `DashboardConfig.aria` and `DashboardConfigSchema.aria`** (objectui#5852).
-
-Both spellings are named verbatim above so a host can grep its own sources: the
-retired member is `aria`, on the TypeScript interface `DashboardConfig`
-(`@object-ui/types`, `designer.ts`) and on its Zod mirror `DashboardConfigSchema`
-(`@object-ui/types/zod`). It declared `{ label?: string; description?: string }`.
-
-**Why.** The spellings `label`/`description` match neither `@objectstack/spec`'s
-`AriaProps` vocabulary (`ariaLabel` / `ariaDescribedBy` / `role`) nor anything a
-renderer maps, so no read point could have consumed them even in principle.
-Re-measured on `main` at the retirement: zero `.aria` reads in
-`packages/plugin-designer/src`, `packages/plugin-dashboard/src` and
-`apps/console/src`; zero occurrences of either name anywhere in the `objectstack`
-repo; and `DashboardConfigPanel.tsx` — the panel the interface's own doc comment
-says it serves — imports neither name.
-
-**The two directions differ, and neither is a no-op:**
-
-- **TypeScript (a narrowed suggestion, not a compile break).** `DashboardConfig`
- carries a `[key: string]: any` catch-all, so an existing `aria:` line still
- compiles; what is gone is the editor suggestion and the false implication that
- the key was part of the contract.
-- **Zod (a behaviour change — read this one).** `aria` is now an ADR-0049
- retirement tombstone (`z.never().optional()`), following this package's
- existing convention. Previously an authored `aria` was **accepted and
- preserved** in `safeParse` output; it is now **refused by name**, with `aria`
- in the issue path and a message telling the author to delete the key. A plain
- deletion was deliberately not taken: `DashboardConfigSchema` is a bare
- `z.object` with no `.strict()`, so deleting the key would have made an
- authored `aria` **silently disappear** from the parsed output instead — a
- quiet data loss in place of a loud refusal.
-
-**External caveat.** In-repo consumer count is zero, but that is not the npm
-count: `@object-ui/types` is published, and stored dashboard configuration is
-not reachable from this repo. A host that authored `aria` on a `DashboardConfig`
-document will now see a validation error naming the key where it previously saw
-a silently carried value. The remedy is to delete the key — it never reached a
-renderer.
diff --git a/.changeset/dashboard-declare-honoured-inputs.md b/.changeset/dashboard-declare-honoured-inputs.md
deleted file mode 100644
index c0129d95e5..0000000000
--- a/.changeset/dashboard-declare-honoured-inputs.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/plugin-dashboard': minor
----
-
-`dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title.
diff --git a/.changeset/dashboard-expandable-family-convergence-5692.md b/.changeset/dashboard-expandable-family-convergence-5692.md
deleted file mode 100644
index b7fcc7a8cf..0000000000
--- a/.changeset/dashboard-expandable-family-convergence-5692.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`plugin-dashboard`'s two private copies of the reference-bearing field family now
-read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating
-it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside
-`computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the
-already-converged consumers carry, so a member-identical private copy fails
-rather than quietly re-forking the table.
-
-Two behaviour changes on the dashboard table's `$expand`, in opposite directions:
-
-- **A `tree` column is now expanded.** A self-referencing hierarchy field is
- reference-bearing and a member of the shared family, so its cell renders the
- parent record's display name instead of a bare id — the same treatment the form
- and grid roads already gave it.
-- **A `reference`-typed column is no longer expanded.** Measured before removing
- it: `reference` is absent from `@objectstack/spec`'s closed `FieldType`
- vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant
- object schema can declare a field whose stored type is `reference`. Dropping it
- is a no-op on real data; the spelling is a legacy dialect alias on the
- action-param surface, folded to `lookup` before any field-type data is read.
-
-`EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license
-widening a published shared set.
diff --git a/.changeset/dashboard-header-collapse-5812.md b/.changeset/dashboard-header-collapse-5812.md
deleted file mode 100644
index 9c0d11f887..0000000000
--- a/.changeset/dashboard-header-collapse-5812.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`DashboardRenderer` no longer emits an empty dashboard header wrapper. The
-wrapper used to render whenever `header` was declared, while each of its
-children — title, description, actions — was additionally suppressible. With
-the console page chrome present (`hideHeaderText`, set because the chrome
-already renders the dashboard's title and description) and no `header.actions`,
-every child evaluated falsy and the DOM still received
-`
`: zero children, yet a full grid row
-(measured 64px) plus `mb-4` of dead band above the filter bar, on every console
-dashboard page (objectui#5812, measured on HotCRM 17.1.0).
-
-The three children are now computed first and the wrapper renders only if one
-of them survives. Nothing else changes: a standalone embed (no chrome) renders
-title and description exactly as before, and declared `header.actions` keep the
-wrapper alive even under the chrome, since the chrome renders text only. Authors
-needed this — dropping `header` from the metadata to reclaim the pixels would
-have cost the standalone embed its title, which is what `header` is for.
diff --git a/.changeset/dashboard-readme-card-widget-7035.md b/.changeset/dashboard-readme-card-widget-7035.md
deleted file mode 100644
index b80bb90ba3..0000000000
--- a/.changeset/dashboard-readme-card-widget-7035.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
----
-
-Docs + test only, releases nothing.
-
-`packages/plugin-dashboard/README.md`'s "Dashboard with Charts" example taught two
-widgets spelled `type: 'card'` with a nested `body`. `'card'` is not a member of the
-widget vocabulary objectui#4600 closed, so the whole document was refused by
-`@object-ui/types/zod`'s `DashboardComponentSchema` — measured, not assumed. They are
-now the chart-family widgets the renderer actually dispatches (`line` / `pie` with
-inline rows under `options.data`), and a new test parses every dashboard example on
-that page through the shipped schema so the shape cannot come back unnoticed.
-
-No package source changed; the new file is a test.
diff --git a/.changeset/dashboard-self-contained-unreachable-arm-4620.md b/.changeset/dashboard-self-contained-unreachable-arm-4620.md
deleted file mode 100644
index 7d8e70c295..0000000000
--- a/.changeset/dashboard-self-contained-unreachable-arm-4620.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained
-(card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally
-(objectui#4620).
-
-`isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the
-`isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The
-`datasetBound` side of that inner fork could never execute: reaching it required
-`isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged —
-the removed arm never ran, and the reachable fork in the Card branch (the one that gives
-a dataset-bound metric its title and border chrome) is untouched.
-
-The cost was to readers, not to users: the shape read as "both branches handle
-dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto
-`DashboardGridLayout` had to pay for the reachability argument before it could decline to
-copy the dead limb. A comment now names the invariant in place so the arm is not re-added.
diff --git a/.changeset/dashboard-stable-empty-rows-4629.md b/.changeset/dashboard-stable-empty-rows-4629.md
deleted file mode 100644
index 9fc9806548..0000000000
--- a/.changeset/dashboard-stable-empty-rows-4629.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for
-"no rows yet" instead of a fresh array literal per render (objectui#4629).
-
-Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so
-whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind`
-path that resolves to an object — the fallback produced a NEW array identity on
-every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so
-every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the
-`isSystemField` pass, the `fieldLabel` lookups) and then discarded by the
-`finalData.length === 0` early return. In `ObjectPivotTable` the value is handed
-straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot
-rebuilt its row/column sets, bucket map and totals on every render over no rows
-at all.
-
-Nothing rendered wrong before or after; this is wasted work in the empty window,
-plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is
-the same module-scope frozen empty `data-table.tsx` adopted for its own
-`EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings.
diff --git a/.changeset/dashboard-table-header-one-spelling-5425.md b/.changeset/dashboard-table-header-one-spelling-5425.md
deleted file mode 100644
index 8a0de721b2..0000000000
--- a/.changeset/dashboard-table-header-one-spelling-5425.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does.
-
-`ObjectDataTable` derives headers on two paths — from the author's declared
-`columns`, and from the object schema when no columns were declared. The
-declared path (and the static `data-table` half of the same widget family)
-already used `humanizeFieldKey`, whose docstring names it "the single home for
-the convention, because both halves of the `table` widget family need it and
-they must agree". The auto-derived path carried a third, inline spelling that
-split camelCase but never turned `_` into a space, so it left a raw underscore
-on screen. Measured over the same object's columns:
-
-```
-path close_date needs_analysis
-object-bound, AUTO-DERIVED (before) Close_date Needs_analysis
-object-bound, AUTO-DERIVED (after) Close Date Needs Analysis
-object-bound, DECLARED columns Close Date Needs Analysis
-static `data-table`, no columns Close Date Needs Analysis
-```
-
-One dashboard can hold all three widgets over one object, so a single field key
-rendered under two spellings — the defect class objectui#5425 rules out. The odd
-path adopts the shared convention rather than the convention gaining a fourth
-dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and
-after — the coincidence that kept the snake_case divergence unnoticed), and a
-translated header still wins: only the fallback handed to `fieldLabel` changed.
-
-Dimension MEMBER labels are untouched by this. The same card reported dashboard
-members rendering a prettified enum instead of the picklist's translated label,
-measured on 17.1.0; re-measured on this branch it no longer reproduces — the
-analytics label net shipped in 17.5.0 routes every non-metric dataset dimension
-through the field's declared options and the locale bundle. That behaviour had
-no test stated in the card's terms and now has one, over the four dashboards the
-card measured, including the property that a bar axis and a pivot header cannot
-disagree about one stored value.
diff --git a/.changeset/dashboard-table-measure-desc-first-5845.md b/.changeset/dashboard-table-measure-desc-first-5845.md
deleted file mode 100644
index 6eb74642b6..0000000000
--- a/.changeset/dashboard-table-measure-desc-first-5845.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/plugin-dashboard': minor
----
-
-Dashboard `table` widget — a numeric **measure** column now sorts **descending
-on the first click** (objectui#5845).
-
-The sortable headers shipped in objectui#5827 started every column ascending,
-which left the card's own motivating complaint — the largest industry rendering
-last — two clicks away. For a measure in an analytics widget the question a
-click asks is "who is biggest", so that column now cycles
-**descending → ascending → the dataset's own order**.
-
-Unchanged: a **dimension** column, and a measure whose values are not numbers
-(a `min()`/`max()` over a text field), keep **ascending → descending → the
-dataset's own order** — the idiom the console's own DataTable uses. The
-first-click direction is decided by the same measure-and-all-values-are-numbers
-classification that decides right-alignment, so it follows the column's *role*
-rather than "it looks like digits": a digit-keyed dimension (a year, a quarter)
-still starts ascending.
-
-Blanks still sort **last in both directions** — that arm is now what a measure
-column's very first click runs — and `aria-sort` reports the direction actually
-applied.
diff --git a/.changeset/dashboard-table-widget-polish-5827.md b/.changeset/dashboard-table-widget-polish-5827.md
deleted file mode 100644
index 9714c2330f..0000000000
--- a/.changeset/dashboard-table-widget-polish-5827.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-dashboard': minor
----
-
-Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering
-fixes so it reads as a table rather than a raw dump (objectui#5827):
-
-- **Numeric columns right-align**, header and cells. `tabular-nums` was already
- applied to every cell and could not do its job while the digits were flushed
- left. A column qualifies when it is a MEASURE and every non-null value in it
- is a number — the declared column `type` cannot decide it, because the
- analytics executor stamps `type: 'number'` on every measure regardless of the
- aggregate, so a `min()`/`max()` over a text field would have been
- right-aligned on the strength of a label. Dimensions stay left-aligned even
- when their bucket keys are digits: a grouping axis is not a quantity.
-- **Sortable headers.** Each header is a keyboard-reachable button that cycles
- ascending → descending → back to the dataset's own order, with the console's
- existing sort indicator and `aria-sort` on the `th`. Blanks sort last in
- BOTH directions. The sort is client-side over the rows already fetched — the
- widget issues one `queryDataset` call and paginates nothing, so no round trip
- is involved.
-- **The empty-dimension bucket sorts last** in the default order instead of
- floating to the top. It keeps its `—` label; only its position moves.
-- **Row hover feedback on every row**, not only on a drillable one. A drillable
- row keeps its stronger accent fill.
-
-The CSV export now follows the order the table is showing, which is the same
-"the CSV is the table's data" convention its cell text already followed.
-Drill-through is unaffected: rows are reordered as `(row, incoming index)`
-pairs, so a drill still resolves through the server's parallel raw-value
-sidecar.
-
-Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and
-chart paths, and every non-dataset table in the console.
diff --git a/.changeset/dashboard-title-locale-writeback-5428.md b/.changeset/dashboard-title-locale-writeback-5428.md
deleted file mode 100644
index 849b85b703..0000000000
--- a/.changeset/dashboard-title-locale-writeback-5428.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/plugin-designer': patch
-'@object-ui/app-shell': patch
----
-
-A widget title stored as an inline per-locale map is editable again in both dashboard
-authoring surfaces, and a save writes back only the active locale's entry
-(objectui#5428).
-
-`@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at
-17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both
-authoring panels edit a title in ONE single-line input. Writing the input's value back
-as the whole value would collapse every other locale on the first keystroke, so both
-surfaces took the same conservative branch: show a map-valued title resolved, and make
-it READ-ONLY.
-
-That branch could not lose data, but it rested on a premise the spec had already
-invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was
-plain `string` through rc.5" — stated sixty lines below a comment in the same file
-documenting the rc.6 widening that makes a stored map reachable. Both could not hold.
-The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward
-was not protect an unreachable path: it denied an author the ability to edit a widget
-title in their own locale.
-
-objectui#5301's maintainer ruling settled the write rule for the sibling surface — a
-save replaces only the active locale's entry and preserves the others — and
-`@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because
-the read and the write have to agree. Both panels now adopt it:
-
-- `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel;
-- `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin.
-
-A plain-string title keeps saving as a plain string, so the common path is unchanged.
-An edit made in a locale the stored map does not carry ADDS an entry under that locale
-rather than overwriting the entry the display fell back to.
-
-The pins are preservation pins, not "the input is editable" pins: at both surfaces a
-keystroke on a map-valued title must leave every other locale's entry byte-identical.
-Reverse-verified by mutating each write back to the flattening form and confirming those
-assertions go red at both surfaces.
-
-Not a multi-locale editor: an author still reaches only the entry for the locale they
-are in. Authoring every locale from one panel remains an open product question. The
-stale deferrals both comments carried pointed at objectui#4163, which closed as
-completed on 2026-08-15 while the placeholders were still in the tree; they are replaced
-with the rule that is actually in force rather than re-pointed at another tracker.
diff --git a/.changeset/dashboard-widget-dataset-stale-cast-5067.md b/.changeset/dashboard-widget-dataset-stale-cast-5067.md
deleted file mode 100644
index ba87f15eca..0000000000
--- a/.changeset/dashboard-widget-dataset-stale-cast-5067.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
----
-
-Internal only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared.
-
-`DashboardRenderer.tsx`'s dataset-bound check (`const datasetBound = !!(widget as
-any).dataset`) drops its `as any` (objectui#5067). The comment's stated reason —
-"the bundled DashboardWidget type gains `dataset` only after objectui bumps
-`@objectstack/spec`" — is stale: the repo already carries `@objectstack/spec@17.0.0`,
-whose `DashboardWidget` declares `dataset`, and `packages/types/src/complex.ts`'s
-`DashboardWidgetSchema` inherits it through its `extends Omit,
-…>` (`dataset` is not in the `Omit` list). `widget.dataset` type-checks directly; the cast
-was pure redundant widening, and a harmful one — after `as any`, `.dataset` reads as
-`any`, so a future spec change to that key's shape would not go red here.
-
-**No runtime behaviour changes.** `!!(widget as any).dataset` and `!!widget.dataset`
-evaluate identically for every input; `as any` is a compile-time-only annotation. Proven
-with a reverse check: temporarily typo'ing the property to `widget.datasetTypoXYZ` turns
-`pnpm --filter @object-ui/plugin-dashboard type-check` red with `TS2339: Property
-'datasetTypoXYZ' does not exist on type 'DashboardWidgetSchema'`, confirming the removed
-cast was suppressing real type coverage rather than papering over a genuine gap.
-
-The ADR-0021 point the original comment made — a dataset-bound widget renders through
-the governed `queryDataset` path (`DatasetWidget`) instead of the inline object-aggregate
-schema — is unchanged and kept; only the now-false justification for the cast is rewritten.
diff --git a/.changeset/dashboard-widget-type-closed-enum.md b/.changeset/dashboard-widget-type-closed-enum.md
deleted file mode 100644
index 11f2c6bbf9..0000000000
--- a/.changeset/dashboard-widget-type-closed-enum.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/types": minor
-"@object-ui/plugin-designer": patch
-"@object-ui/app-shell": patch
----
-
-Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectui's own component extension.
-
-`DashboardWidgetSchema.type` was `string` on the TypeScript interface and `z.string()` in the Zod twin — an unbounded hatch. A typo'd family, a chart type the spec retired, and a component type nothing registers all type-checked and validated, surfacing only as the renderer's red `OBJUI-001` panel at runtime.
-
-It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum).
-
-Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one.
diff --git a/.changeset/data-table-header-declared-key-5351.md b/.changeset/data-table-header-declared-key-5351.md
deleted file mode 100644
index c0b154de2d..0000000000
--- a/.changeset/data-table-header-declared-key-5351.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/components': minor
-'@object-ui/plugin-dashboard': minor
-'@object-ui/plugin-detail': minor
----
-
-`data-table` reads the declared `header`; the producers translate `label` into it.
-
-`TableColumn` declares `header: string` and does not declare `label`. The
-renderer's column normalization nonetheless read `header: col.header || col.label`,
-so the same key had one spelling the type admits and one only the runtime did.
-That alias is gone (objectui#5351), and the translation it used to perform happens
-once at each producer instead: metadata vocabulary in, adapter vocabulary out.
-
-**This narrows what `data-table` accepts, so read this if you author `data-table`
-nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a
-directly authored `data-table` now renders a **headerless** column over live
-cells. Spell it `header` — the key `TableColumn` has always declared. Columns
-reaching `data-table` through `object-data-table`, `object-grid` or a related
-list are unaffected: those producers resolve `header` for you from the spec's
-`ListColumnSchema.label`, so every spelling they accepted before they still
-accept.
-
-`@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader
-producers use to cross that boundary. It is adapter-first (`header` wins over
-`label`), so an author who addressed the table directly is never overwritten.
-
-`object-data-table` also gains a fix from the same move: a column carrying a
-`label` used to render a **blank** header there even while the alias existed,
-because the widget's field-meta enrichment overwrote the authored `label` before
-the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage".
-
-The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here
-and still resolves. Retiring it is objectui#5120's remaining step, which is
-gated on two published skill guides that teach that spelling.
diff --git a/.changeset/date-range-default-range-binding-4984.md b/.changeset/date-range-default-range-binding-4984.md
deleted file mode 100644
index 60e7298530..0000000000
--- a/.changeset/date-range-default-range-binding-4984.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`DashboardComponentSchema.dateRange.defaultRange` is now bound to
-`DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a
-hand-written 14-member union (objectui#4984).
-
-The union was byte-faithful to the spec — all 14 members, same order — so nothing
-a user hits changes today. What was missing is the tie that keeps it faithful:
-`resolveDashboardFilterDefs` takes `Pick`, so this union is what typechecks every TS-constructed dashboard, and
-a preset the spec ADDS would have been a legal document that objectui's own types
-said could not exist — the "narrower than the contract it implements" shape whose
-consequence in objectui#4163 was that the bad reads were invisible to `tsc`.
-
-No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union
-on an interface member has no symbol to collide with, while rule 2's claim heuristic
-was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes
-the file's existing "Aligned with @objectstack/spec" comment structural rather than
-prose.
-
-The emitted `.d.ts` collapses the inline union to the imported alias; the published
-type surface is unchanged — measured with the TypeScript checker over the emitted
-declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`,
-and `defaultRange` resolving to the same 14 string-literal members before and after).
diff --git a/.changeset/dead-metadata-converters-6224.md b/.changeset/dead-metadata-converters-6224.md
deleted file mode 100644
index 63c5a43b3d..0000000000
--- a/.changeset/dead-metadata-converters-6224.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Delete the dead `src/utils/metadataConverters.ts` module. It had zero importers, was not
-re-exported from the package barrel, and no `exports` subpath reached it — `toObjectDefinition`,
-`toFieldDefinition`, `MetadataObject` and `MetadataField` were never part of the published
-surface, so nothing external can break. The module was removed rather than left alone because it
-carried two patterns the live code no longer uses: a name-heuristic `isSystem` (the server's real
-`system` flag is the source of truth) and a three-way `referenceTo` tolerance for a target the
-spec spells `reference`. A dead copy that disagrees with the live one is what a future author
-copies from.
diff --git a/.changeset/dead-refresh-callback-objectview.md b/.changeset/dead-refresh-callback-objectview.md
deleted file mode 100644
index 6f077fa36d..0000000000
--- a/.changeset/dead-refresh-callback-objectview.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
----
-
-Internal only, no released behaviour change: `plugin-view`'s `ObjectView` no
-longer declares a `handleRefresh` callback that nothing referenced.
-
-The callback bumped `refreshKey` but was never passed to the toolbar, exposed on
-a handle, or wired to any control — it advertised a refresh entry point the
-component does not have, which cost objectui#4549 a detour to rule out. The
-reachable refresh paths are unchanged (`onMutation` auto-subscribe, delete, bulk
-delete, form success), and the real toolbar Refresh button continues to live in
-`plugin-list`'s `ListView`, reached through `renderListView`.
diff --git a/.changeset/default-children-retired-5051.md b/.changeset/default-children-retired-5051.md
deleted file mode 100644
index 0605f03d1c..0000000000
--- a/.changeset/default-children-retired-5051.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/core': minor
-'@object-ui/components': minor
----
-
-The register-meta key `defaultChildren` is retired (objectui#5051).
-
-It was declared in four places, produced in eleven, and read in **none**. The designer's
-drop path builds a new node from its twin key only — `PageDesigner.tsx`,
-`props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item
-that declared `defaultChildren` dropped an **empty** node and the declared children never
-materialised. Nothing rendered the wrong thing; an entire declaration surface was simply
-inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer
-ruling of 2026-08-19, the key is removed rather than wired up; if designer
-default-children UX is ever product-wanted it returns as its own designed card.
-
-**If you author plugins against the published register-meta table, drop the key.** It is
-gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A
-meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`,
-and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the
-key is silently dropped from the parse output instead of failing validation. TypeScript
-authors get the loud signal instead: all three `ComponentMeta` declarations
-(`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no
-longer offer it, so re-declaring it is now a compile error.
-
-**No runtime behaviour changes in either direction.** No code path read the key before
-this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were
-feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types
-into the designer produces exactly the node it produced yesterday.
-
-Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key
-is absent from the parse output, with a surviving sibling asserted present through the
-same parse as the control) plus the two TS twins with `@ts-expect-error`, and
-`packages/core` pins the registration surface the eleven producers were written against.
-Both are compile-time-enforced through each package's chained `tsconfig.test.json`.
diff --git a/.changeset/default-view-agenda-retired-5784.md b/.changeset/default-view-agenda-retired-5784.md
deleted file mode 100644
index ae59ebf296..0000000000
--- a/.changeset/default-view-agenda-retired-5784.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`'agenda'` leaves `defaultView` on all three of its declaration faces
-(objectui#5784 — the `defaultView` sibling of objectui#5740's retirement on
-`CalendarViewSchema.view`; ADR-0049 enforce-or-remove): the
-`ObjectCalendarSchema` TS interface (an inline union `#5740`'s
-`CalendarViewMode` narrowing could not reach), the zod `ObjectCalendarSchema`,
-and the list-view `calendar` config's objectui-only `defaultView` extension.
-All three are now `['month', 'week', 'day']`.
-
-The declarations admitted a value nothing enforced: `ObjectCalendar`'s props
-declare `defaultView?: 'month' | 'week' | 'day'`, its schema read casts to the
-same three values, and `CalendarView` renders no agenda view. An author
-writing the type-legal, zod-valid `defaultView: 'agenda'` on an
-`object-calendar` node or in a list view's `calendar` config got a month
-calendar with no error or warning. The spec side already agrees:
-`@objectstack/spec`'s `ObjectCalendarProps.defaultView` is
-`['month', 'week', 'day']`. No in-repo, example, or catalog app authors
-`defaultView: 'agenda'` (measured for objectui#5784 with positive controls,
-including the objectstack tree — its only `agenda` token is the Agenda
-job-scheduler library).
-
-**This narrows the accept set: an author who writes `defaultView: 'agenda'`
-will now be refused at validation.** `defaultView` is a declared key, and
-declared keys are validated even under `.passthrough()`, so
-`defaultView: 'agenda'` is a **validation error that previously parsed
-green** (an `invalid_value` issue on the `defaultView` /
-`calendar.defaultView` path, offering `month`/`week`/`day`). Undeclared keys
-still pass through unchanged. Breaking on the published zod surface; ships as
-`minor` per this repo's version-alignment policy (majors track
-`@objectstack`).
-
-The runtime boundary is unchanged: `ObjectCalendar` still resolves an
-off-union raw `defaultView` away to its `'month'` default. Docblocks and both
-zod `describe` strings now teach the three-value set.
diff --git a/.changeset/defaults-maps-mirror-discovery-7884.md b/.changeset/defaults-maps-mirror-discovery-7884.md
deleted file mode 100644
index 3e2c7c8dc8..0000000000
--- a/.changeset/defaults-maps-mirror-discovery-7884.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only change (objectui#7884): the `defaults-maps-mirror-en-pack` gate's rule "every
-row names a key the en pack actually defines" now runs over the `createSafeTranslation`
-defaults tables discovered from source, instead of a hand-written list of three imported
-maps that judged 400 of 1056 rows. The AST walk objectui#3512 already had moved into
-`@object-ui/test-support` (private, never published) so both gates share one definition of
-the population rather than growing a second traversal. No published behaviour changes.
diff --git a/.changeset/degenerate-magnitude-charts.md b/.changeset/degenerate-magnitude-charts.md
deleted file mode 100644
index 77c597f168..0000000000
--- a/.changeset/degenerate-magnitude-charts.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-Pie, donut, funnel and treemap now say when rows carry no magnitude they can draw.
-
-These four families size a mark BY its measure, so a row whose value is zero,
-negative, `null` or unparseable stays in the data and is given no area. Measured
-in Chromium across 74 tiles: an all-zero pie put ZERO non-white pixels on the
-page while its DOM carried 31 descendants and a real `svg`; a treemap handed
-`40 / null`, `40 / 0` or `40 / -25 / -12` rendered one full-bleed leaf that was
-byte-identical to a genuinely one-row treemap; and a funnel handed `40` beside a
-`null` drew no segments at all and labelled the tile with the row that had no
-value.
-
-When no row can be sized, these charts now render the file's refusal shell
-(`no-positive-magnitude`) instead of a blank tile. When only some rows can be
-sized, the chart still draws and carries a note counting the ones it could not.
-All-positive charts, charts handed no rows at all, bar charts, and both sankey
-answers are unchanged.
diff --git a/.changeset/degrade-set-twin-pin-5880.md b/.changeset/degrade-set-twin-pin-5880.md
deleted file mode 100644
index d87ccf0015..0000000000
--- a/.changeset/degrade-set-twin-pin-5880.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only change: pins the target-required degrade set (`{lookup, master_detail}`) equal
-across `@object-ui/app-shell`'s `paramToField` and `@object-ui/plugin-grid`'s
-`bulkParamToField`, which were two independent statements of one rule with nothing
-mechanical holding them together. No published behaviour changes, no new exported symbol,
-and neither set's membership was touched — the pin derives each package's effective set
-through its own adapter and asserts the contents equal.
diff --git a/.changeset/delete-dead-release-workflow-5405.md b/.changeset/delete-dead-release-workflow-5405.md
deleted file mode 100644
index b1c00f27a1..0000000000
--- a/.changeset/delete-dead-release-workflow-5405.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
----
-
-CI/docs only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared.
-
-Deletes `.github/workflows/release.yml`, a workflow that never ran once in this
-repository's history, and removes the two places on
-`content/docs/guide/ci-cd-pipeline.md` that documented it as a release path.
-
-It triggered on `push: tags: ['v*']`. No tag matching that glob has ever existed here:
-of the 2896 tags on the remote, every one is a Changesets per-package tag
-(`@object-ui/@`) 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/designer-field-key-parity-gate.md b/.changeset/designer-field-key-parity-gate.md
deleted file mode 100644
index af9f06da57..0000000000
--- a/.changeset/designer-field-key-parity-gate.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
----
-
-Tooling and tests only; no published behaviour changes.
-
-Adds `check:designer-field-key-parity`, a gate that compares the field designers'
-statically declared payload shapes (`FieldMetadataPayload`, `ServerFieldSchema`,
-`DesignerFieldDefinition`) against the accept set of the installed
-`@objectstack/spec` `FieldSchema`, plus the draft-I/O round-trip half as a test in
-`@object-ui/app-shell`. Both are new checks over existing code — nothing shipped
-in a package changed, and no offending key was fixed: each one the gate surfaced is
-filed as its own card and recorded in the gate's ledger.
diff --git a/.changeset/designer-publish-package-binding.md b/.changeset/designer-publish-package-binding.md
deleted file mode 100644
index 4bba3134eb..0000000000
--- a/.changeset/designer-publish-package-binding.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/data-objectstack': minor
-'@object-ui/app-shell': minor
----
-
-The metadata designer states its package on the publish step, not only on the save (#5420)
-
-Studio's designer save→publish loop bound the draft to a software package on the
-save (`PUT ?mode=draft&package=`) and then sealed it with a publish that named
-no package at all. `objectstack#10354` (shipped in `@objectstack/rest` 17.2.0) taught
-`POST /meta/:type/:name/publish` to accept `?package=`, so the second call can now
-state the same binding the first one already states.
-
-- `MetadataClient.publish()` accepts `packageId` and sends `?package=`, the same
- wire spelling and the same `encodeURIComponent` treatment `save()` gives it.
-- `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation
- (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The
- `?package=all` "show everything" scope keeps folding to "no package".
-
-The parameter is **omitted**, never sent empty, when the designer holds no binding.
-Empty and absent are the same to the framework's normaliser today, but absent is the
-shape the save door already followed, and the framework's promotion path branches on
-the key being present downstream.
-
-What this buys is **reachability**, not speed: it lets `#9612`'s package-closure
-narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That
-narrowing has a second, independent gate this does not touch — objects carrying no
-`_packageId` provenance are kept unconditionally — so on a tenant-authored overlay
-corpus stating the package still narrows nothing.
diff --git a/.changeset/dom-leak-sweep-components-5574.md b/.changeset/dom-leak-sweep-components-5574.md
deleted file mode 100644
index ce389acf86..0000000000
--- a/.changeset/dom-leak-sweep-components-5574.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
----
-
-Tests only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. The one file changed is
-`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`, which
-`packages/app-shell/tsconfig.json` excludes from the build program
-(`"exclude": [… "**/*.test.tsx"]`).
-
-Measured rather than asserted, because emitted-output behaviour varies per package:
-`packages/app-shell/dist` was built twice from a cleared `dist/` **and** a cleared
-`tsconfig.tsbuildinfo` — once at the `origin/main` version of that file, once at this
-branch's version — and sha256-compared. 862 emitted files, 431 of them `.d.ts`, every
-hash equal. (The first attempt at this measurement read as an empty `dist/`: `tsc` is
-`composite`, so with the build info left in place it skipped the emit entirely and
-produced no output to compare. Clearing the build info is part of the measurement,
-not a detail.)
-
-Widens the objectui#3291 DOM-leak canary sweep to `packages/components/src/renderers/**`.
-That family — 158 registry-reachable types across five namespaces — was outside the
-gate's discovery entirely, which is why `ui:grid`'s leak had to be found by hand. The
-first run records a ledger: 119 of 158 targets leak, in eight measured shapes, every
-renderer named. Nothing is skipped or allow-listed; the per-target assertion stays exact
-set equality in both directions, so a renderer fix cannot go green until its ledger row
-is deleted in the same change.
diff --git a/.changeset/dom-leak-sweep-fixture-depth-5630.md b/.changeset/dom-leak-sweep-fixture-depth-5630.md
deleted file mode 100644
index d471f6afd3..0000000000
--- a/.changeset/dom-leak-sweep-fixture-depth-5630.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only: deepen the `element:definition-list` / `element:repeater` /
-`element:metadata_viewer` fixtures in the DOM-leak sweep
-(`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`) past their
-empty-state placeholder branch, so their clean reading covers the real,
-populated markup instead. No renderer source changed and no published
-behaviour changes.
diff --git a/.changeset/draft-changes-panel-eager-attribution-comment.md b/.changeset/draft-changes-panel-eager-attribution-comment.md
deleted file mode 100644
index e3d23fb00c..0000000000
--- a/.changeset/draft-changes-panel-eager-attribution-comment.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Comment-only correction in `packages/app-shell/src/preview/DraftChangesPanel.tsx`. The block above the `/meta` spelling fold stated that this file's `@objectstack/spec/shared` import puts that subpath on the console's eager graph, and attached a byte figure and a prohibition to that claim. Ablation on `origin/main` shows the import is worth ~+285 bytes gzipped: the subpath is held eager by a runtime import inside `@objectstack/core`, upstream of this repo. The comment now records the mechanism and points at the upstream card. No source behaviour is touched and no published behaviour changes.
diff --git a/.changeset/drawer-bucket-pointer-ruling.md b/.changeset/drawer-bucket-pointer-ruling.md
deleted file mode 100644
index 8b3aaa773c..0000000000
--- a/.changeset/drawer-bucket-pointer-ruling.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
----
-
-Comment text only, zero-pixel: the record-drawer `size: 'lg'` bucket question is
-recorded as ruled, and the three in-code pointers stop naming closed cards as its
-open home.
-
-`ObjectKanban`, `ObjectCalendar` and `ObjectGantt` each carried a comment saying
-the bucket question "stays open" on objectui#6303 / objectui#6259. Both cards had
-closed, so the pointer chain terminated on closed cards and the question it
-pointed at was tracked nowhere. It has since been ruled: objectui#6584,
-2026-08-27 — the record-navigation drawer stays on the CSS literal, with no
-convergence on the `size: 'lg'` bucket, because that bucket (`min(92vw, 960px)`)
-is up to 53% wider below a 1600px viewport and there was no demand for the
-change.
-
-The three comments now cite objectui#6584 and state the ruling and its date. The
-measurement prose they carry (`min(92vw, 960px)` vs `min(960px, 60vw)`) is kept
-verbatim — it is still true and it is the reason the ruling went the way it did.
-`RecordDetailDrawer`'s `width` docblock, the single code home of the literal that
-all three renderers fall through to, gains the same note so the record lives at
-the line a future editor would actually change.
-
-No published behaviour changes: no default, `width` expression, `size` prop or
-runtime value is touched.
diff --git a/.changeset/dts-build-exit-code-5370.md b/.changeset/dts-build-exit-code-5370.md
deleted file mode 100644
index b6fe0bc6dc..0000000000
--- a/.changeset/dts-build-exit-code-5370.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Build tooling only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` is touched: `@object-ui/layout`'s `vite build`
-now exits non-zero when the declaration step reports type errors, instead of printing them
-and exiting 0 (objectui#5370). The typings and the JavaScript it emits are unchanged.
diff --git a/.changeset/dts-explicit-extensions-5365.md b/.changeset/dts-explicit-extensions-5365.md
deleted file mode 100644
index 73de8c9d34..0000000000
--- a/.changeset/dts-explicit-extensions-5365.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/components': patch
-'@object-ui/layout': patch
----
-
-The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them.
-
-`vite-plugin-dts` emits one declaration file per source file, and TypeScript
-copies a module specifier into the declaration verbatim. `export * from './ui'`
-therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in
-`@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted
-trees. Node16/NodeNext resolution does not extension-search a relative
-specifier, so the compiler could follow none of the hops and every symbol they
-carried read as absent from the package:
-
-```
-error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'.
-```
-
-Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls
-in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from
-`layout`), plus 215 TS7006 as fallout from the imports that stopped resolving.
-On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now.
-
-The emitted `.js` never had the defect — rolldown resolves the same specifier
-away — which is why `pnpm check:esm-specifiers`, whose verdict is about
-specifier-preserving `.js` builds, correctly never scanned either package. The
-fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`,
-shared by both `vite.config.ts` files), not in the sources: the same source line
-produces a clean `.js` and a broken `.d.ts`, so no source edit can express the
-difference. The rewriter resolves each specifier against the source tree the
-output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` —
-throws on anything it cannot resolve, and after the build re-parses the emitted
-declarations to assert every relative specifier both carries an extension and
-names a file the build really emitted.
-
-`packages/fields` takes the `nodenext` pin as a result — the same two lines
-`packages/react` has carried since objectui#4538 — so the property is enforced by
-the compiler on the consumer side rather than by review. `packages/app-shell`
-does not: it type-checks clean without the pin and still shows 23 errors with it,
-none of them from these two packages. That residue is filed separately.
diff --git a/.changeset/eager-closure-budget-5324.md b/.changeset/eager-closure-budget-5324.md
deleted file mode 100644
index 230d596709..0000000000
--- a/.changeset/eager-closure-budget-5324.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
----
-
-CI and build tooling only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared. No package `src/` is touched.
-
-The console "performance budget" now weighs the **eager closure** instead of one chunk.
-
-`.github/workflows/performance-budget.yml` gzipped `apps/console/dist/assets/index-*.js`
-and compared it against a 350 KB line. Measured on `77f846a8b`, that chunk is 25,910 bytes
-gzipped, while the closure it statically pulls in — every chunk the browser must fetch and
-parse before the app renders — is 3,881,609 bytes across 58 of 507 chunks. The gate passed
-on 0.67% of the payload it claimed to govern, and `advancedChunks` routes vendor and
-workspace code into named chunks on purpose, so most regressions land outside the entry
-chunk. objectui#5266 is the worked example: 89 KiB gzipped added to every page load, landing
-in `vendor-objectstack-*.js`, structurally invisible here (objectui#5324).
-
-`emitEagerClosureReport` in `apps/console/vite.config.ts` walks rolldown's own
-`chunk.imports` from the entry chunks — static edges only, because the dynamic edge is the
-lazy boundary — gzips the bytes actually written to disk, and writes
-`dist/eager-closure.json`. `scripts/check-eager-closure-budget.mjs` applies the ceiling.
-The split is deliberate: a size ceiling enforced inside `vite build` would fail every
-Vercel preview and every local build, which is how a budget gets switched off rather than
-fixed. Exit codes are distinct — `1` over budget (a verdict about the bundle), `2` no
-trustworthy measurement (a verdict about the gauge) — so a broken gauge is never reported
-as a clean bundle, and vice versa.
-
-Every check in that path is a counter-probe, because this gate's failure mode is silent: a
-walk that finds too little, a stale report, an absent field read as zero all produce a
-SMALL number, and a budget reads a small number as good news. So the build refuses to
-publish a figure unless `react-dom` is inside the closure and at least one chunk is outside
-it, and the checker refuses a report whose totals disagree with its own chunk list, whose
-version it does not recognise, or that has collapsed to its entry chunk — that last one
-being precisely the gauge this replaces.
-
-The ceiling is 3,960,000 gzipped bytes: today's measurement plus 78,391 bytes of headroom.
-It passes on current `main`, and the headroom is deliberately narrower than the 89 KiB
-regression the gate exists to catch, so a repeat of objectui#5266 fails it (verified: the
-baseline plus 89 KiB comes out 12.4 KB over). Both constraints are asserted in
-`scripts/__tests__/check-eager-closure-budget.test.ts`, not merely argued in a comment.
-
-This is a truthful current-state ceiling, not a target. 3.79 MB gzipped before first render
-is a bad payload and the honest long-term line is far below it; lowering it is a separate
-decision with its own work behind it. The entry-chunk budget and its 350 KB line are
-unchanged — replacing a blind gauge is not licence to drop the check that was already there.
diff --git a/.changeset/eighty-pandas-listen.md b/.changeset/eighty-pandas-listen.md
deleted file mode 100644
index 9278e33bd8..0000000000
--- a/.changeset/eighty-pandas-listen.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: `ObjectView`'s two `contractEnvelope` pins (objectui#6726, objectui#6840) now assert on `deliveredThrough(asData)` directly instead of calling it from inside a `waitFor` predicate, and the helper waits on `find()`'s own settled answer before reading the last delivery. No published behaviour changes.
diff --git a/.changeset/esm-load-comment-mask-5382.md b/.changeset/esm-load-comment-mask-5382.md
deleted file mode 100644
index 70abee6d08..0000000000
--- a/.changeset/esm-load-comment-mask-5382.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
----
-
-CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` is touched.
-
-Fixes the comment mask in `scripts/check-node-esm-load.mjs` (objectui#5382). Leg 1 of
-the gate blanked comments out of every source with two ordered regexes and then matched
-specifiers in the result. The block-comment pass ran first and had no notion of already
-being inside a `//` line, so a slash-star sequence occurring in ordinary line-comment
-prose — a package glob, a path pattern, a wildcard import, all of them ordinary here —
-opened a comment that ran to the next closing delimiter anywhere in the file and blanked
-every line between, live code included.
-
-Re-measured on `main` at 478ec54ce over the 805 emitted sources of the 13
-specifier-preserving packages: the mask found 2132 relative specifiers and the TypeScript
-parser found 2133. The one it could not see is a real `import` in
-`packages/app-shell/src/preview/DraftChangesPanel.tsx`, hidden by a line comment naming
-the `@objectstack` chunk group by glob two lines above it.
-
-That direction is the bad one. Since `SPECIFIER_DEBT` emptied, leg 1 is a hard
-requirement rather than a ratchet, so a blind spot in it is somewhere a regression can
-sit permanently while the run reports clean and only the nightly load leg can see the
-consequence.
-
-Leg 1 now reads each file exactly as written and takes its module edges from
-`check-phantom-dependencies.mjs`'s shared TypeScript scanner — the same one
-`check-package-self-import.mjs` uses, so three gates cannot drift apart on what a module
-edge is. Comments, strings, template literals and regex literals stop being questions
-this gate has an opinion about. Reported line numbers stay the compiler's: the specifier
-literal is located inside the statement the parser already identified, which matters
-because `tsc` reports this class at the specifier and the statement opens on a different
-line for 255 of 2066 relative specifiers here.
-
-The gate's verdict is unchanged — 0 findings before, 0 findings after — because the
-newly visible import already carries its `.js` extension. What changed is that it is now
-visible. The whole cheap leg went from 0.71s to 2.43s.
-
-`readTsconfig()` in the same script strips comments with the same kind of
-context-unaware regex and is a separate live instance of this class. It is deliberately
-untouched here and remains open as objectui#5367.
diff --git a/.changeset/evaluateexpression-jsdoc-links-5580.md b/.changeset/evaluateexpression-jsdoc-links-5580.md
deleted file mode 100644
index d8167b1959..0000000000
--- a/.changeset/evaluateexpression-jsdoc-links-5580.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@object-ui/core': patch
----
-
-Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction`
-JSDoc are now qualified, so each resolves to the entity it means (objectui#5580).
-
-`ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method
-on `ExpressionEvaluator` (bare expression, throws) and the module-level export
-(context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block
-referred to both under the one spelling, four lines apart.
-
-The prose link was not merely ambiguous, it was bound wrong. Measured with
-`checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}`
-resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the
-sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}`
-binds to the method, but only because no module-level `evaluate` exists to outrank
-it, so the rule "an unqualified link resolves to the enclosing class's member" does
-not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`,
-which the checker resolves to the `MethodDeclaration`.
-
-The `@example`'s final line is the module-level export — its second parameter is a
-context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat
-two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover
-carries no import to disambiguate. It now names the module-level export and shows the
-import it needs.
-
-This is prose only: the diff is confined to a block comment and no declaration moves.
-It is scored `patch` rather than the empty-frontmatter form because the block is
-emitted into what npm ships — measured, this edit moves both
-`dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js`
-(this package builds with a bare `tsc`, which preserves comments in the JS emit), and
-the ten changed lines in that JS are all comment lines.
-
-`registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather
-than asserting it in prose, since a `{@link}` that binds to the wrong entity is
-indistinguishable in source from one that binds right.
diff --git a/.changeset/filecell-error-slot-5431.md b/.changeset/filecell-error-slot-5431.md
deleted file mode 100644
index c6a890b5e6..0000000000
--- a/.changeset/filecell-error-slot-5431.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/fields': minor
----
-
-**API addition (public-surface widening):** `FileCell` — the compact upload
-control `@object-ui/fields` exports for line-item grid cells — gains the
-published optional `error?: string` slot, mirroring `LookupField` and
-`FileField`: the same validation slot `@objectstack/spec/ui`'s
-`FieldWidgetPropsSchema` declares and `FieldWidgetComponentProps` names
-(objectui#3222). When set, `FileCell` puts `aria-invalid` on its own focusable
-picker button; the message text stays with the host (objectui#5431).
-
-`GridField` now passes that slot for a required-but-empty `file` cell — the one
-cell type objectui#3318's per-cell `aria-invalid` delivery left out. Before
-this, a required `file` cell flagged only the visual ring and `title` on the
-`td`; no element in the cell subtree announced the state, so assistive tech was
-told nothing (a wrapper-only mark is exactly what objectui#5223 forbids). Text,
-number, select, and lookup cells were wired in PR #5429; `file` cells now
-behave identically.
diff --git a/.changeset/fix-parity-aria-line-ref.md b/.changeset/fix-parity-aria-line-ref.md
deleted file mode 100644
index 9197bb525c..0000000000
--- a/.changeset/fix-parity-aria-line-ref.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change to the parity gate's `aria` exemption comment; no published behaviour changes. Fixes a stale `file:line` reference to `packages/plugin-detail/src/index.tsx` (was `:335-337`, now `:554-556`) in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`.
diff --git a/.changeset/flow-entry-condition-builder.md b/.changeset/flow-entry-condition-builder.md
deleted file mode 100644
index 52c43a38dc..0000000000
--- a/.changeset/flow-entry-condition-builder.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-flow designer: the Start node's **Entry condition** now opens in the row-based
-condition builder, with raw CEL one click away as the escape hatch (objectui#6226).
-
-The builder is given the trigger record's own vocabulary — bare field names and
-`previous.`, resolved by `flow-scope.ts` — rather than the record-scoped
-default its five other consumers use, so the subjects it offers are the spelling
-this surface evaluates and teaches. 15 of the 17 entry conditions shipped in the
-example apps now open as structured rows; the remaining two mix `&&` with a
-parenthesised `||` group, which is a grammar limit of the row model and keeps
-them on raw mode by design.
-
-Compiled output is unchanged: the same CEL the runtime already evaluates, in the
-author's own quoting. The legacy `criteria` key, a schedule/manual/webhook
-trigger (which binds no record), and every other `expression`-kind flow field
-keep the single-line input they render today — the builder is opt-in per field
-descriptor and requires a declared vocabulary, never inferred.
diff --git a/.changeset/flow-resume-result-5417.md b/.changeset/flow-resume-result-5417.md
deleted file mode 100644
index 5de2804350..0000000000
--- a/.changeset/flow-resume-result-5417.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-A screen flow's resume result reaches the user — on both outcomes (objectui#5417).
-
-A dogfood walkthrough reported that a refused `resume` and a successful one
-"render identically: the dialog closes and the page is unchanged", leaving no
-gesture that distinguishes "created" from "rejected". Re-measured against `main`
-before any change, one half of that was already fixed — `interpretFlowResponse`
-reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its
-prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors
-after the version the report was measured on. There was no interpreter bug and
-no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were
-real, and they are what changed:
-
-- **A terminal failure no longer closes the dialog.** The reason it closed is
- unchanged and is not reversed: on a `FLOW_FAILED` the engine has already
- consumed the suspension, so a resubmit can only reach "No suspended run" and
- must not be offered. Closing was one way to withhold that dead retry and the
- expensive one — the user had just typed a form they could no longer see, and
- the engine's sentence names a value that left the screen with it. The dialog
- now stays open with the submit affordance withdrawn: the flat footer swaps
- Submit for Close, and an `object-form` step drops its Save (which also stops a
- second click from duplicating the record it had already persisted).
-- **The refusal has a second, non-expiring carrier.** The toast stays — it is
- viewport-fixed, so it still reaches a user scrolled past a tall step's header
- — and an inline destructive `Alert` (`role="alert"`) now holds the same
- sentence inside the dialog, beside the values that produced it. A retryable
- refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before,
- and its banner clears as soon as the user starts editing.
-- **A successful run invalidates what the flow WROTE, not just what the user is
- looking at.** Both hosts answered `onComplete` with
- `notifyDataChanged({ objectName: })`, so a flow that
- created a quote from an Opportunity page never told the related list that
- would now contain it — the record did not appear until a manual reload. The
- runner cannot know which objects a flow touched, so it emits
- `{ objectName: '*' }`: the same scope, for the same stated reason, that the
- record page's manual ⟳ already uses. Everything mounted refetches in place
- over the invalidation bus, with no remount.
-
-The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded
-English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`,
-`nextStep`, `completed`) in all ten packs, plus reuse of
-`common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The
-server's own refusal sentence is still passed through untranslated — it is prose
-the automation engine composed for a human, not copy with a key.
diff --git a/.changeset/form-control-renderers-dom-props-5632.md b/.changeset/form-control-renderers-dom-props-5632.md
deleted file mode 100644
index c13febb1a2..0000000000
--- a/.changeset/form-control-renderers-dom-props-5632.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/components': patch
----
-
-**Behaviour change:** the eighteen renderers whose host element is a form
-control no longer forward their whole prop bag to it. They route it through a
-form-control DOM declaration — the same `pickDomProps` mechanism `grid`, `flex`,
-`stack`, `container` and `text` were converged on — so an authored schema key
-becomes an HTML attribute only if the contract declares it one (objectui#5632,
-the `BARE_SPREAD_MINUS_NAME` slice of objectui#5574).
-
-The eighteen: `action:button`, `action:icon`, `ui:button`, `ui:checkbox`,
-`ui:combobox`, `ui:date-picker`, `ui:email`, `ui:file-upload`, `ui:input`,
-`ui:input-otp`, `ui:password`, `ui:radio-group`, `ui:sidebar-menu-button`,
-`ui:slider`, `ui:sonner`, `ui:switch`, `ui:textarea`, `ui:toggle`.
-
-What this stops reaching the DOM: the renderer's own declared props, consumed
-off `schema` to render the control AND spread onto the element a second time as
-attributes HTML does not define, plus the authored node's SDUI metadata and the
-flattened `props` container. Measured across `examples/schema-catalog`, rendered
-through the real `SchemaRenderer`: 284 illegitimate attributes over 287
-form-control nodes — `button[label]` 140, `button[icon]` 25, `input[inputtype]`
-23, `input[label]` 19, `toggle[label]` 14, `radio-group[options]` 8,
-`date-picker[placeholder]` 7, `file-upload[label]` 7, `file-upload[buttontext]`
-7, `checkbox[label]` 6, `textarea[label]` 6, `toggle[arialabel]` 6,
-`switch[label]` 4, `password[label]` 3, `email[label]` 2, `radio-group[direction]`
-2, and six singletons. The same probe reads 0 after, with `grid`'s 26 nodes at 0
-both times as the control and an unchanged node census across the two runs.
-
-`name` and `disabled` are DELIBERATELY still forwarded. Both are legal on a form
-control and neither was ever part of the leak — the ledgered shape for this
-group is thirteen attributes, not fourteen, precisely because HTML defines
-`name` on these hosts. The whitelist this group uses is therefore the shared
-SDUI one PLUS those two, not the bare `toDomProps` the container renderers take:
-that would have stripped the form-serialization key off every control and
-silently re-enabled every disabled one, and no gate in the repo would have gone
-red for it.
-
-Nothing an author writes renders differently: every leaked key was already being
-read off `schema` and applied. `id`, `role`, `tabIndex`, `className`, `style`,
-event handlers and the open `data-*` / `aria-*` families are unchanged.
-
-Anything that read one of the leaked attributes off the DOM — a CSS attribute
-selector such as `[label="Save"]`, or a test asserting `inputtype` on a rendered
-`ui:input` — must read the schema instead. No `@object-ui` code did; this is
-called out because the attributes were externally visible while they lasted.
diff --git a/.changeset/form-designer-namespaced-field-type-4838.md b/.changeset/form-designer-namespaced-field-type-4838.md
deleted file mode 100644
index 88b03f1e62..0000000000
--- a/.changeset/form-designer-namespaced-field-type-4838.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio's form designer canvas now emits the namespaced `FormField.type` spelling
-instead of passing `objectDef.fields[x].type` through raw (objectui#4838).
-
-Per the maintainer ruling on that card, a bare spec-type name (`markdown`,
-`html`, `richtext`, …) is **not** a legal `FormField.type` — one widget, one
-legal spelling, the namespaced widget id. `ObjectFormDesigner` was the measured
-producer of the bare spelling: it handed a raw object-metadata type straight to
-`isWideFieldType`, a helper whose vocabulary is `FormField.type`. It now
-normalizes through `mapFieldTypeToFormType`, the one place that widget decision
-is made.
-
-User-visible effect: a `repeater` field is finally laid out full-row on the
-canvas, matching the runtime form. `repeater` is a spec `FieldType` that
-resolves to the wide `field:grid` widget, but bare `repeater` is not one of
-`WIDE_FIELD_TYPES`' bare members, so the raw pass matched nothing — the canvas
-showed it at normal width while the real form spanned it. Fields whose spec name
-doubles as a widget id (`textarea`, `markdown`, `html`, `richtext`, `grid`) are
-unaffected; they matched under both spellings.
-
-The two tolerant consumers this makes look redundant are deliberately left
-alone, each scheduled under its own follow-up with deprecation care: the
-`field:`-prefix fallback in `renderFieldComponent`, and `WIDE_FIELD_TYPES`' dual
-spellings. Removing a tolerance is the consumer-tightening half, and other
-producers have not been normalized yet.
diff --git a/.changeset/form-field-predicate-current-user-6010.md b/.changeset/form-field-predicate-current-user-6010.md
deleted file mode 100644
index 6946763e4b..0000000000
--- a/.changeset/form-field-predicate-current-user-6010.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/components': minor
----
-
-**Behaviour change.** Form section and field `visibleWhen` predicates that silently failed
-OPEN now actually evaluate — and a rule that resolves FALSE now hides the field
-(objectui#6010).
-
-`current_user` was bound on two of the three `visibleWhen` surfaces and not the third. A
-page component or app/nav gate got it (`ExpressionProvider` → `SchemaRenderer`), and a
-per-option gate got it (`resolveCascadingOptions(…, predicateScope)`), but every
-`resolveFieldRuleState` call in the form renderer passed `undefined` for the scope
-argument, so a form SECTION or FIELD predicate saw `record` and `previous` and nothing
-else. `'sales_manager' in current_user.positions` therefore named an **unbound root**
-there, and the visibility fallback is fail-open — so the gate did not hide the field from
-the people it named, it **showed the field to everyone**, with no signal beyond one
-deduped `console.warn`.
-
-**What changes for you, in the direction that matters:** if you authored a form-field or
-form-section `visibleWhen` naming `current_user`, saw the field render, and concluded the
-rule was permissive — it was not permissive, it was broken, and it is now enforced. That
-same field will now **hide** for every user the predicate resolves FALSE for. Audit any
-`visibleWhen` on a form field or section that references `current_user` / `user` /
-`ctx.user` / `os.user` before upgrading; a predicate that was quietly inert becomes live.
-
-Two things deliberately do **not** change:
-
-- **A genuinely unbound root still fails open.** A predicate the engine cannot evaluate at
- all still logs one warning and leaves the element visible. Only *evaluated-and-false*
- hides. `visibleWhen` remains presentation, not access control — use field-level security
- or RLS to stop someone reading something.
-- **The deprecated `visibleOn` alias** on a form field now binds the same scope, because
- ADR-0089 D2 folds it into `visibleWhen` at parse; binding one scope for the canonical
- spelling and another for its alias would have reproduced the same defect one spelling
- over. The synthesised legacy `condition: { field, equals }` predicate is unaffected — it
- is generated from a structured object and can only ever name `record.`.
-
-This restores the contract both ADRs already declared: ADR-0068 D1 — *"a predicate authored
-against any one form evaluates identically"* — and ADR-0089 D1 — *"runtime record surfaces
-bind `record` + `current_user`"*. All five surfaces are now pinned against one authored
-predicate text in
-`packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx`,
-so the next divergence is loud instead of silent.
diff --git a/.changeset/form-reset-notification-channels-5235.md b/.changeset/form-reset-notification-channels-5235.md
deleted file mode 100644
index 6a2d2c3f14..0000000000
--- a/.changeset/form-reset-notification-channels-5235.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/components': patch
----
-
-The form renderer now keeps a `defaultValues` reset off `onChange` and off the
-`form_change` `onAction` for **every** caller — including one that memoizes the
-callback (objectui#5235).
-
-"A record landing is not a user edit" was already this file's documented,
-pinned behaviour, but two of the three channels delivered it by accident of
-React's effect ordering: every layout DESTROY runs before any layout CREATE, so
-a caller passing a fresh callback each render had its value subscription torn
-down before the reset and re-established after. The guarantee was therefore
-delivered by the callback's *identity changing*. Wrap the same callback in
-`React.useCallback` — taught everywhere as a semantically neutral performance
-optimization — and the identity stays put, the effect never re-runs, the
-subscription survives the reset, and the whole loaded record comes back to the
-host as if the user had typed it: the false "the user edited this" signal
-objectui#2968 was filed about, in a form no type, doc or call site warned about.
-
-The reset now states what those two channels report, the way `onDirtyChange`
-already did (it computes its payload against the freshly installed baseline and
-calls the host outright). Callers passing inline arrows see byte-identical
-behaviour; callers who memoize stop receiving a phantom edit.
-
-Not a contract change: whether a value channel *should* report a programmatic
-reset stays open in objectui#5235. This only removes the answer's dependence on
-caller identity.
diff --git a/.changeset/formatpercent-comment-citations-4596.md b/.changeset/formatpercent-comment-citations-4596.md
deleted file mode 100644
index 6e68a460df..0000000000
--- a/.changeset/formatpercent-comment-citations-4596.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
----
-
-Comments only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared (objectui#4596).
-
-Why the empty form is the right declaration here: both changed comments are **internal
-reasoning**, not consumer-visible API documentation. One sits inside `formatMeasure`'s
-body; the other is a test-file header. No released behaviour changes, and no declaration
-changes — `dist/utils/dataset-format.d.ts` is byte-identical across this change
-(measured: sha `be5f5938…`, 10,580 bytes on both sides), so nothing a consumer types
-against or reads on hover moves.
-
-Stated honestly, because an earlier draft of this note got it wrong: the shipped
-JavaScript **does** change. `tsconfig.base.json` sets `removeComments: false`
-deliberately and `@object-ui/core` builds with a plain `tsc`, so a body comment is
-emitted — `dist/utils/dataset-format.js` goes from 14,716 to 15,457 bytes, and it is the
-only one of the package's 180 dist files that moves. The test file is excluded from the
-build program (`src/**/__tests__/**`) and never reaches `dist` at all. Bytes moving is
-not the criterion; released behaviour and consumer-visible surface are, and neither does.
-
-Three comments cited `formatPercent` as the live example of the divide-by-100 percent
-route. Each was accurate when written and stopped being true when objectui#4590 landed:
-`formatPercent` renders through `style: 'percentPoints'` with no division. The comments
-now argue the route on its own merits without the expired citation, and record what
-replaced it — no caller in this repo takes the divide-by-100 route today. `formatPercent`
-was the last one; the two remaining `style: 'percent'` sites hand `Intl` a FRACTION, which
-is that style's own contract, and the route otherwise survives only where a test builds it
-in order to show it disagreeing.
-
-The measured argument underneath is unchanged, and the tie / extreme-magnitude pins that
-keep the percentage-points route honest are untouched. The `27,581 of 1,200,013` figure
-now names the grid it came from — objectui#4576's tie-dense grid, 0.005 steps to 2,000,
-precisions 0/1/2, on `formatMeasure`'s call shape — so it stops reading as a discrepancy
-against objectui#4590's `27,577 of 1,200,003`, which is the same grid re-measured through
-`formatPercent`.
diff --git a/.changeset/formfieldspec-dependson-5040.md b/.changeset/formfieldspec-dependson-5040.md
deleted file mode 100644
index 183a8fc65c..0000000000
--- a/.changeset/formfieldspec-dependson-5040.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-metadata-admin: `FormFieldSpec` declares `dependsOn`, and the widget half reads
-the same declaration instead of its own copy of it (objectui#5040)
-
-`FormFieldSpec` — the authoring type for a metadata-admin form layout, the
-element type of `FormSectionSpec.fields[]` — did not declare `dependsOn`.
-`widgets.tsx` held a second, inline description of the same object as
-`WidgetProps.fieldSpec`, and that one did, because two registered widgets read
-it as their primary configuration: `field-selector` resolves
-`dependsOn || reference || 'objectName'` to decide whose field catalog to
-offer, and `dynamic-config` uses it to pick a sub-schema out of
-`WidgetContext.dynamicSchemas`. One value travelling down one channel,
-described twice, disagreeing on the one key that decides what those widgets
-show — so
-
-```ts
-{ field: 'fields', widget: 'field-selector', dependsOn: 'objectName' }
-```
-
-the only configuration that makes `field-selector` work, was a `TS2353` for
-anyone who typed their spec. It survived because in-repo specs reach the form
-through `as any` / loose types, so the authoring type was never asked.
-
-No runtime behaviour changes: `MetadataField` already handed `dependsOn`
-through and both widgets already read it. What changes is the type face — it
-now admits what the runtime has always accepted. The two descriptions are one
-declaration, extracted to a leaf module
-(`views/metadata-admin/form-spec.ts`) that both halves import, because
-`SchemaForm.tsx` imports `./widgets.js` and a back-edge would close a cycle.
-`SchemaForm.tsx` re-exports `FormFieldSpec` and `VisibilityPredicate`, so every
-existing importer is unaffected.
-
-`dependsOn` is `string | string[]` here, deliberately **not**
-`@object-ui/types`' wider canonical `DependsOnInput`, which also admits
-`{ field, param }` objects: both readers index `[0]` and use the result as a
-field name, so the wider shape would be a type that lies. Converging the two is
-its own decision, pinned as a refusal rather than taken silently.
diff --git a/.changeset/formpage-conditional-rule-surfaces-5627.md b/.changeset/formpage-conditional-rule-surfaces-5627.md
deleted file mode 100644
index 6f9028e014..0000000000
--- a/.changeset/formpage-conditional-rule-surfaces-5627.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/console': patch
----
-
-`FormPage` — the console's own form renderer, serving both the public `/f/:slug`
-route and the internal `/forms/:name` one — now honours the three conditional-rule
-surfaces it still dropped after objectui#5594: section-level `visibleWhen` /
-`visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` /
-`requiredWhen` (objectui#5627).
-
-This is the second form renderer in the repo, and it honoured exactly one of the
-four surfaces the sibling chain does. A section an author conditioned away rendered
-in full — heading and every control — on both routes including the anonymous one.
-The object-level half was worse than fail-open hiding: `readonly` was whatever the
-static flag said, so a field a `readonlyWhen` should have locked stayed editable and
-paired with the server's fail-closed unbound-scope behaviour into "the user edits,
-the save reports success, and the value never lands".
-
-Both halves evaluate through the SHARED machinery rather than a fourth consumer-side
-copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three
-field rules — which brings the settled rulings with it, including the `serverOwnedValue`
-carve-out that keeps a create form from requiring a producer-owned control (#4069 /
-#4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first
-`visibleWhen ?? visibleOn` read every sibling reader spells.
-
-Visibility stays a RENDERING rule at both granularities: a hidden section's fields
-still submit their values, exactly as a hidden field's have since #5594.
diff --git a/.changeset/formpage-maxlength-override-5595.md b/.changeset/formpage-maxlength-override-5595.md
deleted file mode 100644
index 337dd53ada..0000000000
--- a/.changeset/formpage-maxlength-override-5595.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/console': patch
----
-
-`buildSections` now honours a FormView field's `maxLength` override instead of always
-taking the object's ceiling (objectui#5595).
-
-The function merges a form's field overrides with the target object's field definitions,
-and its own docstring states the rule: *"Field-level FormField overrides take precedence
-over object defaults."* Every key in the loop is built that way — `override.label ??
-def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder`
-— except one, which read `def.maxLength` unconditionally. So an author who set a tighter
-per-form limit (a short public intake form over a column whose object-level ceiling is
-generous) got the generous one.
-
-The failure was silent in the worst direction: no diagnostic, no warning, and the form
-still submits, so the symptom is a value the author believed the input refused being
-accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two
-`maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"`
-arm.
-
-`override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys,
-so an explicitly declared `0` stays a value the author wrote rather than falling through
-to the column's ceiling. This narrows only what the input allows; the object's storage
-ceiling still decides at submit time, so nothing that was accepted before is now rejected
-anywhere but at the keyboard.
-
-Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all
-until objectui#5542, so no one typing a spec in this app could write the override in the
-first place, and the inert merge branch was never exercised. #5542 converged that type
-onto the shared app-shell declaration, which does declare the key — making the gap
-expressible, and therefore findable.
-
-The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in
-`FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than
-assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered
-evidence for this fix, and it is what made the gap findable in the first place, so it
-keeps its place and names the honoured answer.
diff --git a/.changeset/formpage-record-id-param-comment-4319.md b/.changeset/formpage-record-id-param-comment-4319.md
deleted file mode 100644
index d20c6fa828..0000000000
--- a/.changeset/formpage-record-id-param-comment-4319.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Comment-only repair in `apps/console`'s `FormPage`: the `FORM_RECORD_ID_PARAM`
-docblock no longer borrows `createdRecordPath.ts` as a second witness for
-`@object-ui/app-shell`'s root-barrel unreachability. That sibling case was
-resolved when the host-app resolver was published from the package root, so a
-reader following the cross-reference found the opposite of what it promised.
-The surviving justification is unchanged and still true — the root barrel does
-not re-export `./urlParams` — and now states that fact directly. No published
-behaviour changes.
diff --git a/.changeset/gantt-navconfig-comment-once.md b/.changeset/gantt-navconfig-comment-once.md
deleted file mode 100644
index 0390f51d96..0000000000
--- a/.changeset/gantt-navconfig-comment-once.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
----
-
-`ObjectGantt` internal cleanup, measured as a zero-pixel change:
-
-- The comment block above the `navConfig` default repeated its own last two
- lines verbatim, leaving a mid-sentence fragment. The sentence is now stated
- once, in full.
-- The drawer default no longer spells the spec-deprecated `width`
- (`@deprecated [#2578 -> size]`). It is now `{ mode: 'drawer' }`, so
- `resolveOverlayWidth` returns `undefined` and `RecordDetailDrawer`'s own
- `width` default supplies the identical `min(960px, 60vw)`. The resolved
- overlay width is unchanged on every viewport, and is now pinned by a test.
-
-No published behaviour changes.
diff --git a/.changeset/gantt-owns-data-comment-7222.md b/.changeset/gantt-owns-data-comment-7222.md
deleted file mode 100644
index d45e927904..0000000000
--- a/.changeset/gantt-owns-data-comment-7222.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Comment-only correction in `@object-ui/plugin-list` (objectui#7222): the note above `ganttOwnsData` in `ListView.tsx` claimed the rows `data` prop "short-circuits the renderer's own fetch". It does not — no host prop reaches `ObjectGantt` at all, because the registered `object-gantt` renderer forwards nothing but `schema`. No published behaviour changes.
diff --git a/.changeset/gantt-restated-keep-rationale.md b/.changeset/gantt-restated-keep-rationale.md
deleted file mode 100644
index 671cfb83ba..0000000000
--- a/.changeset/gantt-restated-keep-rationale.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Comment-only change in `@object-ui/plugin-gantt`: no published behaviour, no
-declaration and no type changes. `GanttConfigRestated`'s docblock in
-`ObjectGantt.tsx` said its members are kept for ONE reason — their JSDoc being
-the only prose in this repo for what the renderer does with each key. Measured
-on `main` that was false in two ways: four of the twelve members carry no JSDoc
-there (`parentField`, `baselineEndField`, `assigneeField`, `effortField`), and
-`ObjectGanttSchema` in `@object-ui/types` has documented all twelve since
-objectui#6472. The docblock now states both keep-reasons — the prose, and being
-an operand of `ObjectGantt.configPin.test.ts` — so the deletability test a
-reader applies to `parentField` arrives at "keep".
diff --git a/.changeset/gantt-row-walk-nontotal-reads.md b/.changeset/gantt-row-walk-nontotal-reads.md
deleted file mode 100644
index 17a9e7786b..0000000000
--- a/.changeset/gantt-row-walk-nontotal-reads.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Documentation and pinned tests only; no published behaviour changes. `plugin-timeline`'s gantt row walk now carries the measured enumeration of its non-total reads — six in `findUnusableGanttDate`, three in `calculateDateRange` — in place of the prose count that was short, and the adversarial set exercises both classes instead of asserting them.
diff --git a/.changeset/gantt-viewmode-declared-both-branches-5074.md b/.changeset/gantt-viewmode-declared-both-branches-5074.md
deleted file mode 100644
index 650da3c57f..0000000000
--- a/.changeset/gantt-viewmode-declared-both-branches-5074.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/plugin-gantt': minor
----
-
-**`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both
-gantt renderer branches honour it** (objectui#5074, maintainer ruling
-2026-08-19: declare-and-wire; the spec half landed first upstream).
-
-- `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`,
- DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode`
- enum by reference, so the member list cannot drift. Deliberately no
- default: an omitted `viewMode` keeps letting a persisted layout
- (`persistLayoutKey`) seed the timeline granularity before the renderer's
- `'day'` fallback.
-- The timeline branch (`GanttView`) now receives an authored `viewMode`.
- Previously only the resource-workload branch (`resourceView` +
- `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt
- view was silently ignored.
-- The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both
- branches read the declared `ganttConfig.viewMode`, which also honours the
- key when authored inside the spec's `gantt` config block.
-- Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value
- (e.g. `viewMode: 'hour'`) becomes a zod validation error where it
- previously passed through unvalidated. Values on the published spec enum
- are unaffected.
diff --git a/.changeset/gate-listlocalinstalls-marketplace-5620.md b/.changeset/gate-listlocalinstalls-marketplace-5620.md
deleted file mode 100644
index cb6c4165c2..0000000000
--- a/.changeset/gate-listlocalinstalls-marketplace-5620.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`MarketplacePackagePage`'s third fetch effect — `listLocalInstalls` — is now gated on
-`marketplaceEnabled` and `isAdmin`, the same two predicates the page's other two fetch
-effects already check, in addition to (not instead of) its existing
-`features.installLocal` check (objectui#5620).
-
-`listLocalInstalls`'s only consumer is `localInstalls.find(...)` in the content branch,
-which is unreachable whenever the page has already returned `MarketplaceDisabled` (no
-marketplace on this runtime) or `MarketplaceAccessDenied` (a refused viewer) — both
-decided ahead of the content branch since objectui#5533 and objectui#5583. Before this
-fix, a runtime with `features.installLocal: true` still fired the request — and
-discarded its answer — on a marketplace-off runtime and for a non-admin, the same
-wasted-round-trip class objectui#5533 established the fix for on this page, on the flag
-that card was not about.
-
-`features.installLocal` remains its own axis: a runtime can mount a local kernel install
-path with no marketplace proxy at all, so the fix adds the two predicates as a
-conjunction rather than replacing the existing check. The request still fires exactly
-when it did before AND the viewer would actually see its answer.
diff --git a/.changeset/governed-surface-human-merge-5149.md b/.changeset/governed-surface-human-merge-5149.md
deleted file mode 100644
index 7d63234c48..0000000000
--- a/.changeset/governed-surface-human-merge-5149.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Instruction files only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared. `AGENTS.md` records the governed surface
-(`AGENTS.md`, `CLAUDE.md`, `.claude/**`, `docs/adr/**`) and the rule that agent seats
-leave a PR touching it in draft for a human merge.
diff --git a/.changeset/grid-column-spelling-docs-5352.md b/.changeset/grid-column-spelling-docs-5352.md
deleted file mode 100644
index d80cdfb618..0000000000
--- a/.changeset/grid-column-spelling-docs-5352.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
----
-
-Docs and skills only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared. No package `src/` is touched, so no
-`@object-ui/*` package changes behaviour and there is nothing here for a consumer
-to upgrade to.
-
-Corrects the two published corpora that taught `object-grid` columns in a `name`
-spelling `ObjectGrid` does not read. `ListColumnSchema` (`@objectstack/spec/ui`) is a
-strict object whose column-identity key is `field`; `{ "name": ... }` is refused by
-name (`unrecognized_keys: ["name"]`) and, at runtime, contributes no column.
-
-- `content/docs/api/schema-reference.md` — the `ObjectGridSchema` example authored a
- MIXED array (four bare strings followed by one column object). Two defects in one
- array: the object entry spelled `name`, and mixing forms is itself unsupported —
- `normalizeColumns` dispatches the whole array on `columns[0]`, so a column object
- standing behind a bare string is dropped whatever it spells. Renaming the key alone
- does not fix it; the example is now uniformly `ListColumn` objects. The `columns`
- row of the property table now names `field` and states the no-mixing rule.
-- `skills/objectui/guides/page-builder.md` — the grid example's three columns were
- all-object in the `name` spelling, so the grid rendered its row-number column and no
- data columns at all. Now spelled `field`. A note was added between the grid and form
- examples, which sit adjacent and mean the OPPOSITE thing by the same pair of words:
- `ListColumn.field` names the object field a column shows, while `FormField.name`
- names the field a form input writes. That adjacency is the documented cause of this
- defect family (`packages/core/src/utils/column-identity.ts`).
-
-The adjacent `object-form` example is unchanged and was never wrong — `FormField.name`
-is that layer's real key.
diff --git a/.changeset/grid-default-filters-lowering-4082.md b/.changeset/grid-default-filters-lowering-4082.md
deleted file mode 100644
index 1fa03310a2..0000000000
--- a/.changeset/grid-default-filters-lowering-4082.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-"@object-ui/plugin-grid": patch
----
-
-`ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of
-byte-copying it onto `$filter` (objectui#4082).
-
-The query assembly already lowered the canonical `filter` key through `toFilterNode` —
-the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch
-beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the
-one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter`
-and `plugin-view`'s non-grid fetch both already route the same value through
-`toFilterNode` / `mergeFilterNodes`.
-
-Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is
-declared `Record` (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 1dc2d35067..0000000000
--- a/.changeset/grid-dom-attribute-whitelist-4787.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/components': patch
----
-
-The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer
-land on the rendered `` as invalid HTML attributes (objectui#4787).
-
-`grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and
-`style`, so everything else `SchemaRenderer` hands a registered component reached the
-element. Measured on a canary node, eight attributes leaked:
-`columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`,
-`props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an
-unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as
-`columns="[object Object]"`. Layout was unaffected, so every catalog grid example
-rendered with them — the reason this went unnoticed.
-
-The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist
-objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the
-SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`,
-`role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the
-designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be
-forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly
-any more, including keys `GridSchema` does not have yet; enumerating today's keys to
-strip would have re-rotted on the next schema addition.
-
-No authored input changes and no layout changes: the grid's own vocabulary was always
-read off `schema`, never off these props.
diff --git a/.changeset/grid-prefix-read-no-cast-6458.md b/.changeset/grid-prefix-read-no-cast-6458.md
deleted file mode 100644
index d40529d32e..0000000000
--- a/.changeset/grid-prefix-read-no-cast-6458.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Internal type hygiene in `ObjectGrid`'s column resolver: the authored `prefix`
-column key is now read directly instead of through `(col as any)`, since
-`ListColumn` declares it. No published behaviour changes — the cast erased at
-runtime, so the same property was read before and after; what changes is that
-`ColumnPrefix`'s typing now reaches the prefix cell renderer, and a new pin
-(`columnReadBoundary-6458.test.ts`) refuses any future cast read of a key the
-spec's `ListColumnSchema` declares.
diff --git a/.changeset/grid-select-published-fields.md b/.changeset/grid-select-published-fields.md
deleted file mode 100644
index 932e1c1a56..0000000000
--- a/.changeset/grid-select-published-fields.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio 的记录网格只请求服务端真有的列——「添加字段」不再把整个网格打成「该视图的查询被拒绝」
-
-在 Data 支柱点一下「添加字段」,整片网格立刻变成错误态,并建议「清除筛选条件」——而现场根本没有筛选条件。
-
-根因是投影的来源错了。`gridColumns` 取的是**草稿**对象的字段名,而这个数组是下游取数的输入;`addField` 只往本地草稿推一个 `field_
`。于是列一变就重新取数,`select` 里带着服务端不存在的列,data API 按设计拒绝——它的错误信息还专门解释了为什么不能静默丢弃未知列:那会把窄投影悄悄答成宽投影。
-
-修法的边界是**实测**出来的,不是猜的:把字段存成草稿返回 200 且 `state=draft`,紧接着的 `select` 指名它**仍然** 400。物化发生在**发布**时,所以「有没有保存」是错的问题,「服务端有没有」才是——答案在 `layered().effective` 这条基线里。
-
-因此新增 `publishedFieldNames`(加载时取自基线),并让 `gridColumns` 只保留其中存在的列。新字段照常在右侧检查器里被选中和配置(那本来就是配置它的地方),发布之后它可查询了,才作为列出现在网格里。
-
-过滤放在列数组这一处、而不是取数那一侧,是为了让「网格要什么」只有一个真相源。
-
-回归测试 `DataPillar.gridProjection.test.tsx` 断言在**交给对象视图的列数组**上——那个数组就是投影本身;如果只监视取数调用,日后把数组在下游改一手也能过,缺陷就悄悄搬回来了。撤掉这一行过滤,第二条精确转红、第一条仍绿。
diff --git a/.changeset/grid-unresolved-column-diagnostic-5349.md b/.changeset/grid-unresolved-column-diagnostic-5349.md
deleted file mode 100644
index 43d1c5a7d1..0000000000
--- a/.changeset/grid-unresolved-column-diagnostic-5349.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-`ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence.
-
-objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so
-`ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer
-reads. That was right — the spec refuses `accessorKey` and `header` by name, and the
-census found zero authored usages. But it relocated a failure mode instead of removing
-it: a column authored in a spelling the renderer does not read contributed nothing, and
-nothing said so. No error, no warning, no empty state — the author got a grid with its
-row-number column and no data columns, which is a success receipt for a disagreement
-between the renderer and the author.
-
-An authored column that can never resolve now emits one `console.warn` naming the
-address rather than the symptom: which block (`object-grid` or the `view:grid` alias),
-which object and label, which `columns[i]`, the keys that entry actually carries, and the
-rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }`
-the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped
-it" (the export-format warning), rather than adding a second differently-shaped one.
-
-Rendering is unchanged in every case: this is additive. The diagnostic reads the
-`columns` input and nothing else — it never asks whether the grid found rows, because
-`object-grid` legitimately draws them from five different places (a bare `data` array,
-`data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and
-passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView`
-does). All five are pinned by test, in both directions. A `hidden: true` column is
-authored intent and is never reported, and so are the arms that legitimately produce no
-columns of their own: no `columns` key, an empty `columns` array, and the `string[]`
-spelling.
-
-A throw was rejected: a grid that renders nothing today would become a page that renders
-nothing.
diff --git a/.changeset/grid-user-actions-collision-5240.md b/.changeset/grid-user-actions-collision-5240.md
deleted file mode 100644
index 8f7c79a354..0000000000
--- a/.changeset/grid-user-actions-collision-5240.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-`object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it.
-
-`userActions` names two different blocks. On a **view** it is toolbar policy —
-the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`,
-`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by
-name. On an **object** it is the CRUD-predicate block (`edit` / `delete` /
-`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is
-the only shape `listViewPredicates` can read, since its loop skips every
-non-object value.
-
-`ObjectGrid` read the key view-first when building the `$select` projection
-(`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a
-perfectly legal toolbar block therefore shadowed the object's CRUD predicates,
-the harvest found none, and the predicate's operand left the projection. CEL then
-faults on the absent key, fails closed, and the row Edit/Delete button disappears
-for everyone with nothing pointing at the projection — objectui#3501's failure,
-reached with a success receipt at every step.
-
-The view-level block is not hypothetical: `SpecBridge.transformListView` copies
-it onto the `object-grid` node the renderer receives, and `app-shell`'s
-`ObjectView` builds one unconditionally.
-
-The harvest now reads the resolved object block only. Both `userActions` read
-sites carry a comment naming the collision, and
-`__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes,
-the producer that writes the view one, the harvest's blindness to it, and the
-projection that must keep the object's operand with a toolbar block present.
-
-Toolbar policy itself is untouched — it was never read through this path.
diff --git a/.changeset/grid-widget-aria-invalid-3318.md b/.changeset/grid-widget-aria-invalid-3318.md
deleted file mode 100644
index e644f0adf7..0000000000
--- a/.changeset/grid-widget-aria-invalid-3318.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-Grid field widget: announce a form-level validation failure to assistive tech.
-
-A required `grid` submitted while still empty rendered its "is required" message
-but marked nothing — every row was a ghost row, and ghost rows were skipped by
-the widget's per-cell validity channel. A sighted user saw the red message; a
-screen-reader user was told nothing at all.
-
-The host failure now drives the per-cell channel the widget already owns: when
-the `error` slot is set on an empty grid, the ghost entry row's required cells
-flag, and the mark sits on each cell's own control rather than on the `td`
-wrapper (a `td` is not focusable, and assistive tech reads validity from the
-control). Populated grids are unaffected — they already marked their own empty
-required cells inline.
diff --git a/.changeset/hidden-predicate-widen-7455.md b/.changeset/hidden-predicate-widen-7455.md
deleted file mode 100644
index 9db73d7124..0000000000
--- a/.changeset/hidden-predicate-widen-7455.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**`BaseSchema.hidden` now declares the predicate string the renderer already evaluates** (objectui#7455, maintainer ruling 2026-09-03).
-
-`hidden?: boolean` becomes `hidden?: boolean | string`, and the Zod mirror's `z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible` (#4581) and `disabled` (#4580 ruling Q3-A) on both faces. `hidden` was the third key on the same evaluated path and the only one still declared boolean-only.
-
-This is a **widening**, not a replacement: every boolean `hidden` keeps parsing and keeps type-checking unchanged, and the renderer's behaviour is untouched by this change — `SchemaRenderer`'s `shouldHide` chain already routed this key through `hasDeclaredPredicate` and evaluated it, which is the evidence the widening rests on. What changes is that authors and their tooling can now write `hidden: "${data.status === 'draft'}"` without casting past the declaration, and the Zod mirror stops refusing it (before this, that value failed `safeParse` with `invalid_type` at path `hidden` while the identical string on `visible` parsed).
-
-`hiddenOn` is unchanged and remains the sibling expression spelling. The CEL envelope object form is still declared on none of `visible` / `hidden` / `disabled`; objectui#7530 rules on all three together.
-
-Per this repository's version-alignment convention, a widening of a published type surface ships as `minor` with the semantics spelled out here rather than as `major` (see AGENTS.md, "版本号策略").
diff --git a/.changeset/hollow-view-overlay-hydration-pin-5773.md b/.changeset/hollow-view-overlay-hydration-pin-5773.md
deleted file mode 100644
index a389f4ca47..0000000000
--- a/.changeset/hollow-view-overlay-hydration-pin-5773.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
----
-
-Tests only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` is touched; the only file added is
-`packages/app-shell/src/views/InterfaceListPage.hollowOverlayHydration.test.tsx`.
-
-objectui#5773 — pins the intended post-objectui#5233 behaviour at the seam the card
-named: `InterfaceListPage`'s hollow-view hydration effect merging a personalization
-overlay row it fetched through `listViewOverrides`/`getView` (the same undocked-narrowing
-read path `narrowPersonalizationOverlay`'s docblock in `packages/data-objectstack/src/index.ts`
-describes).
-
-Reachability was measured, not reasoned about — constructed through the REAL
-`ObjectStackAdapter` write (`updateViewConfig`/`buildPersistedViewBody`, the same seam
-`ObjectView.overlayPatchOnly.test.ts` uses) and the REAL `InterfaceListPage` render, not a
-hand-written override fixture. Two cases:
-
-- A hollow ADR-0017 expansion item (system view, no `columns`) plus the CURRENT
- (post-#5233) thin overlay row a toolbar toggle writes today: the page renders
- `defaultColumnsFromObject`'s defaults — reachable, and the intended behaviour per the
- card's disposition (an overlay was never a legitimate source of a view body).
-- The same hollow view with a PRE-#5233 fat overlay row (the shape an install that has
- not touched this view since before the fix is still carrying, per
- `ObjectView.overlayPatchOnly.test.ts`'s own "PRE-FIX" framing): the page renders the
- frozen stale columns instead — a control proving the pin above discriminates a real
- hydration-effect outcome rather than passing vacuously.
-
-Confirmed load-bearing by reverse verification: with the hydration effect's guard
-short-circuited (`if (true) return;`), the CONTROL case flips from the frozen `["status"]`
-to the un-hydrated defaults `["name","status"]` — the assertion depends on the fetch +
-merge actually running. No production code changed; `InterfaceListPage.tsx`'s hydration
-effect already behaves this way today, this was untested rather than unreachable.
diff --git a/.changeset/home-ai-studio-flag-5521.md b/.changeset/home-ai-studio-flag-5521.md
deleted file mode 100644
index 411aa0a99d..0000000000
--- a/.changeset/home-ai-studio-flag-5521.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-Console Home stops offering the metadata-authoring front door on a deployment
-whose own runtime config says authoring is not offered there (objectui#5521).
-
-The "Build an app" cover card is now withheld when
-`GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed
-hosted-SaaS shape that card led a plain tenant into the full authoring flow
-behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry
-holds zero authoring handlers — the entry was offered and the refusal arrived at
-submit. The lockdown criterion for that shape is two-part, UI entry hidden AND
-API refused; only the backend half was green.
-
-- The card is **hidden, not dimmed**, because that is the flag's own declared
- meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when
- false, the SPA hides the AI authoring affordances", and the serving plugin
- documents "set false to force-hide the authoring UI".
-- `features.marketplace` keeps the different presentation objectui#5504 gave it
- — a dimmed card plus a visible localized reason. That flag means a route is
- reachable; this one means force-hide. "Start with a template" is untouched:
- installing a marketplace package is not AI metadata authoring and answers to
- its own flags.
-- No reason line is rendered in its place. `home.build.noCapability` says the
- *account* lacks "Manage Metadata"; on a runtime with no authoring at all the
- surface is absent for everyone, and pointing a viewer at a permission that
- would not help them is the misdirection objectui#5557 is about.
-- Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()`
- already encodes: a runtime predating the flag, or one whose config fetch
- failed, keeps the card exactly as visible as before.
-
-No new authorable config key, no new server surface, and no new copy — the flag
-was already being served and already reaches the browser.
diff --git a/.changeset/host-dispatch-action-keys-5611.md b/.changeset/host-dispatch-action-keys-5611.md
deleted file mode 100644
index 1f43f6e852..0000000000
--- a/.changeset/host-dispatch-action-keys-5611.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/core': patch
----
-
-The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's
-privileged-override safety copy (objectui#5611).
-
-`ActionRunner.execute` classifies the object it was HANDED, and a console host
-hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes
-`overrideNotice` on that dispatch and two param-collection handlers read it —
-yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a
-key two files read as one "no reader recognizes", and prescribed promoting it to
-an explicit field on `ActionDef`. That prescription is the one shape the
-2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic
-walked an author into a rejected design. A false warning on the product's own
-privileged path — the branch that finalises an approval over approvers who have
-not acted — is how a dev console gets muted.
-
-Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to
-`actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the
-fourth input to that set and the first one that is not an authored-surface
-mirror. Measured before and after on the exact dispatch the bar composes: the
-warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS`
-grew by exactly one member, and an action carrying a real typo alongside it
-still warns — naming `targt` only.
-
-The authored surface does not move. `overrideNotice` is still NOT declared on
-`ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal
-remains a compile error, and the AST-derived pin over the interface is unchanged.
-Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what
-an author may write — `actionKeys.pin.test.ts` now pins both halves, including
-the new list's exact contents so a second member cannot arrive quietly.
diff --git a/.changeset/host-only-viewtypes-tree-icon-5321.md b/.changeset/host-only-viewtypes-tree-icon-5321.md
deleted file mode 100644
index 1c922a8bed..0000000000
--- a/.changeset/host-only-viewtypes-tree-icon-5321.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-view': patch
----
-
-A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s
-view switcher instead of the grid one, and the `tree` / `chart` view types are
-recorded as host-composition-only surfaces (objectui#5321).
-
-`viewSwitcherSchema`'s `iconMap` carried an entry for every view type except
-`tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn
-with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by
-adding a single key — nothing recorded that the map had to be COMPLETE, so the
-next missing member went unnoticed. The map is now typed
-`Record`, which is how `ViewSwitcher`'s own
-`DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared:
-a future `ViewType` member fails `type-check` rather than silently rendering as
-a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph
-`DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback
-stays for host props that carry an unrecognised type. Reached in practice by
-the console, whose `CreateViewDialog` offers `tree` among the view types a user
-can create.
-
-No authoring surface changes. `generateViewSchema` renders eight view types
-while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of
-them, so `tree` and `chart` are selectable only through the component's `views`
-prop. The maintainer ruled on 2026-08-20 that both stay recorded as
-host-composition-only rather than being added to those unions, following the
-objectui#5097 precedent; the record now lives beside that one, with the branch
-set derived from a source fence, the authored unions pinned at the type level,
-and host reachability measured.
diff --git a/.changeset/humanize-label-single-home-5444.md b/.changeset/humanize-label-single-home-5444.md
deleted file mode 100644
index 1e4cfdd0ee..0000000000
--- a/.changeset/humanize-label-single-home-5444.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/fields': patch
-'@object-ui/plugin-charts': patch
----
-
-The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies.
-
-`humanizeLabel` turns a stored value into a display string when nothing else
-resolves it — an option with no declared label, an object name, a chart axis
-member. It existed twice, byte for byte: once in `@object-ui/fields` (read by
-`plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own
-renderers) and once as a deliberate local copy in `plugin-charts`'
-`ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on
-`@object-ui/fields`".
-
-Two copies of one convention is a live hazard rather than tidiness: one
-dashboard can hold a chart and a grid over the same stored value, so a change
-landing on one copy alone would put that value on screen under two spellings at
-once. The single implementation now lives in `@object-ui/core` — the shared
-ancestor both packages already depend on, so the dependency the copy existed to
-avoid is still avoided and no new edge is created, and core takes no React
-(objectui#4389: core-canonical logic, plugins consume). Both former sites
-re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps
-working unchanged.
-
-**Nothing rendered changes.** The surviving implementation is byte-identical to
-both deleted copies, and each former call site is pinned by identity against the
-core function — not by a copied output table that someone would have to remember
-to edit in two places.
-
-The core module also writes down, for the first time, why this convention stays
-distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`),
-which additionally splits camelCase:
-
-```
-input humanizeFieldKey humanizeLabel
-needs_analysis Needs Analysis Needs Analysis
-NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
-unitPrice Unit Price UnitPrice <- differ
-BestCase Best Case BestCase <- differ
-lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
-```
-
-A field KEY is authored in the codebase and carries a machine spelling, so
-splitting camelCase recovers words its author meant. A stored VALUE is arbitrary
-tenant data, where a mid-token capital is not reliably a word boundary and
-splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two
-conventions also do not nest — on the last row each leaves alone the separator
-the other rewrites. Whether they should ever converge is a separate decision
-that would move rendered output in four packages at once; it is deliberately not
-made here.
diff --git a/.changeset/i18n-global-reset-4514.md b/.changeset/i18n-global-reset-4514.md
deleted file mode 100644
index 93f3476d18..0000000000
--- a/.changeset/i18n-global-reset-4514.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
----
-
-Test harness only, no published behaviour change (objectui#4514).
-
-Mounting an `I18nProvider` in a test file left react-i18next's global
-default-instance pointer on that provider's instance, so every later
-provider-less render in the same file resolved through it — a failure that
-surfaced hundreds of lines away, in a test nobody had touched, and passed when
-run alone. `vitest.setup.base.ts` now restores the pointer after every test.
-
-`I18nProvider` and `useObjectTranslation()` are unchanged: the global fallback
-that makes the hook provider-safe stays exactly as designed. The five source
-files in this change are all `*.test.*` plus their harness.
diff --git a/.changeset/i18n-guide-label-rule-5081.md b/.changeset/i18n-guide-label-rule-5081.md
deleted file mode 100644
index 16ec799ad5..0000000000
--- a/.changeset/i18n-guide-label-rule-5081.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
----
-
-Docs + gate ledger only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared.
-
-`skills/objectui/guides/i18n.md` attributed its label rule to `@objectstack/spec` v4 in two
-places (`:117`, `:162`) while every manifest here declares `^17.0.0` and `node_modules`
-carries `17.0.0` — thirteen majors, on the surface an AGENT reads before it writes a user's
-project. The version number was the reported defect; measurement against the installed
-package found the rule it was backing to be wrong as well, which is why neither arm of the
-original fork (renumber to v17, or drop the qualifier and keep the sentence) was writable:
-both would have laundered a v4-era false statement into a current one. Maintainer ruling
-2026-08-20, option A: restate the rule per the installed spec, with no version qualifier.
-
-Measured against `@objectstack/spec` 17.0.0's published dist, `I18nLabelSchema` is a union
-of a plain string and an inline locale map whose keys must match
-`/^(default|[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*)$/` — a BCP-47 tag, or `default`. The guide now
-states both forms, shows an inline-map example on the two keys this repo was measured to
-resolve (`card.title` and `button.label`, both through `pickLocalized`), and keeps its
-"don't use `{key, defaultValue}`" advice with the real reason: that key-reference vocabulary
-was retired in objectstack#5055, the spec rejects the object with its own message, and if one
-reaches a renderer anyway `pickLocalized` falls through to the first string value and paints
-the raw translation key on screen.
-
-The `KNOWN_CLAIMS` entry that inventoried the fossil as `stale` is deleted in the same commit
-— the downward ratchet in `scripts/__tests__/doc-version-claims.test.ts` ("no entry may
-outlive the claim it excuses") turns red otherwise — and that file's header prose, which
-restated the now-falsified two-branch fork, is corrected to record what the fork actually
-turned out to be.
diff --git a/.changeset/i18nlabel-comment-4611.md b/.changeset/i18nlabel-comment-4611.md
deleted file mode 100644
index 3a243e944e..0000000000
--- a/.changeset/i18nlabel-comment-4611.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a
-plain string (objectui#4611).
-
-The paragraph explaining why `label` / `options[].label` are inherited rather than
-locally overridden justified itself with a claim about `@objectstack/spec` that was
-never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects
-were dropped in favour of translation files". Measured against the installed GA pin
-`@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of
-a string and a string-to-string record, and the schema's own doc block states two
-authorized forms with "Both are real; neither is deprecated by this schema". Executed
-against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted,
-`{ key, defaultValue }` rejected. A reader who believed the comment would have taken a
-widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one
-seat having nearly done exactly that.
-
-The replacement describes what `I18nLabel` admits and cites the spec's own doc block
-rather than restating a zod expression; where today's spelling is named it is scoped as
-a measurement against 17.0.0 with its file and line, so it ages as a reading rather than
-as a standing fact. The decision itself is unchanged and never depended on the false
-premise — `label` flows in by reference through the spec's schema, and a local
-`string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds.
-
-Documentation only, and the release-visible surface is the declaration file: measured
-with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`),
-108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every
-other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha),
-because the comment documents an `interface`, which is erased at emit along with its
-leading comment. No behaviour changes; hover text and the shipped `.d.ts` do.
diff --git a/.changeset/i18nlabel-inverted-pin-5612.md b/.changeset/i18nlabel-inverted-pin-5612.md
deleted file mode 100644
index ffa052d0b1..0000000000
--- a/.changeset/i18nlabel-inverted-pin-5612.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/types': patch
----
-
-The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and
-`ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613).
-
-Both are residue of the same removed local `label` / `options[].label` override.
-
-The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`
-that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion,
-`const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed
-`I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain
-string is assignable under the narrow shape *and* under the wide one, so that assignment
-could only ever fail if the plain-string form were removed — the opposite of the event it
-was written to catch. The widening had already landed: `@objectstack/spec@17.0.0`
-declares `I18nLabelSchema` as a union of a string and a string-to-string record
-(`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection
-it did not provide, and asserted a false premise in its own name.
-
-It is retargeted at what actually holds the decision up — not which single form the spec
-has, but that **both** authorized forms stay assignable, on the spec type and on the
-inherited `ActionParam['label']` and `options[].label`. It now fails when either form is
-withdrawn, and deliberately does not fail on a further widening, since inheriting by
-reference is exactly what stays correct as the authorized set moves. The comment is
-rewritten against the schema's own doc block (two authorized forms, "Both are real;
-neither is deprecated by this schema") instead of the false premise. Verified by
-construction: against a locally built narrow `type I18nLabel = string` the new assertions
-fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both
-shapes.
-
-`ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since
-the override was removed, and nothing re-exported it — and the doc paragraph that
-recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard.
-
-No behaviour changes; the release-visible surface is the declaration file. Measured with
-the package's real `tsc` build, both legs building from a cleared `dist/` and cleared
-composite build info: 108 emitted files on both sides, exactly one differing —
-`dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed.
-Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged
-sha), because the comment documents an `interface`, which is erased at emit along with
-its leading comment. The deleted type import contributes no emitted delta at all, and the
-rewritten test file is not part of the build.
diff --git a/.changeset/inbox-actor-name-retired-5203.md b/.changeset/inbox-actor-name-retired-5203.md
deleted file mode 100644
index ebb7ec8a97..0000000000
--- a/.changeset/inbox-actor-name-retired-5203.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
----
-
-Internal only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared.
-
-Retires `InboxNotification.actor_name` (`packages/app-shell/src/layout/inboxGrouping.ts`),
-which was dead at both ends. `mergeInboxRows`
-(`packages/app-shell/src/hooks/sharedUserFeeds.ts`) is the single producer of every row
-the bell and Home's action centre render and never mapped it; neither consumer read it;
-and `sys_inbox_message` declares no actor column for it to have been mapped FROM. It was
-the last declared-but-unfilled member of that interface after objectui#5190 removed the
-sibling `source_object` / `source_id` pair.
-
-**No published type surface changes.** `InboxNotification` is not reachable from
-`@object-ui/app-shell`'s public entry: neither `src/index.ts` nor `src/layout/index.ts`
-re-exports it, the built `dist/index.d.ts` does not name it, and the package `exports`
-map offers only `.` and `./styles.css` — no deep subpath an external consumer could
-import it through. The type is internal to the package, so removing an optional member
-of it is not an externally observable narrowing and nothing user-visible ships. Runtime
-behaviour is unchanged in both directions: no code path produced the field and no code
-path read it.
-
-Two pins keep it retired, in opposite directions — a TYPE PIN in
-`layout/__tests__/inboxGrouping.test.ts` that fails if the field is re-declared, and a
-runtime key-set pin in `hooks/__tests__/sharedInboxFeed.rowShape.test.tsx` that fails if
-the producer is ever changed to spread raw `sys_inbox_message` columns through instead
-of mapping them field by field.
diff --git a/.changeset/inert-lifecycle-path-readout-5768.md b/.changeset/inert-lifecycle-path-readout-5768.md
deleted file mode 100644
index e95f072043..0000000000
--- a/.changeset/inert-lifecycle-path-readout-5768.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/plugin-detail': minor
----
-
-`record:path` stops looking like a control it cannot be.
-
-The record page draws the object's lifecycle across the top from the
-`stageField` role, and it drew each stage as a filled, shadowed, equal-width
-pill — a segmented button group, sitting exactly where a CRM user reaches for
-the stage control. Nothing was behind it. Measured in a browser on a shipped
-build (HotCRM `crm_quote`, and this is generic record chrome, so every object
-that declares a `stageField` has it): the segments were `role="listitem"` with
-`cursor: auto` and `tabindex` null, no ancestor `button`/`tab`/`a`, and a full
-pointer sequence (pointerdown → mousedown → pointerup → mouseup → click) left
-the record's status untouched. Advancing a record needs the edit form or a bulk
-action. Users spent two or three clicks on the path before concluding it was
-decoration.
-
-There is no write path to connect it to, and this change does not open one:
-this renderer's only channel is `useRecordContext()`, whose value exposes
-`data` / `refresh` / `headerSystemActions` / `onToggleFavorite` and no
-record-field mutation. Editing runs through `record:details`'
-`` + `` (`dataSource.update(...,
-{ ifMatch })`) or an action via `useActionEngine`; neither reaches this
-component.
-
-So the promise is withdrawn rather than honoured. Each stage now renders as a
-thin decorative rail segment with its label as plain text beneath it — the
-vocabulary app-shell's approval step readout already uses. Gone: the per-stage
-filled pill, the shadow, the ring, the bordered chip, the equal-width tap
-target. Kept exactly as they were: which stage is current (`aria-current="step"`
-and type weight), the travelled/untravelled distinction, the check on completed
-stages, and the separated `lost`-terminal group. The accessible semantics did
-not move — `role="list"` / `role="listitem"` with no tab stop was already
-correct for a readout, and it stays that way.
-
-Three DOM attributes carry the state that colour used to be the only carrier
-of, so the classification is assertable without reading CSS:
-`data-stage-state` (`completed` | `current` | `upcoming`),
-`data-stage-terminal` (`won` | `lost`), and `data-stage-rail` on the decorative
-indicator.
-
-**Not in this change:** click-to-advance. A stage control that writes
-`stageField` through the same permission/validation envelope an edit takes is a
-separate feature with its own appetite, and folding it in here was explicitly
-ruled out.
diff --git a/.changeset/interfacelistpage-drop-derived-marker-title-6343.md b/.changeset/interfacelistpage-drop-derived-marker-title-6343.md
deleted file mode 100644
index 02a60cf22f..0000000000
--- a/.changeset/interfacelistpage-drop-derived-marker-title-6343.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Interface-page maps: drop the derived marker-title binding, restoring the
-object's own declaration as the authority
-
-`defaultMapFromObject` bound a `titleField` alongside `locationField`, derived
-from the object's display field. That binding was added to route around a forge
-in `ObjectMap` — `getMapConfig` filled an absent `titleField` with the literal
-`'name'`, and the marker title was a plain `record[titleField]` read, so an
-object whose display field was not `name` titled every popup `undefined`. The
-forge is gone: `ObjectMap` now resolves marker titles through
-`@object-ui/core#getRecordDisplayName`, the same ADR-0079 resolver the kanban,
-calendar and gantt renderers already used — which is why none of them binds a
-derived title either.
-
-What the binding did once the forge was gone was invert precedence. It reaches
-the resolver as `options.titleField`, i.e. step 0 — ahead of `titleField` on the
-object, ahead of the declared `nameField` pointer, and ahead of the legacy
-`titleFormat` template. A field name derived by the page could therefore only
-ever change the answer by out-ranking something the object itself declared; in
-every other case it reproduced, at step 0, the string the resolver already
-computes further down its ladder. The deriver now binds `locationField` and
-nothing else, exactly like its kanban / calendar / gallery / gantt siblings.
-
-No authoring surface changes. An author's own `map.titleField` is untouched — it
-travels as the view-level `map` block, `ListView` merges it per key, and the
-resolver honours it at step 0 by design. Objects that declare nothing resolve to
-the same field as before, now via the resolver's own type-aware derivation
-rather than a binding forced ahead of it; objects that declare a `titleFormat`
-template (or a `titleField`) now have that declaration honoured on the map, as
-it already was on every other visualization.
diff --git a/.changeset/issue-5373-retire-crud-schema.md b/.changeset/issue-5373-retire-crud-schema.md
deleted file mode 100644
index 1643a2efca..0000000000
--- a/.changeset/issue-5373-retire-crud-schema.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/core': minor
----
-
-Retire `CRUDSchema` and the `type: 'crud'` node spelling (objectui#5373,
-maintainer ruling 2026-08-20, route 2) under ADR-0049 enforce-or-remove.
-
-`crud` had four declaration faces and no registered renderer, for the whole
-life of the key: the TS interface (`packages/types/src/crud.ts`), the zod
-mirror (`packages/types/src/zod/crud.zod.ts`), a dedicated branch in
-`validateSchema` that affirmatively PASSED it, and `CRUDBuilder` in
-`@object-ui/core`. A node spelling it painted the OBJUI-001 "Unknown component
-type" panel, and `content/docs/api/schema-reference.md` published it as
-reference material — so a reader (or an AI author) who copied the page got a
-red panel.
-
-Removed from `@object-ui/types`: the `CRUDSchema` interface and its zod
-mirror, the four shapes that existed only to type its keys — `CRUDOperation`,
-`CRUDFilter`, `CRUDToolbar`, `CRUDPagination` and their zod mirrors and
-`…SchemaType` aliases — and `CRUDSchema` as a member of `CRUDComponentSchema`,
-which is what took it off the node union `AnySchema`. `ActionSchema`,
-`DetailSchema` and `CRUDDialogSchema` are unchanged and remain the union's
-members.
-
-Removed from `@object-ui/core`: `CRUDBuilder` and the `crud()` factory.
-
-Authoring `crud` is now REFUSED BY NAME rather than passed or silently
-ignored. `validateSchema` returns an `error` with `code: 'RETIRED_TYPE'` on
-`schema.type` — at any depth, since it is what `validateChildren` recurses
-with — so `assertValidSchema` throws and `isValidSchema` answers `false`. The
-message names the migration: `object-grid` for the record table with its
-toolbar, filters, pagination and row/batch actions, `object-form` for the
-create/edit form, and `detail` for the record view. `api/schema-reference.md`
-is rewritten around those shapes.
-
-Note on blast radius: the repository itself contains zero authored `crud`
-nodes and zero registrations of the key (measured on the merge base against
-the doc gate's own 659-key registry derivation, which reads `register` and
-`registerLazy` alike). That is an IN-REPO zero, not an npm zero — a published
-consumer that imported the `CRUDSchema` type, called `crud()` / `CRUDBuilder`,
-or authored `type: 'crud'` will see a compile error or a validation error
-respectively. Both are the intended, loud replacement for a shape that has
-never rendered.
diff --git a/.changeset/issue-5389-workspace-admin-positions.md b/.changeset/issue-5389-workspace-admin-positions.md
deleted file mode 100644
index 2c12bc2f6d..0000000000
--- a/.changeset/issue-5389-workspace-admin-positions.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-Restore platform-admin detection for permission-set-derived administrators.
-
-`useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace
-gating and the "Build an app" CTAs. Its third source read `user.roles`, a key
-the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it
-to `positions`). An administrator whose adminship comes from the
-`admin_full_access` permission set — the single-tenant deployment shape, where
-there is no organization member row and the server deliberately no longer
-overwrites `user.role` — matched none of the three sources and read as **not an
-administrator**: Setup and Studio simply disappeared for them.
-
-The hook now reads `user.positions[]`, the one spelling the session publishes.
-Detection is restored for that path and unchanged everywhere else: an active
-member row with an admin role, a stored `user.role` admin scalar, and
-preview/no-auth mode all behave exactly as before, and nobody who was not an
-administrator becomes one — pinned by four negative cases alongside the
-positive one.
-
-Also corrects the now-stale documentation that described the removed spelling:
-the hook's own docblock, the `roles?: string[]` declaration on the client
-`AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and
-two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the
-comment corrections.
diff --git a/.changeset/issue-5474-static-table-narrow-types.md b/.changeset/issue-5474-static-table-narrow-types.md
deleted file mode 100644
index 11b8b4627f..0000000000
--- a/.changeset/issue-5474-static-table-narrow-types.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/components': minor
----
-
-Split the static `table` column type off the rich shared `TableColumn`
-(objectui#5474, maintainer ruling 2026-08-22: Option C), so declared =
-enforced holds per renderer.
-
-`TableColumn` is unchanged and remains the rich shape `data-table`,
-`CRUDSchema` and detail-view relations honour. The static `table` renderer's
-`TableSchema.columns` now declares the new narrow `StaticTableColumn`
-(`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the
-keys that renderer reads). The eleven keys the static renderer never read are
-retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on
-`TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` /
-`filterable` / `resizable` / `editable` / `cell` on its columns.
-
-Breaking for authored metadata that wrote those keys on a `type: 'table'`
-node: they were silently inert before and are now refused loudly — a tsc
-error on the interface (`?: never`) and a parse rejection naming the key in
-`@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration:
-nodes that wanted the interactive behaviour move to `type: 'data-table'`
-(whose columns keep the rich `TableColumn`); right-aligned columns on the
-static table use `cellClassName: 'text-right'`; alternate-row styling uses
-Tailwind on `className`.
diff --git a/.changeset/issue-5619-workspace-admin-unresolved.md b/.changeset/issue-5619-workspace-admin-unresolved.md
deleted file mode 100644
index a6dd79f81b..0000000000
--- a/.changeset/issue-5619-workspace-admin-unresolved.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/auth': minor
-'@object-ui/app-shell': minor
-'@object-ui/console': minor
----
-
-Stop showing an access-denied screen to a real administrator while their
-adminship is still resolving.
-
-`useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not
-arrived yet" and "resolved: not an admin" were the same answer. One of its three
-sources — the active organization member row — is fetched some round trips after
-the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`),
-so an administrator whose adminship lives only in that row rendered at least
-once as a non-admin, and every gate downstream acted on it: the two marketplace
-surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and
-re-added its admin nav entry, and `AppContent` fired a `` that the later flip could not undo.
-
-**Breaking (published API, hence `minor` per this repo's version policy):**
-`useIsWorkspaceAdmin(): boolean` is replaced by
-`useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old
-name is removed rather than kept alongside, so a call site that ignores the
-third state fails to compile instead of silently refusing an administrator.
-
- -const isAdmin = useIsWorkspaceAdmin();
- +const { isAdmin, isResolved } = useWorkspaceAdminStatus();
-
-`AuthProvider` gains `isMembershipResolved` on its context — the organization /
-member pipeline has reached a terminal state — because `organizations`,
-`activeOrganization` and `activeMember` read `[]` / `null` / `null` both before
-the pipeline starts and after it finds nothing.
-
-No extra wait for administrators: `isResolved` is true the instant `isAdmin` is,
-so an admin the session already identifies through `positions[]` never waits on
-the member row.
diff --git a/.changeset/issue-5709-unconsumed-widget-option-warning.md b/.changeset/issue-5709-unconsumed-widget-option-warning.md
deleted file mode 100644
index c4a5485748..0000000000
--- a/.changeset/issue-5709-unconsumed-widget-option-warning.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/sdui-parser': minor
----
-
-`validateTree` now reports a dashboard widget `options` key that no renderer
-consumes as a `unconsumed-widget-option` **warning** naming the consumed set
-(objectui#5709 ruling). The census behind the accepted set — the spec's five
-declared query keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`,
-`stageOrder`) plus the `description` sub-caption convention key — is
-re-measured on every test run against `@objectstack/spec` and the
-`plugin-dashboard` renderer sources. The check fires only on dataset-bound
-shorthand widgets (the spec-legal form) hosted by `dashboard` /
-`dashboard-grid` nodes, and honours the spec's per-widget
-`suppressWarnings: ['unconsumed-widget-option']` escape hatch. Warning
-severity only: documents keep parsing, saving and rendering. Exported for
-other surfaces: `checkDashboardWidgetOptions`, `CONSUMED_WIDGET_OPTION_KEYS`,
-`DASHBOARD_WIDGET_HOST_TYPES`, `UNCONSUMED_WIDGET_OPTION`.
diff --git a/.changeset/issue-5821-table-column-editable-mirror.md b/.changeset/issue-5821-table-column-editable-mirror.md
deleted file mode 100644
index 7f9b967e00..0000000000
--- a/.changeset/issue-5821-table-column-editable-mirror.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Add `editable` to the rich `TableColumnSchema` zod mirror. The `TableColumn` interface declares `editable?: boolean` and `data-table` honours it, but the mirror omitted the key, so a non-strict parse silently stripped it — and since the renderer treats absence as `true`, a column an author locked with `editable: false` came out of validation editable again. Columns locked with `editable: false` now stay locked through any pipeline that parses metadata via `@object-ui/types/zod` (including the CLI `validate` route).
diff --git a/.changeset/issue-5881-record-highlights-header-prose.md b/.changeset/issue-5881-record-highlights-header-prose.md
deleted file mode 100644
index af3ed5571c..0000000000
--- a/.changeset/issue-5881-record-highlights-header-prose.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only comment change: the module header of
-`recordHighlightsInputs.spec-parity.test.ts` no longer narrates
-`RecordHighlightsProps` as a strip-mode `z.object`. It now states the
-pin-independent verdict and points at the file's own
-`specRefusesUnknownTopLevelKeys` probe for which way the installed
-`@objectstack/spec` states it. No assertion changed; nothing releases.
diff --git a/.changeset/issue-5887-toplevel-strictness-probes.md b/.changeset/issue-5887-toplevel-strictness-probes.md
deleted file mode 100644
index 878a97ff50..0000000000
--- a/.changeset/issue-5887-toplevel-strictness-probes.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only. The two `plugin-detail` top-level spec-parity blocks
-(`recordDetailsInputs`, `recordRelatedListInputs`) now measure their OWN
-schema's strictness against an undeclared top-level key, via a per-file
-`specRefusesUnknownTopLevelKeys` probe and a two-arm assertion, instead of
-citing the sibling `record:highlights` probe in prose — strictness is per
-schema. No published behaviour changes.
diff --git a/.changeset/issue-6275-validation-engine-dangling-see.md b/.changeset/issue-6275-validation-engine-dangling-see.md
deleted file mode 100644
index 47ffe32e45..0000000000
--- a/.changeset/issue-6275-validation-engine-dangling-see.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Comment-only change in `@object-ui/core`: the `SimpleExpressionEvaluator` doc block
-no longer points at the deleted `SECURITY_FIX_SUMMARY.md`, and states the security
-rationale inline instead. Releases nothing — the class is not exported, so the doc
-block reaches no `.d.ts`, and no behaviour, type or API surface changes.
diff --git a/.changeset/issue-6567-hoist-internalformroute-import.md b/.changeset/issue-6567-hoist-internalformroute-import.md
deleted file mode 100644
index e8d1e73a62..0000000000
--- a/.changeset/issue-6567-hoist-internalformroute-import.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Test-only: in `apps/console/src/components/FormPage.predicateScope.test.tsx`, hoist
-`InternalFormRoute`'s import from a dynamic `await import()` inside the
-`hop1SessionPrincipal` case to module scope. That load was costing 10204ms of the
-case's 15000ms budget — it is the file's first value request for
-`@object-ui/app-shell`, aliased to source — which made the file's one anti-inert
-case fail under full-project parallel load. Same module, same binding, loaded
-before the timed window instead of inside it. No published behaviour changes.
diff --git a/.changeset/jsonc-fence-extractor-test-only.md b/.changeset/jsonc-fence-extractor-test-only.md
deleted file mode 100644
index 03c7c1f195..0000000000
--- a/.changeset/jsonc-fence-extractor-test-only.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change in `@object-ui/components`: the skill-guide fence extractor in
-`skill-guide-data-table-binding.test.tsx` now reads `jsonc` fences as well as
-`json`, so retagging a comment-carrying guide example no longer hides it from
-every assertion in that file. No published behaviour changes.
diff --git a/.changeset/kanban-calendar-nav-width-converge.md b/.changeset/kanban-calendar-nav-width-converge.md
deleted file mode 100644
index 7f827c27d4..0000000000
--- a/.changeset/kanban-calendar-nav-width-converge.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
----
-
-`ObjectKanban` and `ObjectCalendar` internal cleanup, measured as a zero-pixel
-change — the convergence of the two renderers #6305 left behind.
-
-Each carried the house default `min(960px, 60vw)` at **two** sites: the
-`navConfig` default (`{ mode: 'drawer', width: 'min(960px, 60vw)' }`) and a
-render-site `width={(navigation.width as any) ?? 'min(960px, 60vw)'}`. Both are
-gone. `width` is spec-deprecated (`@deprecated [#2578 -> size]`) and
-`resolveOverlayWidth` gives an explicit `width` priority OVER `size`, so
-spelling it kept the deprecated branch load-bearing on the path most boards and
-calendars take. With both omitted, `resolveOverlayWidth` returns `undefined` and
-`RecordDetailDrawer`'s own `width` default supplies the identical
-`min(960px, 60vw)`.
-
-The resolved overlay width is therefore unchanged on every viewport, for both a
-board/calendar that declares no `navigation` and one that authors
-`navigation.width` — each is now pinned by a test. The three renderers agree
-again.
-
-No published behaviour changes.
diff --git a/.changeset/khaki-rocks-shout.md b/.changeset/khaki-rocks-shout.md
deleted file mode 100644
index 24665f276f..0000000000
--- a/.changeset/khaki-rocks-shout.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-"@object-ui/plugin-charts": patch
----
-
-Draw every categorical x-axis label on short axes
-
-A vertical bar chart in a dashboard-width widget dropped most of its x-axis
-labels — three bars drew one label, five bars drew two — leaving the bars
-unnamed, with no legend to fall back on because a single-series bar chart has
-none.
-
-The x axis applied one tick policy to time and category alike (`preserveStartEnd`
-with a 48px `minTickGap`), which is right for hundreds of dates and wrong for a
-band axis, where a dropped tick is an identity the reader cannot recover rather
-than a sample they can interpolate. It was also keyed to the viewport rather
-than the widget, so a 200px chart inside an 800px console was treated as a wide
-one.
-
-Bar, column, line, area and combo charts now draw every label on a categorical
-x axis of five buckets or fewer — rotating, and ellipsising an over-long name
-rather than clipping it. Longer axes keep the existing measured thinning, and
-horizontal bars are unchanged.
diff --git a/.changeset/layout-default-jsdoc-7361.md b/.changeset/layout-default-jsdoc-7361.md
deleted file mode 100644
index ec7cc25321..0000000000
--- a/.changeset/layout-default-jsdoc-7361.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/types': patch
----
-
-The published `@default` documentation on two `layout.ts` members now matches the value the renderer actually applies. `ContainerSchema.maxWidth` documented `'lg'` while `container.tsx` applies `schema.maxWidth ?? 'xl'`, and the shared `FlexLayoutProps.align` documented `'center'` while `flex.tsx` applies `schema.align || 'start'` and `stack.tsx` applies `schema.align || 'stretch'`. The renderers are unchanged — they are the authority for what runs — so only the docblocks moved; `align` now states both consumers in prose instead of carrying a single `@default`, because one member shared by two deliberately divergent component types cannot have one correct default.
diff --git a/.changeset/layout-direction-default-jsdoc-7734.md b/.changeset/layout-direction-default-jsdoc-7734.md
deleted file mode 100644
index e8943954ec..0000000000
--- a/.changeset/layout-direction-default-jsdoc-7734.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/types': patch
----
-
-The published `@default` documentation on `FlexLayoutProps.direction` no longer states a value that only one of its two consumers applies. The member is declared once (objectui#6151) but `flex.tsx` reads `schema.direction || 'row'` while `stack.tsx` reads `schema.direction || 'col'` ("Default to column for Stack"), so the single `@default 'row'` was correct for `flex` and wrong for `stack` — whose own `defaultProps.direction` is `'col'`. The renderers are unchanged — they are the authority for what runs — so only the docblock moved: `direction` now names both consumers in prose, the same remedy objectui#7361 applied to the sibling `align`. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays: the criterion is a DIVERGENT shared member, not a shared one.
diff --git a/.changeset/layout-renderers-dom-props-5574.md b/.changeset/layout-renderers-dom-props-5574.md
deleted file mode 100644
index b018f67b4b..0000000000
--- a/.changeset/layout-renderers-dom-props-5574.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/components': patch
----
-
-**Behaviour change:** the `flex`, `stack`, `container` and `text` renderers no
-longer forward their whole prop bag to the host element. They route it through
-`toDomProps` — the same whitelist `grid` was converged on — so an authored
-schema key becomes an HTML attribute only if the SDUI DOM contract declares it
-one (objectui#5574).
-
-What this stops reaching the DOM: the renderer's own declared props, which were
-consumed off `schema` to build the class list AND spread onto the element a
-second time as attributes HTML does not define. Measured across
-`examples/schema-catalog`, rendered through the real `SchemaRenderer`: 1194
-illegitimate attributes over 1141 nodes — `text[content]` 522, `flex[align]`
-198, `flex[gap]` 193, `stack[gap]` 153, `flex[justify]` 98, `container[padding]`
-14, `container[maxwidth]` 6, `flex[direction]` 5, `stack[align]` 4,
-`text[value]` 1. The same probe reads 0 after, with `grid`'s 26 nodes at 0 both
-times as the control.
-
-Nothing an author writes renders differently: every leaked key was already being
-read off `schema` and applied as a class, so the markup loses attributes that
-never had meaning and keeps the styling that did. `id`, `role`, `tabIndex`,
-`className`, `style`, event handlers and the open `data-*` / `aria-*` families
-are unchanged — including `data-obj-id` / `data-obj-type`, which now arrive
-through the `data-*` family rather than by hand.
-
-Anything that read one of the leaked attributes off the DOM — a CSS attribute
-selector such as `[gap="4"]`, or a test asserting `align` on a rendered `flex` —
-must read the schema or the class instead. No `@object-ui` code did; this is
-called out because the attributes were externally visible while they lasted.
diff --git a/.changeset/lazy-plugin-jsdoc-compiles.md b/.changeset/lazy-plugin-jsdoc-compiles.md
deleted file mode 100644
index e2dd0c0aa0..0000000000
--- a/.changeset/lazy-plugin-jsdoc-compiles.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/react": patch
----
-
-Fix `createLazyPlugin`'s JSDoc example, which taught a call that does not compile.
-
-The `@example` block passed `() => import('@object-ui/plugin-grid')` as the
-`importFn`. ObjectUI plugin packages export their components by name and have no
-`default`, so that call supplies the module namespace object — rejected by the
-compiler (`Property 'default' is missing`) and, at runtime, handed to
-`React.lazy` as the component. The examples now unwrap the named export via the
-`async` spelling, which is the form that actually type-checks. This ships in the
-published `.d.ts`, so it is what editors show on hover.
diff --git a/.changeset/list-user-actions-collision-5398.md b/.changeset/list-user-actions-collision-5398.md
deleted file mode 100644
index 62a0d3c775..0000000000
--- a/.changeset/list-user-actions-collision-5398.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-`list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it.
-
-`userActions` names two different blocks. On a **view** it is toolbar policy —
-the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`,
-`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by
-name. On an **object** it is the CRUD-predicate block (`edit` / `delete` /
-`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is
-the only shape `listViewPredicates` can read, since its loop skips every
-non-object value.
-
-`ListView` read the key view-first when building the `$select` projection
-(`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view
-carrying a perfectly legal toolbar block therefore shadowed the object's CRUD
-predicates, the harvest found none, and the predicate's operand left the
-projection. CEL then faults on the absent key, fails closed, and the row
-Edit/Delete button disappears for everyone with nothing pointing at the
-projection — objectui#3501's failure, reached with a success receipt at every
-step.
-
-This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and
-it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level
-`userActions` it hands down as an object literal of two spreads, so the left
-operand was `{}` at worst — never nullish. The `??` never fell through, and the
-object's CRUD predicates were never consumed at all on that path, whether or
-not an author wrote any toolbar policy.
-
-The harvest now reads the object block only. Both `userActions` read sites in
-`ListView.tsx` carry a comment naming the collision, and
-`__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the
-two shapes, a producer that manufactures the view one, the harvest's blindness
-to it, and the projection that must keep the object's operand with a toolbar
-block — or an empty block — present on the view.
-
-Toolbar policy itself is untouched — it was never read through this path.
diff --git a/.changeset/list-view-nested-aria-label-i18n-5134.md b/.changeset/list-view-nested-aria-label-i18n-5134.md
deleted file mode 100644
index f330b77a6a..0000000000
--- a/.changeset/list-view-nested-aria-label-i18n-5134.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-`ListView` now resolves the nested `aria.ariaLabel` against the audience's locale
-instead of casting it to a string (objectui#5134).
-
-`@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain
-string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only
-read site in this repo spread it into the DOM as
-`{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a
-conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which
-a screen reader announces as the list view's accessible name — in every locale. The
-read now goes through the spec's own `resolveI18nLabel` (the resolver four other
-in-repo read sites already use) against `useDisplayLocale()`.
-
-Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a
-plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label
-predates rc.6, and no measured author writes one today — but map values are legitimate
-and arrive via API/import, so an imported list view carrying
-`aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong
-accessible name. This is the map form working as declared, not a defect users are
-currently hitting.
-
-Behaviour on the string arm is byte-identical, including `''` (falsy before and after,
-so no attribute). One edge changes for the better: a map that matches no locale used to
-render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an
-unnamed region beats a garbage-named one.
-
-The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different
-vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by
-`SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's
-shape.
diff --git a/.changeset/listview-calendar-crossfield-pin-7122.md b/.changeset/listview-calendar-crossfield-pin-7122.md
deleted file mode 100644
index b642dd5a89..0000000000
--- a/.changeset/listview-calendar-crossfield-pin-7122.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change: pins `@objectstack/spec` 17.3.0's list-view cross-field refusal
-(offering the `calendar` visualization without a `calendar: { startDateField }`
-block) through metadata-admin's real client-validation gate, on the create and
-edit doors. No published behaviour changes — `@object-ui/app-shell` ships no new
-or altered runtime code here.
diff --git a/.changeset/listview-comment-pair-4559.md b/.changeset/listview-comment-pair-4559.md
deleted file mode 100644
index 2f012823cd..0000000000
--- a/.changeset/listview-comment-pair-4559.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-list': patch
----
-
-Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime
-behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does
-change, which is why this is a `patch` rather than an empty frontmatter.
-
-**objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment
-block above the `sortFields` memo still called a formula field "the supported
-alternative (… which sorts like any text column)". Since objectui#4294 the
-`list.sortRelationalHint` string in this same file says the opposite ("Not a formula
-field: it is virtual, so no column is stored for it and the server refuses to sort by
-one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the
-server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical
-now names the remedy the hint, the server's refusal and the README already share — a
-stored field that denormalizes the name onto this object, written when the source
-changes. This was the last copy of the retired advice in the repo.
-
-**objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.**
-It had drifted two declarations up, so the exported `parseSortConfig` carried two
-stacked leading comments and the helper carried none. This one was not cosmetic: because
-`parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into
-`dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort
-parser with a sentence about action labels. Moving the block removes it from the `.d.ts`;
-`formatActionLabel` is module-private, so its now-correct docblock does not appear there.
-It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the
-comment block *attached* to a declaration — a misattributed docblock is the mechanism by
-which a claim gets scored against the wrong symbol. This block carries no spec-alignment
-phrase, so nothing fired today.
-
-No tests accompany this change and none could: both edits are comment-only, and there is
-no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real
-`vite build` before and after, not asserted.
diff --git a/.changeset/listview-map-view-level-config-5042.md b/.changeset/listview-map-view-level-config-5042.md
deleted file mode 100644
index 44ca3ccd04..0000000000
--- a/.changeset/listview-map-view-level-config-5042.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/plugin-list': minor
-'@object-ui/app-shell': minor
----
-
-**Behaviour change:** the spec's view-level `map` block on a list view is now read at
-runtime. `ListMapConfigSchema` (objectstack#9340) has been authorable and validated since
-the `@objectstack/spec` 17.1.0 pin — it flows into this repo's own `ListViewSchema` by
-reference — but nothing consumed it: `ListView`'s `case 'map'` forwarded only the legacy
-`schema.options.map` bag, so declaring `map: { titleField: 'title', locationField:
-'location' }` on a view changed nothing and marker titles fell back to the renderer's
-placeholder.
-
-The block now reaches `plugin-map` and drives every one of its seven reads — coordinate
-extraction, marker title and description, and the initial camera. Precedence follows the
-convention the sibling visualization blocks in the same file already set: the view-level
-block wins over `options.map`, per key, exactly as `kanban` / `calendar` / `gallery` /
-`timeline` / `gantt` each merge their spec config over the legacy bag. Both sources go
-through the existing objectui#5177 key whitelist, and the branch still emits the flat
-form, so `getMapConfig`'s objectui#5018 precedence rule ("neither flattener emits a `map`
-key at all") stays true.
-
-The visualization switcher had the same gap with a sharper consequence: the capability
-gate that decides which visualizations are offered also read `options.map` alone, so a
-view binding its coordinates in the spec block was filtered out of its own
-`appearance.allowedVisualizations` and fell back to `['grid']`. The gate now asks the same
-merged config the render seam forwards, so the two cannot disagree — including for a
-binding split across the two sources.
-
-`InterfaceListPage` (ADR-0047 interface pages) forwards the referenced view's `map` block
-for the same reason. It is passed alongside the auto-derived `options.map` rather than
-replacing it, so a partial authored block — `map: { titleField: 'title' }` — keeps the
-derived coordinate binding instead of dropping it.
-
-No defaults are introduced for `zoom` / `center`: an undeclared camera stays undeclared,
-so the fit-to-queried-records behaviour ruled in objectui#5000 is unchanged.
diff --git a/.changeset/lookup-cell-chip-cap.md b/.changeset/lookup-cell-chip-cap.md
deleted file mode 100644
index 4f1c77677a..0000000000
--- a/.changeset/lookup-cell-chip-cap.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-A multi-value lookup cell no longer grows its row without bound: `LookupCellRenderer`
-now shows at most 3 chips and collapses the rest into one muted `+N` chip, the same
-cap `UserCellRenderer` has always applied to its avatar stack in the very same file.
-
-Previously the array branch rendered EVERY referenced record as its own chip inside a
-`flex-wrap` container. In a grid column that wraps to one chip per line, so a cell
-referencing a large set — a production 排班计划 row referencing 60+ work objects — grew
-a single row to several screens of height and blew the page layout apart. The same
-uncapped rendering reached every surface that resolves through `getCellRenderer('lookup')`:
-grid, related lists, gallery, kanban, report and dashboard tables, and the record detail
-sections.
-
-The collapsed names stay reachable: the `+N` chip's `title` lists the display names of
-the hidden references (resolved through the same option/label/record-name path as the
-visible chips), and the record's own detail view remains the place to see the full set.
-The first 3 chips keep their per-record links (#4336) and their resolution order —
-nothing changes for cells with 3 or fewer references.
diff --git a/.changeset/lookup-dropdown-cell-renderer-5492.md b/.changeset/lookup-dropdown-cell-renderer-5492.md
deleted file mode 100644
index d43f0e19a2..0000000000
--- a/.changeset/lookup-dropdown-cell-renderer-5492.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-A lookup's inline dropdown renders its columns through the same cell renderer the browse-all picker uses, so one `lookup_columns` declaration cannot produce two answers.
-
-A form's lookup field offers two ways to pick a related record, and both read
-the same declaration: the inline dropdown under the field, and the
-"browse all records" picker behind it. The picker resolved every cell through
-the type-aware cell renderer. The dropdown did not — it printed
-`record[descriptionField]` verbatim into the option subtitle and concatenated
-`label: String(rawValue)` into the row's `title` attribute. Measured on the
-same declaration, on a real 17.1.0 deployment:
-
-```
-column inline dropdown (before) browse-all picker
-lookup T5MsMCuwP4t_yUHq (bare FK id) the related record's name
-date 2026-08-20T00:00:00.000Z (ISO) a formatted date
-select pending (enum code) the authored option label
-```
-
-Both surfaces now call one shared module — `widgets/lookupColumnDisplay.tsx`,
-which owns column normalisation, the field-descriptor enrichment from the
-referenced object's schema, and the render itself. The picker's own
-`renderCellContent` and `columnFieldDescriptors` are now thin calls into it, so
-there is a single renderer left to drift from. The dropdown's extra columns are
-rendered into the option row itself; the row's `title` keeps the full option
-label, which is what a truncated label needs, instead of a raw-value dump.
-
-No query changed and no contract widened. `lookupColumns` entries stay bare
-field names — no dot paths, no populate/expand semantics — because neither
-surface's request carries populate to begin with: the picker resolves a
-foreign-key id to a name client-side, in the lookup cell renderer, and the
-dropdown now inherits exactly that. An unresolved reference therefore renders
-what the picker renders for it, and keeps its column: a slot is dropped only
-when the record holds no value for the field, decided on the raw value and
-never on what the renderer makes of it, so an unresolved id can never degrade
-into a silently empty column.
diff --git a/.changeset/loud-lamps-shake.md b/.changeset/loud-lamps-shake.md
deleted file mode 100644
index eae0d16227..0000000000
--- a/.changeset/loud-lamps-shake.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-"@object-ui/react": minor
-"@object-ui/plugin-detail": minor
-"@object-ui/plugin-grid": minor
-"@object-ui/plugin-form": minor
----
-
-`object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so
-
-The three object-bound blocks disagreed about how the data-source adapter reached
-them. `object-grid` and `object-form` were registered through wrappers that read
-it from `SchemaRendererProvider` context; `detail-view` was registered as the raw
-component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads
-only context, so the two wirings were mutually exclusive: measured with correct
-keys in every cell, provider wiring gave the grid `find` 1 and the detail view
-`findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything.
-
-All three now resolve the adapter through one rule — an explicit `dataSource`
-prop first, the provider context second. This is additive: `detail-view` keeps
-its prop form (and direct ` ` callers are untouched),
-`object-form` gains a prop form it did not have, and `object-grid` no longer
-throws `useSchemaContext must be used within a SchemaRendererProvider` when a
-page has no provider.
-
-And the silence is over. A block in this family that resolves no adapter renders
-a **No data source resolved** panel naming the block, the object it was about to
-read, and the ancestor that injects the adapter — instead of a header-only grid,
-a field-less form card, or nothing at all. The check is opt-in per block, so a
-placement with inline rows, inline `customFields`, an inline record or an `api`
-endpoint is untouched.
-
-New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`,
-`noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`.
diff --git a/.changeset/lucide-icon-record-name-gate.md b/.changeset/lucide-icon-record-name-gate.md
deleted file mode 100644
index 2a2f9ca90f..0000000000
--- a/.changeset/lucide-icon-record-name-gate.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
----
-
-Adds `check:icon-record-names`, a repo-level gate asserting that every authored icon
-name reaching a resolver that reads lucide's runtime `icons` record is a live key of
-that record. lucide retires a spelling by dropping it from that record while keeping
-it as a deprecated named export, so a retired name still imports, still type-checks
-and still renders as a component while resolving to nothing as a string — the class
-behind objectui#5586 and objectui#5622, each of which left a local pin behind. The
-gate judges against the record itself rather than any list of retired spellings, and
-re-discovers the resolver population on every run.
-
-No published behaviour changes: the touched `src/` files carry comment updates only,
-and the three repaired spellings are in example schemas and the docs playground.
diff --git a/.changeset/lucky-buttons-warn.md b/.changeset/lucky-buttons-warn.md
deleted file mode 100644
index 739ac7445c..0000000000
--- a/.changeset/lucky-buttons-warn.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`record:activity` now says out loud when an unrecognised `filterMode` is folded onto `all`.
-
-`normalizeFilterMode` folds every value it does not recognise onto `'all'` — the widest of the four declared modes — so a near-miss like `comments-only` opened the panel on the unfiltered stream instead of the slice the author asked for, and said nothing. The fallback is kept (a dropdown handed a value with no matching item renders blank), but the fold now emits one deduped diagnostic naming the offending value and the declared modes. Nothing that renders changes.
diff --git a/.changeset/lucky-donkeys-repeat.md b/.changeset/lucky-donkeys-repeat.md
deleted file mode 100644
index e1a2c2679e..0000000000
--- a/.changeset/lucky-donkeys-repeat.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
----
-
-Tests only — no published behaviour changes.
-
-objectui#6598 reported that a `kind:'html'` page's `` rendered rows but
-no data columns, in all eight `columns` spellings the reporter tried. Re-measured
-on the merged ref: the symptom no longer reproduces in any of the eight. The
-three mechanisms behind it were fixed by objectui#6614 / PR #6669 (the braced
-literal subset), PR #6679 (the unauthored-projection handoff) and objectui#6677
-(the grid's default-column derivation). This adds the matrix pin the card itself
-was missing — every one of the eight spellings, end to end through the real live
-registration and the real object-grid — so no future change can put any of them
-back into the reported state.
diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md
deleted file mode 100644
index 0eb92d73fc..0000000000
--- a/.changeset/lucky-donkeys-shave.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
----
-
-Census only (objectui#7642): record, at each of the six sites, which contract types
-the field-def bag it reads. No runtime behaviour changes — every snake leg is kept,
-so this declares no release.
-
-The card proposed retiring `display_field` / `description_field` / `lookup_filters` /
-`id_field` reads on the ground that `@objectstack/spec`'s `FieldSchema` is strict and
-refuses them. Measured against the installed spec, that is true — of the AUTHORING
-path. Three findings moved every site to KEEP:
-
-1. The SERVE path runs no parse. `ObjectStackAdapter.getObjectSchema` returns the
- server document verbatim plus exactly two rewrites (`normalizeSchemaReferenceKeys`,
- `applyFieldWidgetOverrides`); there is no `ObjectSchema.parse`/`safeParse` on that
- path. The `resolveActionParams` site is served by a different path —
- `useMetadata().objects`, filled by `client.meta.getItems('object')` in `app-shell`'s
- `MetadataProvider` — and that path runs no schema parse either (the provider's only
- `parse` is `JSON.parse` of its session cache; the pinned `@objectstack/client`'s
- three `safeParse` calls are all event-payload schemas, none on `getItems`). A
- stored pre-strict document therefore still delivers these keys to every consumer.
- The legs are not unreachable.
-2. Five of the six sites have NO camelCase leg. They read the refused spelling and
- nothing else, so retiring it does not re-point the read to the declared spelling —
- it deletes the only read of the value. The corollary is the real user-facing gap:
- on fully spec-compliant metadata those five sites already ignore a configured
- `displayField` / `descriptionField` / `lookupFilters` today. `idField` is NOT in
- that list: measured on the pinned spec 17.2.0, `FieldSchema` refuses `idField` with
- `unrecognized_keys` exactly as it refuses `id_field` — on `FieldSchema` neither
- spelling of the id key is declared, so the `id_field` reads have no `FieldSchema`
- spelling to gain a leg for and their only route is the ingestion choke point
- (objectui#7650, option A). That is a `FieldSchema` statement only: the widget
- contract `@object-ui/types` `LookupFieldMetadata` DOES declare `idField` (kept by
- PR #7641 as a widget-contract key), and `LookupField` reads it off the same runtime
- object finding 3 describes.
-3. The object-schema field def and the widget bag are the same object at runtime.
- `ObjectForm` builds its fields from `getObjectSchema` and threads each def to the
- widget, `@object-ui/fields` `LookupField`. What that widget reads moved while this
- census was under review, so this record is dated. At this branch's base (`1ec291c0`,
- 2026-09-04) `LookupField` read `display_field` / `description_field` / `id_field` /
- `lookup_filters` SNAKE-FIRST, the published `@object-ui/types` `LookupFieldMetadata`
- declared all four snake members, and `content/docs/fields/lookup.mdx` documented
- three of them as authorable (`description_field`, `id_field`, `lookup_filters`;
- `display_field` 0 hits in all of `content/docs`, which documented `reference_field`
- instead). PR #7641 (merged 2026-09-04T15:01:32Z as `351eb318`) then converged the
- widget contract on the spec's camelCase. Measured on `origin/main` (`a3eb5d07`):
- `LookupField` reads `displayField` / `descriptionField` / `idField` / `lookupFilters`
- ONLY (its `reference_field` fallback is kept), `LookupFieldMetadata` declares the
- camel members only, and `content/docs` has 0 hits for all four snake keys (controls
- in the same run: `reference_field` 4, `lookupFilters` 3). On the tree this census
- lands in, the split therefore runs the OTHER way: it is KEEPING these six snake
- legs, while the form widget reads camel-only, that lets one stored pre-strict
- document render one way in the form and another in the chart, list, filters and
- action dialogs. KEEP still stands — on findings 1-2 (the serve path delivers the
- stored key, and five sites have no camel leg, so retiring the read deletes the only
- read) and on the ruling that refused option B and made the ingestion choke point
- (option A, objectui#7650) the prerequisite for any retirement. The way to close the
- split is A (canonicalise once at ingestion) plus the additive camel legs tracked on
- objectui#7435, not a consumer-side deletion.
-
-Per site, which way the value would have flipped had the leg been retired:
-
-- `plugin-charts` `ObjectChart` (`id_field`, `display_field`) — bag proved to be the
- object-schema def (`ds.getObjectSchema`). No camel leg: the value would have
- collapsed to the constants `'id'` and `'name'` for every host, spec-compliant or not.
-- `plugin-form` `deriveMasterDetail` (`display_field`) — object-schema def in-repo,
- but `deriveColumns` is a public export, so external callers' bags are untraceable.
- No camel leg: `col.displayField` would have become `undefined`.
-- `plugin-list` `ListView`, columns branch — NOT the object-schema def. The bag is a
- list-view column (`ListColumnSchema`), which refuses BOTH castings of all three keys.
- A third contract, filed separately rather than half-retired.
-- `plugin-list` `ListView`, object-def branch — object-schema def proved. No camel leg:
- the branch's output `displayField` and `idField` (its own descriptor keys, not spec
- spellings) would have gone `undefined`.
-- `plugin-list` `UserFilters` — `objectDef` is a public prop typed `any` on a publicly
- exported component; the bag cannot be traced past this package. No camel leg.
-- `app-shell` `resolveActionParams` — the in-file provenance note is correct; this is
- the object-schema def. No camel leg for any of its four reads.
-- `app-shell` `ObjectFieldInspector` (`lookup_filters`) — the only camel-first site,
- and it writes camel back. Its snake leg reads a stored pre-strict document, so
- retiring it would show an admin an empty filter list and let a save strand the real
- filters.
-
-One live bug found during the census and deliberately NOT filed as a card, not
-addressed here: at this branch's base the designer (`ObjectFieldInspector`,
-`readLookupFilters`) read `lookupFilters ?? lookup_filters` (camel first) while the
-runtime `LookupField` read `lookup_filters ?? lookupFilters` (snake first), so a
-document carrying both keys with different values was displayed one way and honoured
-the other. It is RETIRED on `main`: PR #7641 (merged 2026-09-04T15:01:32Z, `351eb318`)
-made `LookupField` read `lookupFilters` only, so both halves now honour the camel key
-and there is nothing left to file. The fallback the PM recorded on objectui#7642 (a
-card the moment #7641 stopped being its fix) is moot — #7641 landed. The designer's
-snake leg survives as a read of a STORED pre-strict document, which is the ground of
-its KEEP above, not as one side of a competing read order.
diff --git a/.changeset/lucky-forms-omit-server-defaults.md b/.changeset/lucky-forms-omit-server-defaults.md
deleted file mode 100644
index 2df9bd659d..0000000000
--- a/.changeset/lucky-forms-omit-server-defaults.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/console': patch
----
-
-Console form pages no longer submit a cleared server-owned field as a blank.
-
-A field whose declared `defaultValue` is an instruction the server resolves per
-insert (a `NOW()` / `current_user` token, or a CEL expression envelope) opens
-with an empty control on a create form, and its key stays out of the payload
-while nothing touches it. But a submitter who typed into that control and then
-cleared it put the key back holding `''` — and `ObjectQL.applyFieldDefaults`
-resolves a declared default only for a field arriving absent or null, so the
-blank was stored and the declaration silently defeated.
-
-Such a key is now dropped from a CREATE submit on both the internal
-`/forms/:name` and the anonymous `/f/:slug` route. A blank cleared from a field
-with no runtime default — or with a static one — is still submitted, because
-that is the user removing a value; and an edit submit is untouched, where a
-cleared column is a deliberate removal.
diff --git a/.changeset/lucky-pugs-shake.md b/.changeset/lucky-pugs-shake.md
deleted file mode 100644
index 71af6fb8c7..0000000000
--- a/.changeset/lucky-pugs-shake.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-"@object-ui/components": patch
----
-
-`ui:dropdown-menu` now resolves a menu item's authored `icon` to a glyph instead of drawing the name as raw text.
-
-Both arms of the item renderer — `DropdownMenuItem` and `DropdownMenuSubTrigger` — rendered `icon` straight into a text node, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew the literal word `copy` beside its label. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph.
-
-The `components-overlay-dropdown-menu/with-icons` catalog fixture declared the retired lucide spelling `edit`, which is absent from lucide's runtime `icons` record and would therefore have drawn no glyph; it now declares `square-pen`, the live key the retired export resolves to by identity.
diff --git a/.changeset/map-marker-title-binding.md b/.changeset/map-marker-title-binding.md
deleted file mode 100644
index aa8627f38f..0000000000
--- a/.changeset/map-marker-title-binding.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Interface-page maps: derive a marker-title binding from the object's display field
-
-An ADR-0047 interface page that whitelists `map` derives its map binding with
-`defaultMapFromObject`, which bound only `locationField`. With no `titleField`
-reaching `ObjectMap`, `getMapConfig` filled the gap with the literal `'name'`
-and the marker title is a plain `record[titleField]` read — so on any object
-whose display field is not `name` (for example one keyed by `title`), every
-marker popup titled itself `undefined`.
-
-The derivation now also binds the object's display field, resolved with the
-field-name half of ADR-0079's precedence: the declared `nameField` (and its
-`displayNameField` / `NAME_FIELD_KEY` aliases), otherwise the shared
-`deriveTitleField` scan from `@object-ui/core` — the same ranking the kanban,
-calendar and gantt renderers resolve titles through, so a map and a board over
-one object agree on what a record is called. When nothing resolves the key is
-omitted rather than defaulted. A hand-declared `map` block still wins per key.
diff --git a/.changeset/marketplace-catalog-runtime-before-admin-5557.md b/.changeset/marketplace-catalog-runtime-before-admin-5557.md
deleted file mode 100644
index 69a66da580..0000000000
--- a/.changeset/marketplace-catalog-runtime-before-admin-5557.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The marketplace **catalog** page now tells a non-admin that the runtime has no
-marketplace, instead of telling them they lack permission (objectui#5557).
-
-`MarketplacePage` ordered its two early returns admin-first, so on a runtime that
-mounts no marketplace at all (`features.marketplace: false` — an `OS_CLOUD_URL=off`
-deployment, the EE deploy template's factory default) an unprivileged member got
-"access denied" for a surface that exists for nobody. That answer sends them to
-ask an administrator for a grant that would not help them, and it left the
-informational disabled state built in objectui#5504 unreachable for every
-non-admin. The runtime check now answers first, because "this deployment has no
-marketplace" is true regardless of who is asking.
-
-This restores the sibling-page invariant for the one class of viewer it still
-failed for: `MarketplacePackagePage` was reordered the same way in objectui#5533,
-so on a marketplace-off runtime the catalog page and the package detail page now
-give a non-admin the same kind of answer.
-
-Scope, deliberately narrow:
-
-- **Admin-first ordering stays correct where a marketplace exists.** On a runtime
- with `features.marketplace: true`, a non-admin still gets `MarketplaceAccessDenied`
- — the catalog is an install surface, and a member who cannot install has nothing
- to do with it. That boundary is pinned by an explicit test, not left to prose:
- without it, a change that simply dropped the admin check would look correct.
-- Nothing an admin sees changes, on either kind of runtime.
-- No new i18n keys, and no change to `MarketplaceAccessDenied` or
- `MarketplaceDisabled` themselves — only which of the two the page reaches for,
- and in which order it decides.
-- The disabled state is still the server's own answer (`features.marketplace`),
- never inferred from a failed request, and it still fails open.
diff --git a/.changeset/marketplace-detail-admin-guard-order-5583.md b/.changeset/marketplace-detail-admin-guard-order-5583.md
deleted file mode 100644
index b50d522485..0000000000
--- a/.changeset/marketplace-detail-admin-guard-order-5583.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The marketplace package detail page decides "you are not an admin" before it fetches,
-instead of after the load has already failed.
-
-`MarketplacePackagePage` ordered its early returns with the `!isAdmin` guard *after*
-both the loading branch and the `error || !data` branch, and gated its two fetch
-effects on `features.marketplace` alone. On a runtime that mounts a marketplace, a
-non-admin who opened a package URL was therefore walked through the fetch and the
-skeleton, and — when the load failed — was handed the destructive "Failed to load
-package" card carrying the server's own error message. Whether that viewer was
-refused or handed a diagnosis about a surface they are not allowed to use came down
-to whether an unrelated request happened to succeed.
-
-The guard now sits ahead of both branches, and `getMarketplacePackage` and
-`getCloudInstallationInfo` are gated on `isAdmin` as well, so the page stops issuing
-requests on behalf of a viewer it has already decided to turn away. That is the
-discipline objectui#5533 established on this same page for `features.marketplace`,
-applied to the other predicate that decides the same thing. It is also the ordering
-`MarketplacePage` carries after objectui#5557, so the two sibling pages now answer one
-runtime the same way for every viewer. The server remains the authority on what a
-non-admin may fetch; this only stops the client doing work it would discard.
-
-Unchanged for an admin, deliberately and under test: a failing load still produces the
-destructive card with the server's message intact, and a successful one still renders
-the package. A "fix" that hoisted the refusal unconditionally, or that deleted the
-failure branch, would satisfy every non-admin assertion and fail those two.
-
-`loading` stays seeded from `marketplaceEnabled` alone rather than from
-`marketplaceEnabled && isAdmin`. `isAdmin` reads `activeMember`, which `AuthProvider`
-resolves asynchronously *after* the session settles, so an admin whose role comes from
-the org member row renders once as a non-admin before the flag flips. Seeding `false`
-there would leave that first admin render with `loading: false` and no data — the
-destructive card, painted for a frame before the effect could raise the flag again.
-`MarketplacePackagePage.guardOrder.test.tsx` pins the flip case for that reason, along
-with the ordering, the skipped requests, and the marketplace-off boundary the guard
-must not jump above.
diff --git a/.changeset/marketplace-detail-disabled-state-5533.md b/.changeset/marketplace-detail-disabled-state-5533.md
deleted file mode 100644
index 672d7f993a..0000000000
--- a/.changeset/marketplace-detail-disabled-state-5533.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The marketplace **package detail** page now states that the marketplace is turned
-off, instead of red-erroring, on a runtime that has none (objectui#5533).
-
-`MarketplacePackagePage` takes the same `isMarketplaceEnabled()` early return its
-sibling catalog page took in objectui#5504, rendering the informational
-`MarketplaceDisabled` state when the server reports `features.marketplace: false`
-(an `OS_CLOUD_URL=off` deployment — the EE template's factory default). Until now
-the same runtime answered a bookmarked or pasted package URL with a destructive
-"Failed to load package / Not found." card, so the two sibling pages reached
-opposite conclusions about one runtime: the catalog called it configuration, the
-detail page called it a failure.
-
-Both requests the page fires for its own view are skipped in that state — the
-package fetch and the cloud-installation probe — rather than fired and discarded:
-a discarded request still reaches the server and can race the destructive card
-onto the screen before the disabled state settles.
-
-Scope, deliberately narrow:
-
-- **Not** a "swallow all errors" change. With `features.marketplace: true` the page
- behaves exactly as before, and a package that genuinely is not there still
- renders the destructive card carrying the server's own message. The flag is the
- runtime's own answer, never inferred from the shape of a failure, and it fails
- open — a runtime that answers nothing keeps its detail page.
-- The `installLocal` surfaces are untouched. That is a different capability flag,
- and an air-gapped `OS_CLOUD_URL=off` runtime still has a working install-local
- path.
-- No new i18n keys: `marketplace.disabled.*` and `marketplace.action.backHome`
- already ship in all ten locale packs.
diff --git a/.changeset/marketplace-disabled-state-5504.md b/.changeset/marketplace-disabled-state-5504.md
deleted file mode 100644
index 4462a09136..0000000000
--- a/.changeset/marketplace-disabled-state-5504.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a
-first-class disabled state, and the load-failure hint describes the control plane
-the runtime was actually pointed at (objectui#5504).
-
-`apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory
-default, so a stock self-hosted stack has no marketplace at all. The Console still
-recommended one: Home led with "Start with a template" and "Browse App
-Marketplace", and the click landed on a red **Failed to load marketplace / Not
-found** card whose hint claimed this runtime "points at the public ObjectStack
-cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for
-exactly the deployment reading them — the operator had not left the default, and
-the advice pointed back at the template that told them to set `off`. "Marketplace
-disabled by configuration" is a configuration conclusion, not a load failure.
-
-- `isMarketplaceEnabled()` (`runtime-config`) reads the server's own
- `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the
- serving app's route table (objectstack#8356). It is never inferred from the shape
- of a failed request: a control plane that is merely DOWN leaves the flag `true`,
- so an outage still renders as an outage. Unknown fails OPEN.
-- The marketplace page renders an informational "App Marketplace is turned off"
- state — muted, not `destructive` — and issues no request it knows will 404.
-- Home's "Start with a template" cover greys out with a visible localized reason,
- and the "Browse App Marketplace" shortcut is withheld, exactly as they already
- are for the `manage_metadata` capability gate.
-- `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the
- configured control plane) and `failedHintSameOrigin`. The "points at the public
- cloud by default" sentence is gone: it was rendered unconditionally, including on
- every runtime whose operator had overridden `OS_CLOUD_URL`.
-
-All ten locale packs carry the new keys.
diff --git a/.changeset/master-detail-formtype-vocabulary.md b/.changeset/master-detail-formtype-vocabulary.md
deleted file mode 100644
index 00f7c1d493..0000000000
--- a/.changeset/master-detail-formtype-vocabulary.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-"@object-ui/plugin-form": patch
----
-
-`object-master-detail-form` declares `formType` as a closed vocabulary instead of a bare `string`.
-
-The block declared `formType` as `type: 'string'` while the sibling `object-form` declared the
-same key as an `enum`, and both funnel into the renderer that switches on those variant names. A
-value outside the vocabulary therefore matched no branch and fell through to the flat field list
-with no diagnostic — measured, a `formType` of `'wizzard'` renders the parent half with its
-authored sections silently gone.
-
-The declared set is `simple | tabbed`, measured against the master-detail composition rather than
-copied from the sibling's six: `drawer` and `modal` host the parent half in a portal dialog outside
-the master-detail container, so its single bottom Save bar has no form to submit; `wizard` mounts
-only the current step's fields and turns that Save bar into a `Next`; `split` renders inline but
-persists through `dataSource.create` instead of the atomic batch.
-
-Authoring-surface only. The manifest, the JSX-page compiler and the save gate now report an
-out-of-vocabulary value as `invalid-enum`; rejection at publish time remains `@objectstack/spec`'s.
diff --git a/.changeset/max-tool-roundtrips-deprecate-5605.md b/.changeset/max-tool-roundtrips-deprecate-5605.md
deleted file mode 100644
index 895bfcddfe..0000000000
--- a/.changeset/max-tool-roundtrips-deprecate-5605.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
-'@object-ui/types': patch
----
-
-`maxToolRoundtrips` on `ChatbotSchema` is deprecated: it is inert, and an author
-who sets it is now told so instead of being left believing the documented cap
-applies (objectui#5605).
-
-The key was declared authorable in `@object-ui/types` (interface and zod, with a
-description), threaded from the authored document through the chatbot renderer at
-three call sites, accepted by `useObjectChat`, given a default — and then dropped.
-Measuring the installed chat runtime says it cannot be honoured from here rather
-than that someone forgot to wire it: `@ai-sdk/react`'s `useChat` takes `ChatInit`
-plus throttle/resume, and `ChatInit` declares exactly one loop control — the
-boolean predicate `sendAutomaticallyWhen` — and no numeric cap under any
-spelling. The numeric knob was removed from `useChat` in a major, and its
-successor was renamed through `continueUntil` to `stopWhen` / `stepCountIs`,
-which the installed `ai` package declares only on `generateText`, `streamText`
-and the tool-loop agent settings — all server-side. ObjectUI is backend-agnostic,
-so it owns no server loop to cap either, and putting the number in the request
-body would only move the same dead key one hop onto a wire contract no backend
-reads.
-
-This is stage one of a two-stage retirement, so nothing an author already wrote
-breaks: the key still parses, still carries its declared shape, and the renderer
-still threads it. What changes is that it is now marked `@deprecated` in the
-interface, the zod description and the docs, and that authoring it logs a
-one-time notice naming the knob that does work — `planning.maxIterations` on the
-agent. A follow-up removes the declaration once this deprecation has shipped in a
-release.
diff --git a/.changeset/menu-icon-test-phantom-value.md b/.changeset/menu-icon-test-phantom-value.md
deleted file mode 100644
index ca89894e20..0000000000
--- a/.changeset/menu-icon-test-phantom-value.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: the two overlay-menu icon suites (`context-menu-item-icon`,
-`dropdown-menu-item-icon`) no longer transcribe the undeclared `value` key from
-the catalog fixtures objectui#7072 cleaned, and each block now records why the
-key is absent. No published source and no rendered behaviour changes.
diff --git a/.changeset/metadata-client-actor-retired-4834.md b/.changeset/metadata-client-actor-retired-4834.md
deleted file mode 100644
index 2aa8de93c1..0000000000
--- a/.changeset/metadata-client-actor-retired-4834.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/data-objectstack': minor
----
-
-**Breaking (published surface):** remove `options.actor` from `MetadataClient`'s
-`save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor`
-request header.
-
-The server stopped honouring that header. objectstack#7941 ruled that the
-recorded actor is the identity the request was authorized as, and removed the
-header limb from the `/meta` write resolver — attribution cannot drift from
-authorization. The option therefore typed cleanly, sent a header, and could not
-influence the audit or history row it appeared to address: a false affordance
-that promised attribution and silently failed to deliver it.
-
-Three declarations go: `MetadataClientSaveOptions.actor` (inherited by
-`MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`),
-and the inline `{ actor?: string }` on each of `publish` and `rollback`.
-`MetadataAuditEntry.actor` is unaffected — that is the server's read-back of
-who acted, and it remains the way to see attribution.
-
-Marked `minor` rather than `major` per this repo's version-alignment policy
-(the fixed group's major tracks `@objectstack`, and `major` in a changeset
-would drag all 39 packages off that cadence).
-
-No caller in this repo passed `actor`; the census found the only in-repo
-occurrence was the client's own unit test. Callers outside this repo that still
-pass it are unaffected at runtime beyond losing a header the server already
-ignored — the property is dropped rather than forwarded, pinned by
-`metadata-actor-retired-4834.pin.test.ts`.
diff --git a/.changeset/metadata-lock-state-5024.md b/.changeset/metadata-lock-state-5024.md
deleted file mode 100644
index 785458cbfb..0000000000
--- a/.changeset/metadata-lock-state-5024.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/data-objectstack': patch
-'@object-ui/app-shell': patch
----
-
-The metadata lock banner can no longer render an amber, padlocked box with no
-title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three
-times (objectui#5024).
-
-`MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four
-states out by hand, 42 lines apart in one file, compared by no gate. They are now
-one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s
-`z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies
-were restating a schema rather than filling a gap.
-
-The user-visible half is the banner. Its title was three independent `&&` branches
-with no fallback, while the switch that opens the banner is true for any non-`none`
-value — so a lock state outside the four opened the box and left the headline
-empty. That is reachable without a fifth state ever being added here:
-`MetadataClient.layered()` casts the wire value through unchecked, so a newer
-server reaches this banner as-is. Measured, not assumed — feeding `no-publish`
-through the page rendered the padlock, the border and an empty title. The title is
-now a keyed lookup with a loud fallback that names the unrecognised token, so a
-fifth state fails `type-check` here and, if one arrives from a server anyway, the
-operator reads a sentence instead of a blank box.
diff --git a/.changeset/nav-selection-root-keys-edge-5600.md b/.changeset/nav-selection-root-keys-edge-5600.md
deleted file mode 100644
index 5fd1bb1b3c..0000000000
--- a/.changeset/nav-selection-root-keys-edge-5600.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
----
-
-Internal layering fix, behaviour-unchanged: publishes nothing, declared explicitly
-with an empty frontmatter rather than left undeclared.
-
-Inverts the dependency edge objectui#5600 found: `nav-selection.ts` — otherwise pure
-URL-parsing plumbing (`parseSurfaceParam` / `formatSurfaceParam` / `parseNavSelParam`
-and friends) — statically imported `APP_NAV_ROOT_KEYS` from the 510-line
-`AppNavInspector.tsx`, so every consumer of the URL helpers (including two small
-utilities, `console/ai/artifactStudioPath.ts` and, transitively via
-`useSurfaceDeepLink.ts`, the studio-design surface) pulled a React inspector
-component into their static import graph for one array of root keys.
-
-`APP_NAV_ROOT_KEYS` (`['nav', 'navigation', 'tabs', 'items', 'menu']`) is a plain
-string-literal array — it describes nav SHAPE, not the inspector — so it now lives in
-`nav-selection.ts` instead. `AppNavInspector.tsx` never read the array itself, only
-re-exported it (`export const APP_NAV_ROOT_KEYS = ROOT_KEYS;` as its last line), so
-the fix is a straight move with no back-import needed on that side.
-
-Verified structurally rather than by reading the import line: a BFS over static
-(non-type, non-dynamic) relative imports from `nav-selection.ts` and the other
-non-test importers no longer reaches `AppNavInspector.tsx` (158 files visited, target
-not found), where the same walk over the pre-fix tree found it in 6 files via
-`nav-selection.ts -> AppNavInspector.tsx` directly.
diff --git a/.changeset/nervous-donkeys-tap.md b/.changeset/nervous-donkeys-tap.md
deleted file mode 100644
index 31a9d1b170..0000000000
--- a/.changeset/nervous-donkeys-tap.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-Studio workbench rails converge on an AI copilot apply without a page reload (objectui#7255)
-
-The copilot is the right dock of the Studio document and already announces
-every authoring turn on the assistant bus (`emitMetadataRefresh`), but the
-four pillar rails never subscribed: a just-applied object or nav item stayed
-invisible until the author reloaded the page. The Interfaces / Data /
-Automations / Access rails now list that pulse as a load dependency and refetch
-their package-scoped reads in place. The Interfaces rail holds the pulse while
-the nav editor has unsaved edits (its load rehydrates the nav edit buffer) and
-releases it once they settle, and re-reading the same package no longer flaps
-the app status back through `loading`.
diff --git a/.changeset/network-escape-batch1.md b/.changeset/network-escape-batch1.md
deleted file mode 100644
index 7473843dc6..0000000000
--- a/.changeset/network-escape-batch1.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: the five network-escape files in batch 1 of objectui#7307 now serve their `/api/v1/security/explain` and `/api/v1/meta/object/` probes from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. No published behaviour changes — no product source is touched.
diff --git a/.changeset/network-escape-batch2.md b/.changeset/network-escape-batch2.md
deleted file mode 100644
index f80e7f7caa..0000000000
--- a/.changeset/network-escape-batch2.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: the four network-escape files in batch 2 of objectui#7307 (three in `plugin-kanban`, one in `plugin-gantt`) now serve their `/api/v1/security/explain` probe from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. No published behaviour changes — no product source is touched.
diff --git a/.changeset/network-escape-batch3.md b/.changeset/network-escape-batch3.md
deleted file mode 100644
index f30fed98fe..0000000000
--- a/.changeset/network-escape-batch3.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: the four `plugin-detail` network-escape files in batch 3 of objectui#7307 now serve their probes from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. One of the four also reached `/api/task/42` through `DetailView`'s `api` branch, so its router serves that route too. No published behaviour changes — no product source is touched.
diff --git a/.changeset/network-escape-batch4.md b/.changeset/network-escape-batch4.md
deleted file mode 100644
index aa1dd6a38d..0000000000
--- a/.changeset/network-escape-batch4.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only change: the four `app-shell` `console/home` network-escape files in batch 4 of
-objectui#7307 now serve their `/api/v1/meta/_drafts` probe from a recording double instead
-of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. The
-double answers an empty draft ledger, which is what `PendingDraftsBanner` already rendered
-from the failed read, so no assertion moves. No published behaviour changes — no product
-source is touched.
diff --git a/.changeset/network-escapes-batch5.md b/.changeset/network-escapes-batch5.md
deleted file mode 100644
index 1967c76f96..0000000000
--- a/.changeset/network-escapes-batch5.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only (objectui#7307 batch 5): three app-shell suites now serve their
-`GET /api/v1/meta/object`, `GET /api/v1/automation/_status` and
-`/api/v1/ai/conversations` probes from recording doubles instead of a real
-socket, and their three rows leave the network-escape ledger. No published
-runtime code changes, so nothing to release.
diff --git a/.changeset/network-escapes-batch6.md b/.changeset/network-escapes-batch6.md
deleted file mode 100644
index 40594bf6f0..0000000000
--- a/.changeset/network-escapes-batch6.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Test-only (objectui#7307 batch 6, the last): `FlowNodeInspector.specKeys.test.tsx`
-now serves its `GET /api/v1/meta/object` probe from a recording double instead of
-a real socket, which empties the network-escape burn-down ledger. The ledger is
-therefore RETIRED with it — the guard's `KNOWN_ESCAPES` set, its attributed-stderr
-branch for listed files and its known/unknown split are gone, exactly as the
-ledger's own non-vacuity floor prescribed for reaching zero, and
-`scripts/__tests__/network-escape-ledger.test.ts` now pins that the machinery
-stays gone. The standing guard from objectui#6640 is untouched and now covers
-every file: any test that reaches a real socket is red on its first run. No
-published runtime code changes, so nothing to release.
diff --git a/.changeset/node-gate-data-predicate-report-5687.md b/.changeset/node-gate-data-predicate-report-5687.md
deleted file mode 100644
index 5465efc0e4..0000000000
--- a/.changeset/node-gate-data-predicate-report-5687.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/react': patch
----
-
-`SchemaRenderer`'s node visibility gate now emits a dev-only warning when a
-predicate reads `data.*` and the data-source adapter cannot answer that read, so
-an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the
-constant they authored instead of shipping it (objectui#5687).
-
-At the node tier `data` is the data-source **adapter** — the object `${data.total}`
-in a props bag resolves against — and it has never been the row. A predicate
-written with the deprecated `data.*` spelling therefore resolves `undefined ==
-'draft'`, which is a perfectly good `false`: the block is hidden on every row, and
-because it does not throw, the unresolvable-predicate reporter added for
-objectui#5454 never fired. Measured on this base, the same predicate written as a
-`{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether
-an author heard about the identical mistake depended on which dialect they happened
-to write it in, which is the arbitrariness objectui#5454 existed to remove.
-
-**No verdict changes and no interpolation changes** (maintainer ruling,
-2026-08-22, option A). The node tier keeps its documented `data` = adapter
-semantics; objectui#5330's row binding does not extend here. The evaluator's answer
-is returned exactly as computed and `${data.*}` interpolation is untouched — only
-the silence moved.
-
-The report fires on a `data.*` read the bound adapter answers with `undefined`, not
-on the spelling. A genuine adapter read stays silent (`data.total > 0` against an
-adapter carrying `total`), a canonical `record.*` predicate stays silent, and a
-correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per
-node type + key + predicate source — the same module, Set and lifecycle as the
-objectui#5454 reporter.
-
-This loudness is temporary by design: it dissolves when objectui#5330's deprecation
-window for the `data.*` row spelling closes.
diff --git a/.changeset/normalise-client-error-user-message-5901.md b/.changeset/normalise-client-error-user-message-5901.md
deleted file mode 100644
index d6739f5405..0000000000
--- a/.changeset/normalise-client-error-user-message-5901.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/data-objectstack': patch
----
-
-Preserve the producer's `userMessage` marking when `normaliseClientError` re-wraps a refusal.
-
-`ApiErrorSchema.userMessage` (objectstack#9934) is the opt-in channel an application author
-sets at throw time to say "this text is for the end user", and the contract states it
-status-agnostic — any refusal status may carry it. Both of the shapes this adapter re-wraps
-into typed errors dropped the marking: a hook that refused a write with `VALIDATION_FAILED`
-or `CONCURRENT_UPDATE` and marked its own sentence had that sentence discarded at the
-adapter boundary, before any surface could render it. Nothing threw and the typed error was
-otherwise correct, so the only symptom was the user reading a generic string instead of the
-sentence their administrator wrote.
-
-The marking now rides both re-wraps, in the form the shared reader (`declaredUserMessage`)
-already looks for: on the details bag for `DataApiValidationError`, exactly the way `fields`
-already survives, and on a new readonly `userMessage` member for `ConcurrentUpdateError`,
-which has no details bag. Unmarked refusals are untouched — they carry no key and still read
-as `null`, so nothing a producer did not opt into can reach a user.
diff --git a/.changeset/notready-t-interpolates.md b/.changeset/notready-t-interpolates.md
deleted file mode 100644
index 5c806e4ec1..0000000000
--- a/.changeset/notready-t-interpolates.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-fix(i18n): `useObjectTranslation`'s provider-less `t` now interpolates its inline `defaultValue`
-
-With no `I18nProvider` mounted, react-i18next hands back its not-ready `t`, which
-returns `options.defaultValue` **verbatim** — so an inline default written
-`'Deleted {{count}} rows'` reached the user with the braces intact. 68 inline
-defaults across 24 files rendered through that path on any host that embeds an
-ObjectUI component without a provider, which is the configuration
-`createSafeTranslation` exists for.
-
-`useObjectTranslation` now runs its not-ready result through the same one
-interpolator `createSafeTranslation`'s `fallbackT` uses, so both provider-less
-renderers fill exactly the `{{name}}` spelling the copy is already gated to. The
-ready path is untouched: with a provider, i18next's own `t` is returned by
-reference and nothing is interpolated twice. Pre-interpolated template-literal
-defaults (`` `Deleted ${n} rows` ``) stay correct — they have no holes left to
-fill — so no call site changes.
diff --git a/.changeset/npm-presence-publish-predicate-5442.md b/.changeset/npm-presence-publish-predicate-5442.md
deleted file mode 100644
index c77030eafe..0000000000
--- a/.changeset/npm-presence-publish-predicate-5442.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
----
-
-CI only — this publishes nothing, declared explicitly with an empty frontmatter rather
-than left undeclared.
-
-`.github/workflows/changeset-release.yml` keys its publish lane on **"is the version this
-commit declares already on npm?"** instead of on **"are changesets pending?"**
-(objectui#5442, maintainer ruling 2026-08-22, Option B).
-
-The two predicates read as interchangeable and come apart exactly where it costs a
-release. The version PR is cut from `main` at T and merged at T+n; `main` takes ~18
-merges a working day and the merge does not remove the changesets that landed in
-between. Those belong to the *next* version, but keyed on them the version this commit
-just bumped to is skipped, and the next version PR bumps straight past it. Measured on
-`bc21c704b`: of the 90 versions `packages/core/CHANGELOG.md` declares, 16 never reached
-npm, and the repository said `17.6.0` while `dist-tags.latest` said `17.5.0`.
-
-The npm predicate is also cheaper than the one it replaces rather than a trade against
-it: an ordinary landing does not move the manifest version, so it answers "already
-published" and the expensive job is skipped.
-
-Alongside it, a **loud check**. The defect was never a red run — run 3370 on `cfeb378b5`
-completed `success` having published nothing — so the publish lane now reads the registry
-back and fails if the version it exists to ship is still absent. A repo/npm divergence is
-a failing run, not something an audit finds 16 versions later.
-
-The refresh lane is untouched and still cannot publish: no `publish:` input and no npm
-credentials in its `env:`, the double denial that keeps a scheduled tick from releasing
-something nobody merged.
diff --git a/.changeset/object-schema-metadata-derive-from-spec.md b/.changeset/object-schema-metadata-derive-from-spec.md
deleted file mode 100644
index 501ecb2388..0000000000
--- a/.changeset/object-schema-metadata-derive-from-spec.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/app-shell': patch
-'@object-ui/react': patch
----
-
-`ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject`
-instead of being a hand-written copy (objectui#5362; maintainer ruling
-2026-08-20: the object document type belongs to the spec).
-
-What changes on the published type surface:
-
-- **Gained:** the full spec object-document surface, including the three keys
- the runtime already reads but the old interface rejected as excess
- properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`,
- `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of
- the spec document).
-- **Removed:** nine members the old interface declared that no objectui
- runtime code reads and the spec document does not know: `extends`,
- `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling
- is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`.
- `ObjectTrigger` and `ObjectRelationship` remain exported unchanged.
-- **Kept:** `editMode` — the one measured client-side member the runtime reads
- (`recordFormNavigation` / `AppContent`) — now declared on the new
- `ObjectSchemaClientExtensions` interface, which the derivation intersects.
- Note the spec's strict parse rejects `editMode` on published documents
- (`unrecognized_keys`); it is a client-type member only.
-
-Spelling settlement: `listViews` (camelCase) is canonical — `list_views`
-appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in
-`@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling
-READ fallback for stored pre-settlement documents (that stock has never been
-censused — objectstack#7917); the CRUD guide and its pinned transcription now
-author the canonical spelling.
diff --git a/.changeset/object-view-unmirrored-keys-7779.md b/.changeset/object-view-unmirrored-keys-7779.md
deleted file mode 100644
index 65ab787d01..0000000000
--- a/.changeset/object-view-unmirrored-keys-7779.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-'@object-ui/types': minor
----
-
-**Breaking for authored metadata:** `ObjectViewSchema.viewTabBar` is RETIRED
-(objectui#7779, maintainer ruling B of 2026-09-06; ADR-0049 enforce-or-remove).
-An `object-view` node that authors `viewTabBar` no longer validates: the parse
-fails loudly on the `viewTabBar` path with the explanation in the message, and
-the TS member is a `?: never` tombstone so the same document is refused at
-compile time. Nothing ever read the key off the node — the tab-bar UX config
-(`ViewTabBarConfig`, still exported) is the `config` prop of the `ViewTabBar`
-component, composed by the host, not authored metadata. Remove the key.
-
-In the same change, eight `ObjectViewSchema` keys the TypeScript interface
-declared and the Zod mirror never did are now ENFORCED. Until now a document
-authoring any of them passed the validator unexamined through `BaseSchema`'s
-passthrough while the published type invited the author to write it — declared
-but not enforced (objectui#7279's `UnmirroredDeclared` reading). Each key now
-admits exactly what the declaration promises and refuses a wrong-typed value at
-its own path; a correctly typed document is untouched.
-
-| key | disposition | how |
-| --- | --- | --- |
-| `navigation` | mirrored | the spec's `ListViewSchema.navigation` slot by reference (`NavigationConfigSchema`, optional) |
-| `searchableFields` | mirrored | the spec's `ListViewSchema.searchableFields` slot by reference (`string[]`, optional) |
-| `filterableFields` | mirrored | the spec's `ListViewSchema.filterableFields` slot by reference (`string[]`, optional; the spec marks it a legacy shorthand for `userFilters.fields`) |
-| `allowCreateView` | mirrored | the sibling `ViewSwitcherSchema.allowCreateView` slot by reference — the renderer forwards the value verbatim into the `view-switcher` node it composes |
-| `viewActions` | mirrored | the sibling `ViewSwitcherSchema.viewActions` slot by reference, for the same reason |
-| `defaultViewType` | mirrored | local literal: the declaration's seven-value union (`grid`, `kanban`, `gallery`, `calendar`, `timeline`, `gantt`, `map`) — read as `schema.defaultViewType \|\| 'grid'` |
-| `defaultListView` | mirrored | local literal: `string` — read as `namedListViews?.[schema.defaultListView]` |
-| `showViewSwitcher` | mirrored | local literal: `boolean` — read as `schema.showViewSwitcher === true` |
-| `viewTabBar` | RETIRED | `?: never` + `retirementTombstone()` — zero reads on the node |
-| `listViews` | unchanged (still unmirrored) | see below |
-
-**What was measured.** Every reading was taken on the `object-view` node
-renderer (`packages/plugin-view/src/ObjectView.tsx`, registered by
-`plugin-view/src/index.tsx`) with `schema.objectName` / `schema.layout` as the
-positive controls of the same `schema.KEY` query, so each zero is a reading;
-the repo-wide census of `viewTabBar` finds the key in no source file outside
-`@object-ui/types` (two doc tables listed it as authorable and are corrected
-here). The spec side was read through the installed pin
-(`@objectstack/spec@17.2.0`, `ui` entry, 117 exported object schemas walked;
-control keys `objectName` / `columns` / `navigation` / `listViews` hit): the
-three spec-modelled keys are optional slots on `ListViewSchema` and
-`ObjectListViewSchema`; the six local keys have no spec slot anywhere.
-
-**`listViews` stays unmirrored, on the ruling's own fallback clause.** The
-declaration's value is the local `NamedListView` — **47 declared top-level
-members**, of which the renderer reads six: `label`, `type`, `columns`,
-`filter`, `sort`, `options`. It reads a seventh key off a named view, `data`,
-but `data` is **not a declared member of `NamedListView` at all**: it reaches
-the renderer through an `as any` cast on the named-view config in
-`packages/plugin-view/src/ObjectView.tsx`, so it never was one of the declared
-members a mirror would have to carry. The spec's `ViewSchema.listViews` is a
-record of the STRICT `ObjectListViewSchema`, which requires `columns` and
-refuses `options`, ObjectQL tuple filters and `default` — that is, it refuses
-the named views this package's own README and
-`content/docs/api/schema-reference.md` teach (`{ label: 'All Users' }` fails at
-`columns`; `filter: [["owner", "=", "..."]]` fails at `filter.0`). Mirroring the
-spec value would lose documented behaviour; mirroring the local value would
-enforce **41 unread members** (47 declared, minus the 6 that are both declared
-and read) into the contract — the
-very thing ruling B refused for the six local keys. The key therefore stays in
-the parity ledger with that measurement, pinned, until the maintainer decides its
-value type. It is not papered over with `z.any()`.
-
-**Who is affected:** an author who wrote `viewTabBar` on an `object-view` node
-(remove it), or who wrote a wrong-typed value for one of the eight keys — e.g.
-`defaultViewType: 'tree'` (host-composition-only, objectui#5321),
-`navigation: 'page'` (write `navigation: { mode: 'page' }`),
-`searchableFields: 'name'` (write an array), `viewActions: 'share'` (write
-`[{ type: 'share' }]`). Such documents used to pass validation and render with
-the key ignored; they now fail at the key with the reason.
-
-**Who is NOT affected:** every correctly typed document, and every document
-that never wrote these keys — `absent` stays valid on all nine. No renderer
-changed. The parity ledger (`zod-mirror-parity.test.ts`) records the move:
-`UnmirroredDeclared` 14 entries / 96 keys to 14 / 87, the `ObjectViewSchema`
-entry re-derived into the SPEC-DERIVED half because the mirror now references
-the spec in code.
-
-Graded `minor`, not `patch`: this narrows the accepted input set, which is
-breaking for any author who wrote the tolerated spellings. It is not `major`
-per this repo's fixed-group convention (objectui's own breaking changes ship as
-`minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
-mechanically enforced by `scripts/check-changeset-no-major.mjs`).
diff --git a/.changeset/objectchart-fieldoptionlabel-ref-5587.md b/.changeset/objectchart-fieldoptionlabel-ref-5587.md
deleted file mode 100644
index 4b61e3f15d..0000000000
--- a/.changeset/objectchart-fieldoptionlabel-ref-5587.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-`ObjectChart` now depends on the `fieldOptionLabel` resolver directly instead of
-holding it behind a ref, so a chart re-resolves its groupBy option labels when
-the resolver genuinely changes (objectui#5587).
-
-The ref existed for a reason that no longer holds. `useSafeFieldLabel()` returned
-a fresh object on every render outside an i18next provider, so a direct
-dependency made `fetchData`'s `useCallback` identity fresh on every render, and
-the effect that depends on `fetchData` refetched on every render — an unbounded
-loop. `ObjectChart` worked around that locally with `fieldOptionLabelRef` plus a
-`useEffect` keeping it current. `useObjectLabel`'s memo now holds with or without
-an i18next instance bound (objectui#5564), so the resolver's identity is stable
-on both paths and the indirection buys nothing.
-
-It did cost something, and that is the user-visible half: a ref-hidden dependency
-meant `fetchData` did NOT re-run when the resolver changed. A chart mounted
-before its `I18nProvider`, or rendered across a language switch, kept serving
-groupBy labels resolved by the old resolver until some unrelated dependency
-(object name, filter, aggregate) happened to move. It now refetches once on that
-transition and shows labels in the active language.
-
-Pinned by `ObjectChart.fieldOptionLabelRefetch.test.tsx`, which counts fetches
-across forced re-renders both outside and inside a provider. Reverting
-`useObjectLabel.ts` to its pre-objectui#5564 state turns the no-provider case red
-(2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so
-the removal is pinned to the fix that unlocked it rather than to a comment.
diff --git a/.changeset/objectmap-marker-title-resolver-5953.md b/.changeset/objectmap-marker-title-resolver-5953.md
deleted file mode 100644
index fa48d3279b..0000000000
--- a/.changeset/objectmap-marker-title-resolver-5953.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/plugin-map': patch
----
-
-`ObjectMap` now resolves marker titles through `@object-ui/core`'s
-`getRecordDisplayName` (ADR-0079), instead of reading a hard-coded `'name'` key.
-
-`getMapConfig` used to fill an absent title binding with the string literal
-`'name'`, and the marker transform then did a bare `record[titleField]` read. For
-every object whose display field is not literally `name`, that read was
-`undefined`, so each marker popup titled itself `undefined`. The literal is gone
-from both branches that carried it, and the read site hands the decision to the
-same resolver `ObjectKanban`, `ObjectCalendar` and `ObjectGantt` already title
-their items through — `ObjectMap` was the fourth renderer and the only one still
-outside it.
-
-An authored `map.titleField` keeps winning outright: it is passed through as the
-resolver's explicit `titleField` option, which it checks first. What is new
-underneath is everything a static field-name binding cannot express — the
-object's declared `nameField`, its deprecated `displayNameField` alias, a legacy
-`titleFormat` template, type-aware field derivation, and a name-ish probe over
-the record's own keys for the inline-data case where no object definition is
-fetched at all.
-
-Records with no resolvable name now read `Record #` rather than `undefined`
-or a uniform `Marker`; the `Marker` placeholder is kept only for a record that
-carries no id either. No authoring surface changes and no new map config keys.
diff --git a/.changeset/objectmap-schema-data-shorthand-5305.md b/.changeset/objectmap-schema-data-shorthand-5305.md
deleted file mode 100644
index c21345fce8..0000000000
--- a/.changeset/objectmap-schema-data-shorthand-5305.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/plugin-map': patch
----
-
-`ObjectMap` reads `schema.data` in one place again, so an array-shorthand map stops
-making a metadata request it never uses.
-
-The fetch effect carried a second short-circuit beside the `props.data` one
-objectui#5003 fixed: it read `schema.data` directly and tested whether that value was
-itself an array. eslint reported it as `missing dependency: schema.data` — the last
-`react-hooks/exhaustive-deps` warning on that effect.
-
-The dependency was never actually missing. `getDataConfig(schema)` already returns
-`schema.data` verbatim, and the result is memoized on `JSON.stringify(rawDataConfig)`
-into `dataConfig`, which **is** one of the effect's declared dependencies. The authored
-rows therefore reached the effect before this change; the direct read was a duplicate of
-an already-threaded value, which is why neither adding a dependency nor deleting the
-branch was right.
-
-The array handling moved into `getDataConfig`, where `ObjectGrid`'s own `getDataConfig`
-already pins the same normalization (`"Check if data is an array (shorthand format)"`).
-Same rows render, and the effect now reads only `dataConfig`.
-
-One behavioural consequence, and it is the point: an array under `data` now yields
-`provider: 'value'`, so `hasInlineData` is true and the sibling effect no longer calls
-`dataSource.getObjectSchema()` for it. That request's only read site is
-`buildExpandFields()` inside the object-provider fetch branch, which an inline schema
-never reaches — so the call was pure waste, and the shorthand now behaves exactly like
-the declared `{ provider: 'value', items }` form it is shorthand for.
-
-Deleting the branch instead was measured, not assumed: with no producer-side handling,
-an array-shorthand map renders `Error: DataSource required for object/api providers`
-rather than its markers. The shorthand is a live convention in six sibling blocks
-(`ObjectGrid`, `ListView`, `ObjectTree`, `ObjectChart`, `ObjectDataTable`,
-`calendar-view-renderer`), so `object-map` would have become the one block in the family
-that answers it with an error box.
diff --git a/.changeset/objectpivottable-i18n-resolver-refs-5625.md b/.changeset/objectpivottable-i18n-resolver-refs-5625.md
deleted file mode 100644
index b5f4cc6308..0000000000
--- a/.changeset/objectpivottable-i18n-resolver-refs-5625.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel`
-resolvers directly instead of holding them behind refs, so a pivot re-derives
-its header and option labels when the resolver genuinely changes
-(objectui#5625).
-
-The refs existed for a reason that no longer holds. `useSafeFieldLabel()`
-returned a fresh object on every render outside an i18next provider, so a direct
-dependency would have re-run the metadata-derivation effect on every render —
-and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly
-built objects, so each run scheduled the next one: an unbounded derive loop.
-`ObjectPivotTable` worked around that locally with `fieldLabelRef` /
-`fieldOptionLabelRef` plus a `useEffect` keeping them current.
-`useObjectLabel`'s memo now holds with or without an i18next instance bound
-(objectui#5564), so both resolvers have a stable identity on both paths and the
-indirection buys nothing.
-
-It did cost something, and that is the user-visible half: a ref-hidden
-dependency meant the derivation did NOT re-run when the resolver changed. A
-pivot mounted before its `I18nProvider`, or rendered across a language switch,
-kept showing its top-left header label and its select-option cell labels as
-resolved by the old resolver — until some unrelated dependency (the data source,
-the object name) happened to move. It now re-derives once on that transition and
-renders in the active language.
-
-This is the same removal objectui#5587 made in `ObjectChart`, one package over.
-
-Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations
-across forced re-renders both outside and inside a provider, checks that the two
-derived state maps settle, and asserts the language-switch re-derivation.
-Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the
-no-provider case red (derivations in the dozens instead of 1), so the removal is
-pinned to the fix that unlocked it rather than to a comment.
diff --git a/.changeset/objectview-overlay-pair-completeness-5025.md b/.changeset/objectview-overlay-pair-completeness-5025.md
deleted file mode 100644
index 85273d19a3..0000000000
--- a/.changeset/objectview-overlay-pair-completeness-5025.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`sanitizeViewOverride` now strips a half-filled `between` from a stored view
-overlay, instead of handing it back to the merge (objectui#5025,
-objectstack#8815).
-
-The overlay recovery pass asked "is this filter row filled in?" with the
-shape-blind predicate objectstack#8815 retired — `value == null || value === ''
-|| (Array.isArray(value) && value.length === 0)`. That is correct for `scalar`
-and `list` and blind to `pair`: a `between` carrying one bound is
-`['2024-01-01', '']`, an array of length 2, so the pass read it as a real
-condition and kept it. The two write paths (`plugin-list`'s `ListView` and
-app-shell's `viewFilterFold`) were converted to the builder's arity-aware
-`isFilterValueComplete`; this read path was the third verbatim copy, and the one
-whose job was to clean up exactly the rows the other two used to write. Until
-now a stored half-range survived the pass, reached the query, and the server
-refused the whole view (`400 INVALID_FILTER`) for every user on every later
-read.
-
-The pass now delegates to the same `isFilterValueComplete` the write paths use,
-on both of the at-rest shapes it handles (the spec `ViewFilterRule` object and
-the legacy runtime triple). A complete range — bounds of `0` and `false`
-included — is untouched, and the `scalar` and `list` families read exactly as
-before, since the retired predicate was already right for them. One shape is
-newly stripped beyond the half-filled array: a `between` whose value is a bare
-scalar, which `ViewFilterRuleSchema` itself refuses.
-
-Which operators want no value at all is unchanged and still answered by
-app-shell's `VALUELESS_FILTER_OPERATORS` — that set is already derived from the
-builder's `VALUELESS_FILTER_BUILDER_OPERATORS` rather than being a private copy,
-and its parity is pinned.
diff --git a/.changeset/objectview-sort-sink-4869.md b/.changeset/objectview-sort-sink-4869.md
deleted file mode 100644
index 9408f95811..0000000000
--- a/.changeset/objectview-sort-sink-4869.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/plugin-view': patch
----
-
-**Bug fix:** a non-grid `object-view` (calendar / kanban / gallery / timeline)
-whose sort came from the deprecated `table.defaultSort` no longer comes back
-empty. `ObjectGridSchema.defaultSort` is declared a single `{ field, order }`
-object, and `ObjectView`'s own fetch handed it to `$orderby` verbatim — where
-the ObjectStack adapter reads it as an `$orderby` map and folds it with
-`Object.entries`, so the request went out sorting by two columns literally
-named `field` and `order`. The server answers `400 INVALID_SORT`, `ObjectView`
-swallows the error, and the view rendered with no records — while the *same*
-metadata sorted correctly as a grid, because `ObjectGrid` lowers that pair
-before using it (objectui#4869, maintainer ruling 2026-08-22).
-
-`ObjectView` now performs the same legacy-to-canonical lowering `ObjectGrid`
-already does (`sort ?? (defaultSort ? [defaultSort] : undefined)`) and routes
-the whole chain — named view sort, `views` prop sort, `table.sort`,
-`table.defaultSort` — through the shared `convertSortToQueryParams` sink. This
-was the last object-bound read site sending an authored sort to `$orderby`
-unlowered; every other block (gantt, map, calendar, timeline,
-`record:line_items`) already did, so an adapter that implements `find` itself
-now receives one normalized `Record` from all of them instead
-of two different dialects.
-
-Precedence is unchanged, and both spellings of the pair keep working. The
-shared sink was deliberately **not** widened to accept a bare `{ field, order }`:
-its input slot also legitimately carries `$orderby`'s own map, in which
-`{ field: 'desc' }` is a valid ordering by a column named `field`, so widening
-it would make one shared function guess.
-
-Two visible shape changes on the wire, both semantically identical to before: a
-string `table.sort` such as `'name desc'` now serializes as `-name` rather than
-riding through as `name desc`, and a `SortConfig[]` arrives as a map rather than
-as an array.
diff --git a/.changeset/olive-cameras-repeat.md b/.changeset/olive-cameras-repeat.md
deleted file mode 100644
index 6688e0b3d6..0000000000
--- a/.changeset/olive-cameras-repeat.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-"@object-ui/plugin-map": patch
----
-
-`ObjectMap`: make the marker `useMemo` actually memoize.
-
-`getMapConfig(schema)` ran unmemoized in the render body, so `mapConfig` carried
-a fresh object identity on every render. The marker transform names `mapConfig`
-in its dependency array, so it recomputed on every single render — walking every
-record through `extractCoordinates` and the display-name resolver, and re-running
-`ObjectMapConfigSchema.safeParse` on each pass — while declaring that it does not.
-The invalidation cascaded on into `filteredMarkers`, `clusteredData`,
-`markerBounds` and `initialViewState`.
-
-`mapConfig` is now memoized on `schema`, the one value `getMapConfig` reads.
-Behaviour is unchanged; the config is still rebuilt whenever the schema
-genuinely changes.
diff --git a/.changeset/olive-crabs-shave.md b/.changeset/olive-crabs-shave.md
deleted file mode 100644
index 6d6476f633..0000000000
--- a/.changeset/olive-crabs-shave.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Every envelope reader on the package surfaces now renders a producer-marked
-`error.userMessage`, and the Studio package list opens the failure body at all.
-
-`fetchPackages` — the `/api/v1/packages` read behind the Studio switcher, the writability
-courtesy gate, the namespace lookup and the builder landing page — answered a refusal with
-`HTTP ` and never opened the body, so `message`, `code` and `userMessage` were
-discarded together: a 403 whose envelope said `Reading packages requires the studio.access
-or setup.access capability.` reached the author as four characters. `apiJson` on the
-package admin page read `error.message` and never the mark, and rendered no code.
-`duplicatePackage` read `error.message` alone.
-
-All three now ask one shared rule (`readEnvelopeFailureText`): a producer-marked
-`userMessage` outranks the diagnostic `message` at any status — presence of the field is
-the producer's marking, and a consumer that sees it renders it verbatim — with `error.code`
-appended to whichever prose won. An unmarked refusal is unchanged and still renders its
-diagnostic; a body carrying no prose still falls back to each reader's own status text.
diff --git a/.changeset/olive-donkeys-invent.md b/.changeset/olive-donkeys-invent.md
deleted file mode 100644
index b3139d133f..0000000000
--- a/.changeset/olive-donkeys-invent.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-"@object-ui/app-shell": patch
----
-
-ObjectView no longer forwards `chart.config` onto the `object-chart` node it builds.
-
-`@objectstack/spec`'s `ListChartConfigSchema` is a `strictObject` declaring exactly
-`chartType` / `dataset` / `dimensions` / `values`, and objectui binds it by reference, so
-`config` is refused by name (`unrecognized_keys ["config"]`) — by the client schema and by
-the same schema the platform's metadata write door parses every save through. The rung was
-therefore a channel no conforming author could feed, on both the dataset and the legacy
-branch, and it landed on a node that does not declare `config` either.
-
-For every spec-conforming view this is a no-op. A non-conforming row that somehow carried
-the key degrades rather than breaks: `ChartRenderer` generates a container config from
-`series` plus a positional palette when none is present, so the chart still renders, with
-series-derived labels and default colours.
diff --git a/.changeset/olive-donkeys-scream.md b/.changeset/olive-donkeys-scream.md
deleted file mode 100644
index 36ffa1eb22..0000000000
--- a/.changeset/olive-donkeys-scream.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Pin the form-view option element's two narrowings (objectui#6263): a per-option `default`
-written on a metadata-admin form field stays refused at the type level, and the re-pointed
-`visibleWhen` still refuses the spec's `dialect`-only envelope. Type-level pins plus a
-tombstone comment recording the 2026-08-28 ruling; no published behaviour changes.
diff --git a/.changeset/olive-donkeys-shave.md b/.changeset/olive-donkeys-shave.md
deleted file mode 100644
index 6e765665f9..0000000000
--- a/.changeset/olive-donkeys-shave.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-'@object-ui/plugin-dashboard': minor
----
-
-Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846).
-
-The flat branch now requests the `[]` marginal grouping — the same server-side
-totals machinery the cross-tab has used since framework#1753 — and renders the
-answer under the console's existing `dashboard.total` label. Totals are computed
-by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/
-`max` reports the whole-set aggregate); nothing is recombined client-side. The
-row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null
-bucket, and never joins the drillable rows.
-
-No footer is rendered when the executor answers no `[]` grouping, and none is
-requested for a table truncated by `options.limit`, whose visible rows a
-whole-set total could not be reconciled against.
diff --git a/.changeset/olive-donkeys-smile.md b/.changeset/olive-donkeys-smile.md
deleted file mode 100644
index e643a315e7..0000000000
--- a/.changeset/olive-donkeys-smile.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-"@object-ui/plugin-charts": minor
----
-
-Dashboard chart widgets no longer render as a blank area when their height class
-resolves to `auto`.
-
-`ChartContainer`'s min-size fallback was applied only to the wrapper `div`.
-Recharts measures its own `width:100%;height:100%` size-detector element, and a
-percentage height never resolves against an ancestor's `min-height`, so the
-wrapper obediently grew to 280px while the measured element stayed at 0 — and
-Recharts renders no children at all for a non-positive box. The result was a
-widget card with its title over an empty chart area: no marks, no refusal, no
-empty state, and permanent, because a box that never changes fires no resize.
-The floor is now applied to the measured element as well, under the same
-condition, so an author's explicit height still wins.
diff --git a/.changeset/olive-donkeys-smoke.md b/.changeset/olive-donkeys-smoke.md
deleted file mode 100644
index 890c2a7f3d..0000000000
--- a/.changeset/olive-donkeys-smoke.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Docs-only: correct the stale docstring on the Studio Data-pillar grid-columns pin
-(`StudioDesignSurface.gridColumns.test.tsx`). Since cloud#1652 added the
-`publishedFieldNames` filter to `gridColumns`, the second case exercises
-identity-churn liveness rather than a real column change, and its prose said the
-opposite. Comment and test title only — no assertion, no source, no published
-behaviour changes. objectui#6729.
diff --git a/.changeset/olive-mails-repeat.md b/.changeset/olive-mails-repeat.md
deleted file mode 100644
index 488ed71f25..0000000000
--- a/.changeset/olive-mails-repeat.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-**Bug — a `code`/`text` value whose text is JSON rendered as the literal `[Object]`.** `coerceToSafeValue` classified strings by SHAPE: any string starting `{`/`[` and ending `}`/`]` was `JSON.parse`d and the result run through the reference-label extraction (`name || label || externalId || id || _id || '[Object]'`), which answers the placeholder for an object carrying none of those keys. Every text-like cell reaches that helper — `text`, `textarea`, `code`, `time`, `auto_number` and `qrcode` all register to `TextCellRenderer` — so a stored `{"ok": true}` displayed as `[Object]`, and `[1, 2, 3]` in a text field displayed as `1, 2, 3`.
-
-A string is now returned verbatim, whatever its shape. The reference case the parse was written for (an unresolved external-id reference arriving as `'{"externalId":"…"}'`) belongs to reference-TYPED columns and is already handled there: `LookupCellRenderer` carries its own JSON-string branch, which resolves the label through the referenced object's schema and links to the record — neither of which the type-blind helper could do. The behaviour is scoped to the column type that owns it, not dropped. Object and array VALUES still coerce, so React error #310 stays fixed.
diff --git a/.changeset/olive-moons-reclassify.md b/.changeset/olive-moons-reclassify.md
deleted file mode 100644
index c01ea206f0..0000000000
--- a/.changeset/olive-moons-reclassify.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only. The zod-mirror parity guard's declared-but-unmirrored ledger is split by
-remedy: 23 callback-shaped keys move out of `UnmirroredDeclared` (121 to 98 keys) into
-a separately pinned `RuntimeOnlyDeclared`, reconciled against the same measurement
-through the union of both ledgers. No published type, no mirror and no runtime
-behaviour changes; nothing releases.
diff --git a/.changeset/olive-moons-shave.md b/.changeset/olive-moons-shave.md
deleted file mode 100644
index 507ad25d28..0000000000
--- a/.changeset/olive-moons-shave.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
----
-
-Internal test-support change, no user-visible behaviour (objectui#6924).
-
-The 17 hand-written `(Schema as { options?: readonly string[] }).options` casts
-across 16 spec-parity test files converge onto `@object-ui/test-support`'s
-`enumOptions(node)` — a sibling entry point onto the wrapper walk
-`shapeEnumOptions` already carried, which now delegates to it rather than
-holding a second copy. Only test files, the private (never-published)
-`@object-ui/test-support`, and `devDependencies` edges change; no package's
-shipped `dist/` and no public type moves.
diff --git a/.changeset/olive-pears-invent.md b/.changeset/olive-pears-invent.md
deleted file mode 100644
index 8c303f8953..0000000000
--- a/.changeset/olive-pears-invent.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
----
-
-Declare the batch write-warning's unattributed-strip placeholder in
-`@object-ui/data-objectstack`, and pin what it is (objectui#7160).
-
-`notifyBatchDroppedFields` resolves the object a cross-object strip is about
-from the wire entry's `object`, else from the operation its `index` addresses.
-When neither channel answers, it wrote a bare `''` — a value satisfying the
-spec's required `object: string` while naming no object at all. That literal is
-now the named, documented `UNATTRIBUTED_STRIP_OBJECT`, carrying the reachability
-argument, why such a strip is still emitted rather than refused, and why the
-placeholder must stay falsy. A stale comment claiming an unattributable strip
-"reads as an update" is corrected to what the code does (it lands on `create`,
-tracked as objectui#7170).
-
-No published behaviour changes and no released surface moves: the value is
-unchanged, and the new boundary suite passes identically against the pre-change
-source. The emitted `dist/index.d.ts` is byte-identical at both shas, with a
-live control (one temporary exported const) proving the comparison detects a
-real surface change.
-
-`node scripts/check-changeset-presence.mjs` on this tree, verbatim:
-
-> ✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/olive-pears-invent.md.
-> Every one of them has an EMPTY frontmatter — declared as releasing nothing, which
-> is the explicit exemption and a complete answer to this gate.
diff --git a/.changeset/olive-pugs-shave.md b/.changeset/olive-pugs-shave.md
deleted file mode 100644
index 18c839a639..0000000000
--- a/.changeset/olive-pugs-shave.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/components': patch
----
-
-Stop the record header title from collapsing when the action tail is wide
-
-The `page:header` record header laid the title column and the action tail out
-in a `flex-nowrap` row. The tail is `shrink-0`, so the title column — the only
-flexible item — absorbed the entire width deficit. On a record carrying three
-labelled `record_header` actions plus the `⋯` and `⟳` chrome, a 799px viewport
-left the title column 29.5px of a 687px header and the `h1` rendered as a
-single character and an ellipsis, while the breadcrumb above it still showed
-the full record name.
-
-The title column now carries a 12rem floor and the header may wrap at `sm` and
-up, so a tail that cannot fit alongside a readable title drops to its own line
-instead of starving it. Below `sm` the header is already a column and is
-unchanged. Headers whose tail already fits keep their single-line layout.
diff --git a/.changeset/olive-pugs-sing.md b/.changeset/olive-pugs-sing.md
deleted file mode 100644
index 0ebe368860..0000000000
--- a/.changeset/olive-pugs-sing.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-Dashboard record fields: percent columns now render through the one percent
-scaling decision instead of a second, drifted copy of it.
-
-`renderFieldValue`'s `%`-format branch normalised the value itself before
-calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`,
-then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`,
-which `@object-ui/core` documents as the single source of truth for percent
-display scaling, so the branch was re-deciding what core owns — and its copy had
-drifted from it in three measured ways:
-
-- `(value / 100) * 100` is not value-preserving in binary floating point,
- re-introducing one call frame upstream the round trip that was removed from
- inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000
- values change bit pattern and 1,108 rendered strings move, every one a
- last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is
- `1.61%`.
-- A stored fraction below `0.01` was scaled twice — the local `* 100` put it
- back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered
- `50.00%`.
-- The local test was `value > 1` rather than core's symmetric `|value| < 1`, so
- a negative already in percentage points took the fraction arm: `-5` rendered
- `-500.00%`.
-
-The branch now hands the raw stored value to `formatPercent` — the identical
-call the list-view percent cell already makes for an ordinary percent column —
-so a percent reads the same as a record field, as a grid cell and as a dashboard
-measure. Output moves where it was wrong: values at or above 1 whose round trip
-lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly
-`1`, which is one percentage point by core's convention and now renders
-`1.00%` at two decimals, where the local `value > 1` test had made it
-`100.00%`.
diff --git a/.changeset/one-origin-dev-configs-5745.md b/.changeset/one-origin-dev-configs-5745.md
deleted file mode 100644
index a9ed98970c..0000000000
--- a/.changeset/one-origin-dev-configs-5745.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
----
-
-Dev/build configuration only — this publishes nothing, declared explicitly with an
-empty frontmatter rather than left undeclared. No package `src/` is touched. The
-files changed are `apps/console/.env.development`, `examples/console-starter/.env.*`
-and `examples/console-starter/vite.config.ts`; `.env.development` is not part of
-`@object-ui/console`'s published artifact (`files: ["dist"]`, built from
-`.env.production`, which is untouched), and the example is private and on the
-changeset `ignore` list.
-
-Converge the dev stacks on one origin: empty `VITE_SERVER_URL` and let the Vite
-proxy do the split-host hop.
-
-Both dev env files pointed `VITE_SERVER_URL` at `http://localhost:3000` while the
-page was served from `:5180` (console) and `:5173` (starter). Every client in these
-apps coalesces an unset value to `''` and then builds a relative `/api/...` URL, so
-an empty value routes same-origin through the dev proxy instead. `console-starter`
-had no `server` block at all, so it gets the same one-stanza `/api` proxy
-(`DEV_PROXY_TARGET` or `http://localhost:3000`) that `apps/console` already carries;
-no port is pinned, because its README documents the example on Vite's default 5173.
-
-This is the ruled prerequisite for the `sameOriginOnly` action-runtime default. Under
-that default a non-empty `VITE_SERVER_URL` makes every relative-target `type: 'api'`
-action resolve cross-origin and be fetched bare — no `Authorization`, no
-`X-Tenant-ID`, no `Accept-Language` — i.e. a 401 for the standard `pnpm dev` stack.
-
-`examples/console-starter/.env.production` is emptied too. Its committed
-`https://demo.objectstack.ai` was not a deliberate choice for the example: it is the
-same pre-convergence value the sibling console carried until 2026-05-24, when
-`c351c9604` ("default published SPA to same-origin") cleared it there for CORS-blank
--page reasons and touched only `apps/console/.env.production`. The starter was left
-behind by that commit, and as a fork-ready scaffold its committed value is the
-production origin every fork inherits. Split-origin deployments inject
-`VITE_SERVER_URL` at build time, the way the console's own `.env.production` already
-documents.
diff --git a/.changeset/opennativepicker-coverage-7570.md b/.changeset/opennativepicker-coverage-7570.md
deleted file mode 100644
index d615beca89..0000000000
--- a/.changeset/opennativepicker-coverage-7570.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: pin the `openNativePicker` click path through `DateField` — that
-clicking the field invokes the helper on its own input, and that a host-supplied
-`onClick` arriving through DOM props is not swallowed (objectui#7570). No published
-behaviour changes.
diff --git a/.changeset/overlay-slot-requiredness-7106.md b/.changeset/overlay-slot-requiredness-7106.md
deleted file mode 100644
index ff0088abf8..0000000000
--- a/.changeset/overlay-slot-requiredness-7106.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Docs-only fix: `alert-dialog.mdx` and `sheet.mdx` published `trigger`/`content`
-node slots as required; the shipped `AlertDialogSchema`/`SheetSchema`
-declarations (and their Zod mirrors) already declare them optional. No
-declaration changes; test-only update to the fixture pin that records this
-class of divergence.
diff --git a/.changeset/owd-width-single-home-5477.md b/.changeset/owd-width-single-home-5477.md
deleted file mode 100644
index 10aa33c86c..0000000000
--- a/.changeset/owd-width-single-home-5477.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`ObjectSettingsPanel` now calls `isExternalWider` from `owd-sharing.ts` instead of
-re-declaring `OWD_WIDTH` and the ADR-0090 D11 width comparison inline (objectui#5477).
-
-`owd-sharing.ts` states its own purpose — it is "the single home" for the pieces the
-per-object Settings tab and the package-level OWD overview must agree on, the D11
-"external ≤ internal" comparison among them — and the Settings tab was the one surface
-not calling it. The two implementations were verified equivalent before the swap, over
-the full domain of both dials plus `undefined`, the rejected legacy aliases and
-prototype-chain keys: 144 input pairs, zero disagreements. The module's extra `!!`
-truthiness guards are redundant at this call site, which already normalizes both dials
-to `''`, and `'' in OWD_WIDTH` is false regardless. So no author-visible verdict
-changes — the same three pairs warn, and the same twenty-two stay calm.
-
-The substantive half is the pin that keeps it that way. `ObjectSettingsPanel.owdAgreement.test.tsx`
-drives BOTH surfaces over the full 5×5 cross-product of the values their dials offer and
-requires all three legs — the Settings tab's rendered warning, the overview's per-row
-error, and `isExternalWider` itself — to agree on every pair, with the violating pairs
-pinned by name so the sweep cannot pass vacuously. Re-inlining a drifted copy into either
-surface, swapping the `(internal, external)` argument order, or refining D11 in only one
-place now fails a test instead of silently leaving the authoring surface enforcing the
-old rule.
diff --git a/.changeset/page-source-tailwind-framing-5461.md b/.changeset/page-source-tailwind-framing-5461.md
deleted file mode 100644
index 4762675119..0000000000
--- a/.changeset/page-source-tailwind-framing-5461.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted.
-
-This is a published type surface: the TSDoc ships in `@object-ui/types`'s built
-`.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'`
-page is "constrained JSX/HTML + Tailwind" — and it links
-`content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to
-say the opposite. Shipped type documentation was contradicting the guide it points
-readers to.
-
-ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that
-framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind
-is compiled at build time by scanning the console's own `src`, and there is no
-safelist — so an authored utility class produces CSS only by coincidence, when
-objectui already ships that exact class, and otherwise produces nothing with no
-error anywhere. That is the ADR-0065 failure mode verbatim ("works only by
-coincidence"), and it is how a modal's `bg-black/50` backdrop reached production
-fully transparent.
-
-The tiers themselves are unchanged, and every load-bearing claim in the TSDoc
-survives verbatim — parse-never-execute and untrusted-author safety for `html`,
-the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the
-`react-pages` host capability for `react`, the ADR-0080 citation and the guide
-link. Only the styling conclusion changes, to the primitive each tier actually has:
-
-| `kind` | Style with |
-|---|---|
-| `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. |
-| `"react"` | Inline `style` objects. |
-
-Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows
-light/dark and whatever theme the deployment installs. The TSDoc now also names the
-rule that reports a violation — `page-source-className-tailwind`, shipped in
-`@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by
-`os validate` as a warning on both tiers.
-
-No behaviour change, and the accepted `kind` set is untouched.
-
-`packages/components/src/renderers/layout/react-page.tsx` carries the same
-correction on its two source comments (the injected-scope note and
-`buildComponentScope`), and gains the styling note the file was missing. Those are
-internal comments — they do not project into any `.d.ts` and change no export — so
-they get no entry of their own; there is nothing an `@object-ui/components`
-consumer could read in a CHANGELOG and act on.
diff --git a/.changeset/page-source-tailwind-prose-retraction-5469.md b/.changeset/page-source-tailwind-prose-retraction-5469.md
deleted file mode 100644
index f125f38941..0000000000
--- a/.changeset/page-source-tailwind-prose-retraction-5469.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/react-runtime': patch
-'@object-ui/sdui-parser': patch
-'@object-ui/components': patch
----
-
-Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's
-`source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065,
-Accepted) retracted. objectui#5461 corrected three sites; a multiline census
-found eight more, in three spellings a line-oriented grep could not see.
-
-A page's `source` is *runtime metadata*. The console's Tailwind is compiled at
-build time by scanning the console's own `src`, and there is no safelist, so it
-never sees your page: an authored utility class produces CSS only by coincidence
-(when objectui already ships that exact class) and otherwise produces nothing,
-with no error anywhere. That is the ADR-0065 "works only by coincidence" failure
-mode, and it is how a modal's `bg-black/50` backdrop reached production fully
-transparent. `os validate` reports it as `page-source-className-tailwind`, a
-warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`.
-
-The tiers themselves are unchanged and every load-bearing claim survives —
-parse-never-execute, the untrusted-author safety argument for `html`, and the
-deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording
-`content/docs/guide/react-pages.md` §Styling already uses:
-
-| `kind` | Style with |
-|---|---|
-| `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. |
-| `"react"` | Inline `style` objects. |
-
-Colors on both tiers come from the theme as `hsl(var(--token))`.
-
-Why each package has an entry — each was measured against its built artefact, not
-assumed:
-
-- **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes
- `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is
- the paragraph that routes untrusted-author work to the `html` tier, and it
- carried the retracted framing line-wrapped across `:17-18`. It also gains the
- §Styling section it was missing — the absence is why the framing survived here.
-- **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects
- verbatim into the published `dist/types.d.ts`.
-- **`@object-ui/components`**: the corrected header of
- `src/renderers/basic/html-elements.tsx` projects verbatim into the published
- `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm
- comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a
- function body) and is included here only because the same package already owes
- an entry.
-
-No behaviour change: this is prose only. `CHANGELOG.md` occurrences are
-deliberately untouched — immutable release history.
diff --git a/.changeset/palette-canonical-discussion-5495.md b/.changeset/palette-canonical-discussion-5495.md
deleted file mode 100644
index 18563f664d..0000000000
--- a/.changeset/palette-canonical-discussion-5495.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-**The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.**
-
-The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495.
-
-**The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered".
-
-One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one.
diff --git a/.changeset/palette-exclusion-reason-truthfulness.md b/.changeset/palette-exclusion-reason-truthfulness.md
deleted file mode 100644
index ad1bcf1077..0000000000
--- a/.changeset/palette-exclusion-reason-truthfulness.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Corrects two `PALETTE_EXCLUSIONS` reason strings in the Studio page-palette ledger that
-claimed "no renderer" for `element:text_input` and `element:record_picker`, both of which
-have registered renderers, and pins the class with a test. The exclusions themselves are
-unchanged decisions and no published behaviour moves: the reason strings are developer-facing
-ledger prose, read by no runtime code path.
diff --git a/.changeset/param-dialog-title-fallback-4282.md b/.changeset/param-dialog-title-fallback-4282.md
deleted file mode 100644
index 41c71b6810..0000000000
--- a/.changeset/param-dialog-title-fallback-4282.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-The console's param-collection dialog now titles itself from `action.label` alone —
-the unreachable `|| action?.title` fallback beside it is removed (objectui#4282).
-
-`title` is declared on no action surface in the ecosystem: it is absent from
-`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from
-`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS`
-inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts`
-`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`,
-`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand
-side of that `||` could not be reached by authored metadata: a fallback that cannot
-fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce.
-Nothing a user hits changes; the line now reads exactly one key, matching the
-`description` line directly below it.
-
-`useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader so the alias cannot
-be reinstated silently: an action carrying `title` and no `label` must open an untitled
-dialog rather than a dialog named by a key no producer sets.
diff --git a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md b/.changeset/paramtofield-reference-rule-derives-from-core-5312.md
deleted file mode 100644
index a3aa770c31..0000000000
--- a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`ActionParamDialog`'s "this param carries a reference target" rule now derives from the shared reference-field family instead of the last private copy of it.
-
-`packages/app-shell/src/utils/paramToField.ts` restated the rule inline as
-`LOOKUP_WIDGET_TYPES.has(type) || type === 'user'` — the fourth and last
-hand-maintained answer to one question ("does this widget resolve a foreign key,
-so hand it `reference_to` / `display_field` / the rest of the picker config").
-The other three converged on `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` in
-objectui#4770 / #4790 / #4815; this face is now the fourth.
-
-No reachable behaviour change. The shared set is one member wider (`tree`), and
-that member can never be a widget key on this surface: it is absent from
-`fields`' widget map and `mapFieldTypeToFormType` sends it to `field:lookup`, so
-every key the rule tests arrives as `lookup`. Both halves are pinned, so
-registering a real `tree` widget surfaces the change instead of shipping it
-silently.
-
-The module's second rule — which widget keys degrade to a text input for want of
-a declared `referenceTo` — is a different set over overlapping types (`user`
-defaults its target to `sys_user` and must never degrade) and was deliberately
-left un-merged, matching the same split the plugin-grid twin keeps.
-
-Also retires a comment that claimed the disjunction "moves in lockstep with
-plugin-grid's `bulkParamToField` twin — the two param faces are never split".
-Measured on the tip before this change, that was false in both senses: the twin
-had read core's Set since objectui#4815 while this line read a private literal,
-so the two shared nothing and no gate could report a split; and the two member
-sets already differed, by `tree`. Lockstep now holds mechanically — the pin is
-on object identity (a spy on core's `has`), so a member-identical private copy
-fails where a value check would pass.
diff --git a/.changeset/parity-gate-lazy-registrations.md b/.changeset/parity-gate-lazy-registrations.md
deleted file mode 100644
index c6a27a5a2c..0000000000
--- a/.changeset/parity-gate-lazy-registrations.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change to the console's registry/spec parity gate (`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`): it now loads the two spec-carried plugin blocks the console registers lazily, and asserts its own coverage so a block can no longer fall out of the judged population silently. Also adds one member pin next to the renderer it constrains (`packages/plugin-kanban/src/__tests__/ObjectKanban.filterMembersReachTheWire-8176.test.tsx`), so the `filter` key objectui#8186 declared is answered with a pin rather than with an exemption. No published behaviour changes — no renderer, registration or runtime file is touched.
diff --git a/.changeset/paywall-moment-exit-and-failure-state-7253.md b/.changeset/paywall-moment-exit-and-failure-state-7253.md
deleted file mode 100644
index 83fe84d5f9..0000000000
--- a/.changeset/paywall-moment-exit-and-failure-state-7253.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-chatbot': minor
----
-
-Fix the three breaks at the AI paywall moment (#7253), measured on a free plan's
-second build iteration when the cloud guardrail refuses "Confirm changes" with a
-429 `AI_DESIGN_QUOTA_EXHAUSTED`.
-
-- **The upgrade CTA no longer opens a 404.** It used to open a client-composed
- `${cloudBase}/apps/cloud-control/sys_environment`, which guessed the control
- plane's console mount, app slug and route — all three wrong — and landed on
- the API's `ENDPOINT_NOT_FOUND` JSON. `cloudInstallDeepLink` /
- `cloudPricingDeepLink` are replaced by `cloudConsoleUrl()`, the
- runtime-supplied cloud origin with no path appended; the control plane's own
- root redirect decides the landing page. The former
- `|| 'https://cloud.objectos.app'` default is gone: a runtime with no upstream
- cloud now renders no upgrade link at all rather than pointing a self-hosted
- user at the vendor's SaaS.
-- **The confirm card gets an explicit failure state.** A quota refusal parks the
- card on "not applied" with the server's own next step (reset tomorrow /
- upgrade) plus the upgrade action, instead of silently rolling back to
- "Confirm / Adjust" as though the click had never happened. Transient failures
- (offline, per-minute rate limit) still roll back, because retrying is the
- right next step there.
-- **The composer is no longer refilled with an already-delivered message.**
- Only text typed into the composer is restorable now; card-driven sends
- (confirm, approve, suggestion chips) send canned text the user never typed and
- no longer leave the previous prompt staged as if it needed resending.
diff --git a/.changeset/pending-drafts-bar-locale-parity-5705.md b/.changeset/pending-drafts-bar-locale-parity-5705.md
deleted file mode 100644
index 6bafb84358..0000000000
--- a/.changeset/pending-drafts-bar-locale-parity-5705.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-`console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings —
-now exists in all ten locale packs. It previously existed only in `en` and `zh`, so
-`ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and
-`all-locales-key-parity` failed on `main` (objectui#5705).
-
-The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was
-titled for the locale packs but reached only `zh`, so eight packs × five keys stayed
-missing and the parity assertion — which carries no allowlist — was red on `main` and
-on every PR whose diff touched source. Source-free diffs skip the shard that runs it,
-which is why the breakage survived several merges.
-
-Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`,
-`ru` puts the number last (`…: {{count}}`) as it already does for the sibling
-`home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}`
-because that value is a runtime message rather than a single token — both choices
-carry an in-pack note. Terminology is taken from each pack's existing publish-bar
-vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read
-alike.
-
-Both interpolations survive verbatim in every pack — `{{count}}` in `count` and
-`{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated
-packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a
-different node and is untouched.
diff --git a/.changeset/pending-drafts-i18n-keys.md b/.changeset/pending-drafts-i18n-keys.md
deleted file mode 100644
index 84dab23b48..0000000000
--- a/.changeset/pending-drafts-i18n-keys.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@object-ui/i18n': patch
-'@object-ui/app-shell': patch
----
-
-Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere.
diff --git a/.changeset/per-user-active-org-and-session-user-purge-5664.md b/.changeset/per-user-active-org-and-session-user-purge-5664.md
deleted file mode 100644
index 45bdd142c8..0000000000
--- a/.changeset/per-user-active-org-and-session-user-purge-5664.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-'@object-ui/auth': patch
-'@object-ui/console': patch
----
-
-The active organization id is now stored per user, and a change of session user drops the
-previous user's client state wholesale (objectui#5664).
-
-`auth-active-organization-id` was a single un-namespaced `localStorage` key while its
-siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`,
-`flow-palette-recents:u:`). On a browser handed from one account to another — a shared
-machine, a kiosk, a handover, a support session — the arriving user's console read the
-PREVIOUS user's organization id. The header workspace chip rendered the previous user's
-workspace for a user whose `organization/list` was empty, and the consequence past the
-cosmetics is the one worth stating: the polluted org context suppressed
-`RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so
-a brand-new user on that browser silently never got the new-user flow at all.
-
-Nothing about row visibility rode on this. With the stale id the server answers
-`403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero
-environments; the damage was entirely in what the client believed about itself.
-
-Three changes, and the third is the one that closes the class rather than the instance:
-
-- The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention
- its siblings already use.
-- It can no longer be written un-namespaced at all. Where no session user is known yet the
- value lives in memory for that page-load only — a namespacing that kept a bare-key
- fallback would re-open the defect the first time a write happened before the user id
- resolved.
-- **A change of session user drops the previous user's client state wholesale.** This is an
- allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so
- the NEXT storage key someone adds without a `:u:` scope is covered before it is written.
- Only device-scoped entries survive: the arriving session's own bearer token, the pointer
- recording whose state the browser holds, and the UI theme.
-
-Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a
-non-null `localStorage` read and falls back to the in-memory value, and the memory value is
-nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge
-too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then-
-sign-in path that never reloads the page.
-
-Existing browsers are not migrated. A value sitting under the retired bare key is
-unattributable — nothing recorded whose org id it is — so migrating it is precisely the
-defect it would be migrating away from, and it is deleted instead. A signed-in user loses
-nothing durable: the active organization is a server-owned fact that
-`AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no
-active org is held, including the ADR-0081 single-membership repair. One boot re-supplies
-it; users with no organization land on the guided first-run flow, which is the outcome this
-card is about.
-
-`apps/console`'s pre-render auth preflight purges every spelling of the active-org key —
-the retired bare one and each `:u:` scope — when it finds a dead bearer token, and
-deliberately leaves the session-user pointer in place so the next sign-in can still tell
-that the browser changed hands.
diff --git a/.changeset/per-view-config-alias-fold-2890.md b/.changeset/per-view-config-alias-fold-2890.md
deleted file mode 100644
index 1636fc20ce..0000000000
--- a/.changeset/per-view-config-alias-fold-2890.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/core': patch
----
-
-`normalizeListViewSchema` now folds the four per-view-type config aliases phase 3
-carried over — `kanban.groupField` → `groupByField`, `kanban.cardFields` →
-`columns`, `gallery.imageField` → `coverField`, `timeline.dateField` →
-`startDateField` (objectui#2890).
-
-These are the pre-#2231 objectui spellings, kept declared alongside the spec keys
-so stored view metadata would keep validating. They now fold at the same
-component boundary as the A1–A5 vocabulary folds, in the same one-directional
-shape: the canonical key wins when a config carries both, and the legacy key is
-removed from the result so a missed read-site fails loudly instead of quietly
-taking the legacy path.
-
-One rendering behaviour changes, and it is a correction of the same inverted
-precedence the `densityMode` fold fixed: `ListView`'s kanban adapter resolves the
-card field list as `cardFields || columns` — legacy over canonical — so a kanban
-config carrying **both** rendered the legacy `cardFields` value and silently
-ignored the spec-canonical `columns`. After the fold the authored `columns` is
-what reaches it. Configs carrying only one of the two are unaffected, and every
-other reader of these four keys was already canonical-first.
-
-`calendar.defaultView` is deliberately **not** folded: it aliases nothing and has
-no spec counterpart, so it wants promotion upstream rather than a rename.
diff --git a/.changeset/percent-figure-grid-citation-5608.md b/.changeset/percent-figure-grid-citation-5608.md
deleted file mode 100644
index 7c6075e49b..0000000000
--- a/.changeset/percent-figure-grid-citation-5608.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
----
-
-Comments only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared (objectui#5608).
-
-Two live sites still carried the bare `27,581 of 1,200,013` figure with neither the grid
-nor the measured surface named: the `DisplayNumberFormatOptions.style` doc comment and a
-comment in the percentage-points test. Both were accurate. Neither was *reconcilable* — a
-reader who had also seen `packages/fields`'s `27,577 of 1,200,003` met two counts that
-look like a contradiction, and a figure a reader cannot reconcile is what makes a correct
-comment get distrusted wholesale. That is the same defect class objectui#4596 was queued
-for, and the wording here is lifted from its PR rather than phrased a third way — a third
-phrasing for one fact is the defect one level up.
-
-Both counts stand unchanged. The disambiguator is the surface measured, so each site now
-names it: objectui#4576's tie-dense grid (0.005 steps to 2,000, precisions 0/1/2) on
-`formatDisplayNumber` / `formatMeasure`'s call shape, against objectui#4590's re-measure
-of the same route through `formatPercent`. No value, assertion or behaviour moves.
-
-Why the empty form is the right declaration, stated from what was measured rather than
-carried over from objectui#4596's PR — the two cases are mirror images. There, a function
-BODY comment was changed, so the emitted `.js` moved and the `.d.ts` did not. Here the
-changed source comment is the JSDoc on an exported interface member: the interface erases
-at runtime, so the new prose reaches `dist/utils/number-display.d.ts` and is absent from
-`dist/utils/number-display.js` (checked on a real build of the package). The second site
-is a test file, excluded from the build program by `src/**/__tests__/**` and never in
-`dist` at all.
-
-So a consumer-visible byte does move — the text shown on hover. That is documentation of
-an existing measurement, not a change to the measurement, the type, or any behaviour:
-nothing a consumer types against or calls is different, which is why this releases
-nothing. Bytes moving is not the criterion; released behaviour and consumer-visible
-surface are, and neither moves.
diff --git a/.changeset/pin-advisory-sink-seam-6969.md b/.changeset/pin-advisory-sink-seam-6969.md
deleted file mode 100644
index be477420c6..0000000000
--- a/.changeset/pin-advisory-sink-seam-6969.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: pins the metadata advisory sink seam (`useMetadataClient` ->
-`createConsoleMetadataClient` -> `MetadataClient`), so an advisory the server
-returns on a save or a publish is asserted to actually reach the toast. No
-published behaviour changes.
diff --git a/.changeset/pin-ledger-docstring-key-totals.md b/.changeset/pin-ledger-docstring-key-totals.md
deleted file mode 100644
index f38646b69f..0000000000
--- a/.changeset/pin-ledger-docstring-key-totals.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
----
-
-Test-only change to `@object-ui/types`' zod-mirror-parity pin file. Both LEDGER
-DOCSTRINGS — above `interface UnmirroredDeclared` and above `interface
-RuntimeOnlyDeclared` — stated that ledger records **94 keys**; two independent
-instruments measure **87**, wrong since objectui#7779 and fully green the whole time
-(objectui#8243). Those two live figures are corrected and are now derived from the
-ledger through the docstrings' own spelling. The stale decomposition beside them
-(`94 − 1 seeded + 3 mirrored + 2 retired + 23 reclassified`) needs key provenance no
-ledger here records, so instead of being refreshed or deleted it is rewritten as a
-reading at NAMED REVISIONS — 98 keys at `beccf1c6b`, of which 85 survive and 13 have
-left at `ed7178bf3` — which cannot rot and needs no hand re-derivation at the next
-repair. The measured 13 and the prose-derived 10 / 3 split of it are kept visibly
-apart. No published behaviour changes.
diff --git a/.changeset/pin-zod-mirror-parity-header-key-totals.md b/.changeset/pin-zod-mirror-parity-header-key-totals.md
deleted file mode 100644
index 22f7df993b..0000000000
--- a/.changeset/pin-zod-mirror-parity-header-key-totals.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Test-only change to `@object-ui/types`' zod-mirror-parity pin file. Its header stated
-`KnownDrift` as **62 keys**; two independent instruments measure **63**, wrong since
-objectui#7664 and green the whole time (objectui#8222). The digit is corrected and the
-remaining LIVE figures in that header are closed out: the two unpinned key totals and
-the three cross-ledger restatements are now derived from the ledgers through the
-header's own spelling, and the one figure that cannot be derived is excluded in
-writing with its reason. No published behaviour changes.
diff --git a/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md b/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md
deleted file mode 100644
index 0409f99ecd..0000000000
--- a/.changeset/pin-zod-mirror-parity-ledger-entry-counts.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change to `@object-ui/types`' zod-mirror-parity pin file: the three LEDGER
-entry counts its header states, and the ratchet section's restatement of the first of
-them, are now compared to the ledgers they describe (objectui#7733). One of those
-figures was already wrong on `main`. No published behaviour changes.
diff --git a/.changeset/pivot-table-stable-empty-rows-5562.md b/.changeset/pivot-table-stable-empty-rows-5562.md
deleted file mode 100644
index d7423a5b53..0000000000
--- a/.changeset/pivot-table-stable-empty-rows-5562.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
----
-
-`PivotTable` no longer re-runs its cross-tabulation memo on every render when it
-has no rows (objectui#5562).
-
-The component spelled the empty array twice — as the destructuring default for
-`schema.data` and as the `Array.isArray` fallback that keeps a provider-config
-object out of iteration — so a schema declaring no `data` key, or one whose
-`data` is a provider config rather than rows, produced a fresh array identity on
-every render. That value is the first entry of the memo's dependency list, so
-the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the
-aggregated matrix and the row/column/grand totals on every render, over nothing.
-Both spellings now resolve to one module-scope frozen empty, so "no rows" is a
-stable value and the memo holds.
-
-Wasted work only: the churn feeds a memo rather than a `setState`, and
-`PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no
-render loop was possible. The identical fix landed for `data-table` in
-objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the
-direct-use path those two did not cover, where `DashboardRenderer` and
-`DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the
-chain.
diff --git a/.changeset/placeholder-spelling-one-rule.md b/.changeset/placeholder-spelling-one-rule.md
deleted file mode 100644
index 1a727b92fa..0000000000
--- a/.changeset/placeholder-spelling-one-rule.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Tooling and test-only (objectui#7310): the placeholder-spelling rule that
-objectui#3512's gate and `check-i18n-call-site-keys.mjs` class 7 each carried a
-copy of now lives once, in `scripts/placeholder-spelling.mjs`, which both
-import. No published behaviour changes — `scripts/` ships in no package and
-`packages/i18n`'s `__tests__/` is excluded from its build. The merged
-implementation was proven byte-equivalent to both copies over a 23,420-input
-corpus before either was deleted, and the gate's full output is unchanged.
diff --git a/.changeset/plain-taxis-attend.md b/.changeset/plain-taxis-attend.md
deleted file mode 100644
index 4b7cd354ce..0000000000
--- a/.changeset/plain-taxis-attend.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/console': patch
----
-
-Approvals drawer: the business summary card no longer renders a field the object
-declares `hidden: true`.
-
-`payloadSummary` built the card from the request's `payload_json` snapshot behind
-five filters (system keys, the lead amount key, null/object/empty values,
-unresolved opaque ids, a six-field cut) and no field-visibility filter, so a
-hidden field that survived to the first six survivors rendered in the card,
-labelled. The drawer now reads the open request's object metadata and drops the
-declared-hidden keys before the six-field cut, so the next business field is
-promoted into the freed slot rather than the card silently shrinking. The lead
-amount figure at the top of the same card takes the same trim.
-
-Per the platform ruling, `hidden: true` is a UI-only contract and `internal: true`
-is the serialization primitive, so this is the UI enforcing the only contract
-`hidden` has — not a client-side compensation. Field-level security is unchanged
-and remains the server's answer. The metadata read is the same cached
-`GET /meta/object/:name` the record form already performs, once per object per
-page visit, and an unanswered read leaves the card exactly as it renders today.
diff --git a/.changeset/plugin-component-input-deprecate-5674.md b/.changeset/plugin-component-input-deprecate-5674.md
deleted file mode 100644
index 7f89a7f1de..0000000000
--- a/.changeset/plugin-component-input-deprecate-5674.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`PluginComponentInput` is deprecated in favour of `ComponentInput` (objectui#5674).
-
-This is stage 1 of a two-stage retirement the maintainer ruled on 2026-08-22: deprecate
-for a release, then remove. Nothing is removed here — the export still exists and still
-names the same type.
-
-`@object-ui/types`' entry point publishes `ComponentInput as PluginComponentInput`. Until
-objectui#4972 that alias pointed at a genuinely different declaration: `plugin-scope.ts`
-restated its own nine-key `ComponentInput`. objectui#5671 converged that declaration onto
-`base.ts`, so the alias became a second published name for the *same* type, carrying no
-information the first does not. Two published names for one type is a shape that costs
-readers a step and gives AI-authored code a coin-flip between spellings.
-
-**Why a deprecation window rather than a deletion.** The measurement that licenses
-deleting an export from a published package is *"no importer"*, and what can be measured
-from inside this repository is only *"no importer here"*. In-repo the name has zero
-importers — searched across every root (`packages/`, `apps/`, `content/`, `docs/`,
-`skills/`, `examples/`, `e2e/`, `scripts/`, and the root docs), plus the sibling framework
-repository, with a control name searched identically so a broken search could not read as
-a clean one; the only occurrences are the alias itself and prose about it. What no search
-here can see is a consumer on npm. The deprecation window is the answer to that
-unmeasurable half: it converts a silent break into a warned one before the removal lands.
-
-**Reversibility, deliberately.** Deprecating is undoable; deleting a published export is
-not. Where the evidence is one-sided, the retirement takes the reversible step first.
-
-The tag reaches consumers rather than only the source: a JSDoc block on an export
-specifier survives declaration emit and lands attached to that specifier in the emitted
-`index.d.ts`, which was measured for this change rather than assumed. With comments
-stripped, the emitted entry point is byte-identical before and after — the published
-*type* surface does not move, it only gains the notice.
-
-Stage 2 (removing the alias, and the now-dead re-export in `plugin-scope.ts` that exists
-only to feed it) is filed as a follow-up and ships as a `minor`, per this repo's policy
-that its own breaking changes never declare `major`.
diff --git a/.changeset/plugin-form-readme-classname-quantifier-5131.md b/.changeset/plugin-form-readme-classname-quantifier-5131.md
deleted file mode 100644
index 112411d63c..0000000000
--- a/.changeset/plugin-form-readme-classname-quantifier-5131.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/plugin-form': patch
----
-
-`README.md`'s "Not a `FormField` key" table said a field-level `className` is
-"read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier
-holds only for the renderer's *explicit* read — `className={fp.className}` on
-the `section-divider` branch of
-`packages/components/src/renderers/form/form.tsx`. The same renderer forwards
-every key it did not destructure, and `className` is not among the names taken
-off the field config, not among the ones `stripRendererOnlyProps` removes, and
-so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input`
-branch spreads it onto ` `. A field-level `className` therefore lands
-visibly on ordinary built-in controls, and a reader taking "exactly one"
-literally concludes the opposite of what the code does (objectui#5131).
-
-The cell now describes the contract rather than the reader count: an undeclared
-key still rides the props spread down to whichever component the field resolves
-to, nothing in the contract promises that, and a registered widget honours it
-only if it happens to spread its leftover props — the wording the docs site
-already ships, so the two sources agree again. The advice in the row is
-unchanged and was never wrong (`span` / `colSpan` for width,
-`FormSchema.fieldContainerClass` for the grid), and the explicit
-`section-divider` read is kept, now named as explicit.
-
-This is a documentation fix to a file `plugin-form` publishes to npm, which is
-why it carries a version: the npm landing page only picks up the correction on a
-release. No behaviour, export, type, or `dist` byte changes.
diff --git a/.changeset/plugin-map-reach-test-narration.md b/.changeset/plugin-map-reach-test-narration.md
deleted file mode 100644
index 7fe78586d7..0000000000
--- a/.changeset/plugin-map-reach-test-narration.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Comment-only correction in `packages/plugin-map/src/ObjectMap.listViewMapConfigReach.test.tsx`: the narration above the `titleField` arm attributed the `'Marker'` placeholder to `getMapConfig`, which never held it. No assertion, fixture or published behaviour changes.
diff --git a/.changeset/preview-sample-lookup-reference-6647.md b/.changeset/preview-sample-lookup-reference-6647.md
deleted file mode 100644
index a6f2cbbcc2..0000000000
--- a/.changeset/preview-sample-lookup-reference-6647.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Dev-only + test-only change; no published behaviour changes.
-
-The `object` preview sample (`apps/console/src/preview-samples.ts`) now spells its
-lookup target `reference` instead of the by-name-refused `reference_to`, and the
-`KNOWN_STALE` ledger entry in `preview-samples-spec-valid.test.ts` records both
-defects that actually remain. Neither file reaches a published entry point:
-`preview-samples.ts` is imported only by `preview-gallery.tsx`, which is served
-from the standalone `preview-gallery.html` Vite entry — not a `rollupOptions.input`
-of the production build, so it is absent from the `dist` that `@object-ui/console`
-publishes.
diff --git a/.changeset/properties-template-visibility-diagnostic-5756.md b/.changeset/properties-template-visibility-diagnostic-5756.md
deleted file mode 100644
index 5d050acf44..0000000000
--- a/.changeset/properties-template-visibility-diagnostic-5756.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/react': patch
----
-
-`SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated
-spelling of the objectui#5454/#5687 diagnostics when it is written inside
-`properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }`
-(objectui#5756).
-
-The `properties.*` evaluation loop runs, and interpolates every `${…}` template it
-finds, **before** the visibility gate ever sees the value — so by the time the
-existing diagnostics ran, a template-spelled predicate had already collapsed into a
-plain boolean and there was no predicate text left to inspect. The bare-string
-spelling of the exact same gate (`properties: { visible: "data.status == 'draft'"
-}`) was unaffected — nothing interpolates a string with no `${` in it — and was
-already reported; only the template spelling was structurally invisible.
-
-Reached by moving the diagnostic check to inside the `properties` evaluation loop,
-on the predicate's raw (pre-interpolation) text, gated on the key being one of the
-six visibility keys the render chain consults (`visibleWhen` / `visible` /
-`visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content`
-interpolation, or any other non-visibility key, is untouched and stays silent.
-
-Reports only the key that actually **decides** the node's visibility, mirroring
-objectui#5454's own leg semantics (its reporter is likewise only ever invoked on
-the leg the chain's early-return sequence actually reaches): a `properties.visible`
-template that a co-declared `visibleWhen` outranks is not reported for deciding
-nothing.
-
-**No verdict changes and no interpolation changes.** The diagnostic call's return
-value is discarded; the real verdict is still computed afterward, off the
-post-evaluation, post-hoist schema, by the same code path as before this change.
-Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data
-predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate —
-only the silence moved, one render-step earlier.
diff --git a/.changeset/proto-instrument-fixture.md b/.changeset/proto-instrument-fixture.md
deleted file mode 100644
index a3e358f90f..0000000000
--- a/.changeset/proto-instrument-fixture.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only (objectui#6524): the `__proto__` instrument fixture in
-`MetadataService.objectPayloadFieldsMap.test.ts` was spelled as a plain object
-literal, which per Annex B.3.1 sets the prototype instead of adding a key — so
-the assertion parsed an empty `fields` map and would have stayed green if the
-spec began refusing the name. Respelled as a computed key and pinned with the
-literal-versus-computed control. No published behaviour changes.
diff --git a/.changeset/queryparams-closed-keys-7497.md b/.changeset/queryparams-closed-keys-7497.md
deleted file mode 100644
index 0e341c38ba..0000000000
--- a/.changeset/queryparams-closed-keys-7497.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/types': minor
----
-
-`QueryParams` (`@object-ui/types`) no longer carries the `[key: string]: any`
-index signature; its key set is now exactly the nine declared `$`-prefixed
-members — `$select`, `$filter`, `$orderby`, `$skip`, `$top`, `$expand`,
-`$search`, `$searchFields`, `$count` (objectui#7497).
-
-**What stops type-checking that type-checked before.** Any object literal
-assigned or passed as a `QueryParams` that carries a key outside those nine —
-the unprefixed spellings the readers silently dropped (`{ filter }` for
-`$filter`, `{ limit }` for `$top`, `{ options: { $top } }`), and any
-`$`-prefixed name that is not declared (`{ $limit }`). Reading an undeclared
-key off a `QueryParams` value (`params.filter`) is refused too. A published
-guide had taught `adapter.find('contacts', { filter: { active: true } })` and
-asserted the client saw that `filter`: it compiled, `convertQueryParams`
-dropped the key, and the assertion could never pass. That literal is now a
-compile error at the call site.
-
-**What does not change.** Every reader in this repository —
-`convertQueryParams` and `rawFindWithPopulate` in `@object-ui/data-objectstack`,
-`queryParamsToRecord` in `@object-ui/core`, `ValueDataSource.find` — reads only
-declared members, so nothing the runtime honoured is refused. A
-`Record` or `Record` VALUE (the shape
-`@objectstack/spec`'s `ViewData.params` parses to) still passes, as does a
-spread of one beside `$top` / `$skip`; a type assertion (`{ limit: 1 } as
-QueryParams`) still compiles because assertions skip excess-property checks,
-which is why `object-ui/no-unprefixed-query-params` keeps its typed cases.
-
-**Why `minor` and not `patch` or `major`.** Narrowing the accepted set of a
-published contract is a breaking change for any downstream caller that relied
-on the extra keys. The census behind the grade: across this repository's
-packages, apps, examples, e2e and scripts trees (4,362 files), zero `find` /
-`findOne` call sites or `QueryParams` literals carry a non-`$` key, and no
-adapter reads one — the signature carried nothing but typos. This repository
-records its own breaking changes as `minor` with the break spelled out; `major`
-is reserved for following `@objectstack` across a major (see AGENTS.md, version
-alignment). A `patch` would be wrong: this is a deliberate narrowing, not a
-fix inside the accepted set.
-
-Migration for a downstream caller that did pass an extra key: if an adapter of
-yours reads it, declare it on your own params type and widen at your adapter's
-boundary; if nothing reads it, it was already being dropped — delete it.
diff --git a/.changeset/quick-reference-release-sync-5394.md b/.changeset/quick-reference-release-sync-5394.md
deleted file mode 100644
index d88e63d9ed..0000000000
--- a/.changeset/quick-reference-release-sync-5394.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
----
-
-Release tooling and repo docs only — this publishes nothing, declared explicitly with an
-empty frontmatter rather than left undeclared.
-
-`changeset:version` now runs `scripts/sync-quick-reference-release.mjs` after bumping the
-manifests, so `QUICK_REFERENCE.md`'s "Current Release" block moves in the same commit as
-the versions it quotes. Before this, the release path had no human in it and nothing
-updated the doc, so the block fossilised once per release and the anti-fossil gate
-`scripts/__tests__/quick-reference-current-release-4143.test.ts` reddened `main` on the
-push build every time (objectui#4642, objectui#4977, objectui#5394). The gate is
-unchanged — it is the judge; this is the thing that keeps the doc true.
-
-No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is
-nothing here for a consumer to upgrade to.
diff --git a/.changeset/radio-multiple-docblock-correction.md b/.changeset/radio-multiple-docblock-correction.md
deleted file mode 100644
index 2d7522ebdd..0000000000
--- a/.changeset/radio-multiple-docblock-correction.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Comment-only correction in `@object-ui/fields`. The `MULTI_VALUE_FORM_TYPES` docblock in
-`field-type-alias.ts` asserted that every `MULTI_CAPABLE_TYPES` member other than `select`
-renders both arities inside one widget, naming `LookupField`, `FileField` and `ImageField`
-while silently omitting `radio` — which reads `multiple` nowhere, and so is the one member
-with no multi-arity renderer. The docblock now states that exception, pins it to this repo's
-own single-value contract for `radio`, and points at the producer-side work that refuses the
-authored combination. No published behaviour changes.
diff --git a/.changeset/react-form-view-bridge-drifted-arms-5652.md b/.changeset/react-form-view-bridge-drifted-arms-5652.md
deleted file mode 100644
index 3f0c9b7965..0000000000
--- a/.changeset/react-form-view-bridge-drifted-arms-5652.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/react': minor
----
-
-The spec-bridge's form-view input now accepts everything `@objectstack/spec`'s
-FormViewSchema accepts — three key types had drifted narrower, and one of them
-had inverted (objectui#5652).
-
-`bridges/form-view.ts` held a third hand-written description of the FormViewSchema
-contract, after the leaf (objectui#5542) and the two containers (objectui#5596)
-were converged elsewhere. A description nothing compares is one spec release from
-being a fork, and this one had already drifted on the keys that decide whether a
-legal document renders:
-
-- `FormSection.columns` refused the string spelling of a column count, which the
- contract admits and folds to a number in its own pipe. It is now the contract's
- type, and the bridge performs the fold — the `object-form` node's section
- declares `columns` as a number and its container indexes a grid-class map by
- it, so forwarding `'2'` handed every downstream renderer a value outside the
- type it declares.
-- `FormField.dependsOn` was declared `string[]`, which is the exact inverse of the
- contract: it admitted only the array arm the contract rejects, and refused the
- bare parent-field name that is the one configuration making `field-selector`
- and `dynamic-config` work (objectui#5040).
-- `visibleWhen`, on both fields and sections, was declared `string`, so the
- ADR-0089 expression object — the arm `evalFieldPredicate` reads — could not be
- described at all. Both arms now travel whole onto the node.
-
-A section's `fields` may also be a bare object-field name, the spec shorthand the
-list bridge already honours. The form bridge ran it through the object mapper
-instead, producing a field with no identity (`{ name: undefined }`) for the most
-ordinary section a form can declare; it is now forwarded verbatim, which is what
-the node's own `fields` slot admits.
-
-Each drift-prone key's type is now bound to the `@objectstack/spec` symbol that
-owns it rather than restated, so it cannot drift again, and the compile-time pins
-in `FormViewWidenedArms.test.ts` fail if a future edit restates any of them by
-hand. The documented subset itself is unchanged: the keys this bridge does not
-declare, including the retirement ledger it keeps, stay exactly as they were.
diff --git a/.changeset/react-pages-guide-source-contradictions-5413.md b/.changeset/react-pages-guide-source-contradictions-5413.md
deleted file mode 100644
index dbc67fae9f..0000000000
--- a/.changeset/react-pages-guide-source-contradictions-5413.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
----
-
-Docs only — this publishes nothing, declared explicitly with an empty frontmatter rather
-than left undeclared.
-
-`content/docs/guide/react-pages.md` contradicted the framework's sources on four points.
-The guide is live material (`@object-ui/react-runtime`'s README links it twice), so each
-one was reachable teaching. All four were re-measured against `objectstack@f094214b3` and
-resolved toward the source:
-
-- **Tailwind styling.** The guide taught Tailwind `className` as the react tier's styling
- primitive, which ADR-0080's 2026-06-30 amendment retracted under ADR-0065 (Accepted): a
- page's `source` is runtime metadata the console's build-time Tailwind never scans, so an
- authored utility class silently produces no CSS. Replaced with the per-tier primitive the
- shipped `page-source-className-tailwind` rule names — inline `style={{ … }}` with
- `hsl(var(--token))` on the react tier, structured props plus a JSON `style` object on the
- html tier — and added a Styling section giving the mechanism.
-- **`record:*` scope.** The family was offered as the illustrative in-scope example while
- `os validate` rejects it (`react-block-needs-record-context`, severity error, matched by
- type). The tag-derivation rule is kept — it is accurate — and the exclusion, the real
- error text, and the per-block alternatives are now stated beside it.
-- **`adapter.find` options.** The `Live data` sample passed `filters:`, which is not a
- `QueryParams` key; `convertQueryParams` reads only `$`-prefixed keys, so the sample
- returned the object's records unfiltered with no error. Corrected to `$filter`. The same
- sample also treated the result as an array — `find` resolves to a `QueryResult`, so
- `.map` on it throws; corrected to `res.data` alongside it.
-- **Block inventory.** The guide conflated the runtime scope (every public non-container
- block) with the authored contract (`REACT_BLOCKS`: `ObjectForm`, `ListView`,
- `ObjectChart`, `Block`). Both are now stated as two tiers, with the generated
- `react-blocks.md` named as the prop authority, and the flat-props example moved off two
- deprecated `ObjectGrid` spellings (`pageSize`, `fields`) onto the canonical
- `pagination` / `fields` on `ListView`.
diff --git a/.changeset/readonly-richtext-raw-markup-5498.md b/.changeset/readonly-richtext-raw-markup-5498.md
deleted file mode 100644
index c14d4ec2e2..0000000000
--- a/.changeset/readonly-richtext-raw-markup-5498.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/fields': patch
----
-
-A readonly `markdown` / `html` / `richtext` form field now renders its content
-FORMATTED instead of showing the user its markup source (objectui#5498).
-
-`RichTextField`'s readonly early return rendered `{value}` as a React text child,
-so a readonly field of any of those three types displayed the stored markup as
-literal characters — a markdown field's asterisks and hashes, a richtext field's
-tags. The `prose` classes on that wrapper were the tell: they style rendered rich
-content, and there was none to style. Every other read surface — grid, kanban
-card, gallery, related list, dashboard record panel and the record detail page's
-read mode — dispatches through `getCellRenderer` and rendered the same stored
-bytes formatted, so one field disagreed with itself depending on which surface it
-was read on.
-
-The readonly branch now renders through the same components `getCellRenderer`
-resolves: `markdown` through the GFM renderer, `html` and `richtext` through the
-sanitizing HTML renderer. The two renderers moved out of the package barrel into
-`widgets/richTextDisplay.tsx` so the widget can reach them without importing the
-barrel back, and both sides now read one shared type-to-renderer table rather
-than two that can drift apart.
-
-The editor header's format label is fixed with it: it was computed as
-`field.format || 'markdown'`, and `format` is declared on `date` / `datetime` /
-`time` / `phone` / `auto_number` and on no rich-content type — so it read
-`undefined` for every real field and labelled an `html` field "Format: markdown".
-The label is now derived from the field type's display pipeline, so it names the
-syntax the value is actually stored in.
diff --git a/.changeset/reanchor-adr-0124-d1-citations-5701.md b/.changeset/reanchor-adr-0124-d1-citations-5701.md
deleted file mode 100644
index e176b10272..0000000000
--- a/.changeset/reanchor-adr-0124-d1-citations-5701.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
----
-
-Traceability only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. The change is comment text at eight live source sites; no
-executable line moves, and `git diff -U0` carries zero non-comment added or removed lines.
-
-`server enforces, client is courtesy` was cited at those eight sites as the framework's
-**`ADR-0057 D10`**. That decision reads *"Setup-nav surfacing follows the capability
-(ADR-0029 K2); the object stays open"* — nav-entry tiering, not enforcement location. The
-rule these sites actually invoke is decided by the framework's **`ADR-0124 D1`**, *"The
-server is the enforcement point; client-side gating is a usability courtesy"* (Accepted
-2026-08-18). #5699 fixed the repo-ambiguity half of this defect — whose ADR numbering the
-citation meant — and deliberately left the anchor half; this is that half.
-
-The new anchor is derived from an authority, not chosen: the framework's own ADR-0057
-carries a note aimed at precisely this citation — *"If a citation of `ADR-0057 D10`
-brought you here looking for that rule, ADR-0124 is where it is decided."* The substantive
-claim at every site is unchanged; only the anchor moves.
-
-The disambiguating parenthetical #5699 shipped — *"framework numbering; this repo's own
-ADR-0057 is an unrelated document"* — is retired along with the number it disambiguated.
-It warned about a collision specific to `0057`; this repository has no ADR-0124 at all
-(its own series stops at `0059`), and ADR-0124 records that a fresh, unambiguous number
-was chosen so that citations of it would not need such a warning. The `the framework's …`
-possessive stays at every site, so each one still says whose numbering it means.
-
-Left byte-untouched, deliberately: `packages/data-objectstack/src/appAccessProbe.test.ts`
-cites the same decision for *"an app gated by an absent optional service"* — the
-capability/service-gating family that decision genuinely does decide, so it is correct as
-it stands. `docs/adr/0036-field-conditional-rules.md`, the wording all eight derive from,
-carries the same misattribution and moves in its own PR: `docs/adr/**` is a governed
-surface that stops at draft for human merge.
diff --git a/.changeset/record-activity-open-vocabulary-fallback.md b/.changeset/record-activity-open-vocabulary-fallback.md
deleted file mode 100644
index f1e300a948..0000000000
--- a/.changeset/record-activity-open-vocabulary-fallback.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`record:activity` renders an author-extended `sys_activity.type` instead of dropping it
-
-**Behaviour change.** `activityRowToFeedItem` used to return `null` for any
-`sys_activity.type` outside `ACTIVITY_TYPE_TO_FEED_TYPE`: the row was stored,
-queryable and invisible, with only a console warning to say so. It now renders
-through a defined fallback presentation (`UNMAPPED_ACTIVITY_FEED_TYPE`, the
-generic `system` feed type), still announced once per distinct type.
-
-This follows the maintainer ruling of 2026-08-24 on objectstack#11507,
-direction 4: `sys_activity.type` is **author-extensible**. Every field on
-`sys_activity` is `readonly: true` and objectql's `validateRecord` skips
-readonly fields on both write branches, and ADR-0052 §5b.2 forwards an author's
-`activityMilestones[].type` into the column verbatim — so a value the platform
-never declared is legitimate stored data, not a mistake, and dropping it
-reproduces the objectui#5840 failure for every author who writes one.
-
-The types this map deliberately excludes (`commented` / `mentioned` / `login` /
-`logout` → `undefined`) are unchanged: those are decisions, not gaps, and they
-still return `null` silently. `ACTIVITY_TYPE_TO_FEED_TYPE` itself is unchanged,
-so the copy `RecordDetailView` reads is unaffected.
-
-The pin that goes with it replaces the set-equality check objectui#5840 removed,
-and is deliberately two-directional: the map must cover every **built-in** type
-(superset — a new built-in turns it red), and an unknown type must reach the
-feed through the fallback (never dropped, never crashing). Set equality is not
-asserted in either spelling, and the docblock says so, because pinning to the
-closed declaration is what made #5840 drop stored rows.
diff --git a/.changeset/record-activity-scheduled-event-5840.md b/.changeset/record-activity-scheduled-event-5840.md
deleted file mode 100644
index 011e1447b6..0000000000
--- a/.changeset/record-activity-scheduled-event-5840.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-"@object-ui/plugin-detail": patch
----
-
-`record:activity` no longer discards scheduled activities. `sys_activity` rows with
-`type: "scheduled"` now map to the `event` feed kind instead of being dropped before any
-filter runs, so a not-yet-held meeting reaches the timeline and `types: ['event']` stops
-being a permanently empty tab. Held meetings are unchanged: `completed` still maps to
-`task` and still hides unless `showCompleted` is set, which is what keeps an upcoming
-meeting visible by default while a finished one is not.
-
-The unknown-type default is unchanged and deliberately so — a row whose type nothing maps
-is still dropped rather than bucketed into `system`, because rendering an unmeasured kind
-as something it is not would be new wrong data rather than recovered data. What changes is
-that the drop is no longer silent: an unmapped type now logs one `console.warn` naming it,
-once per type. Types the map knows and deliberately excludes (`commented`, `mentioned`,
-`login`, `logout`) stay silent, since those are decisions rather than gaps.
diff --git a/.changeset/record-activity-types-filter-never-widens-5841.md b/.changeset/record-activity-types-filter-never-widens-5841.md
deleted file mode 100644
index 94250820b8..0000000000
--- a/.changeset/record-activity-types-filter-never-widens-5841.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-"@object-ui/plugin-detail": patch
----
-
-`record:activity` no longer widens when its `types` filter cannot be honoured. An
-unrecognised or empty kind list used to sanitise to `undefined`, which the filter
-pipeline reads as "no filter authored" — so three distinct authored intents collapsed
-into one rendering, and the widest one: a page that named the wrong kind was served
-every activity on the record, with no diagnostic anywhere at runtime. The principle now
-held is that a sanitiser may narrow an author's request or refuse it, but must never
-silently widen it; widening turns a typo into "show the user everything", which is the
-one outcome no author asked for, and it hides behind a plausible result — a populated
-timeline reads as working, an empty one gets investigated.
-
-**Behaviour change.** A page authoring `types: []`, or a `types` list whose every entry
-is unrecognised, now renders an EMPTY timeline where it previously rendered every kind.
-That is the fix rather than a regression, but a page relying on the old fallback will
-visibly change: `types: []` is honoured as "no kinds", and a list that keeps nothing
-filters to nothing. A mixed list keeps its recognised entries and drops the rest. Omitting
-`types` is unchanged and remains the only way to say "no filter". A `types` that is not a
-list at all (`types: 'comment'` — brackets dropped) is likewise refused rather than
-ignored, since a filter that cannot be read is not a request to remove the filter.
-
-Unrecognised entries are now named out loud: one `console.warn` listing them and the
-declared feed item types, deduped so one bad kind warns once however many times the feed
-re-renders, matching the unknown-activity-type diagnostic already in this file. A
-well-formed filter stays silent, `types: []` included — there is nothing to report about
-a request that was carried out exactly.
diff --git a/.changeset/record-activity-unproduced-feed-kinds-5877.md b/.changeset/record-activity-unproduced-feed-kinds-5877.md
deleted file mode 100644
index d99137a020..0000000000
--- a/.changeset/record-activity-unproduced-feed-kinds-5877.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`record:activity` now says out loud when a `types` entry names a feed kind nothing produces.
-
-`FeedItemType` publishes thirteen kinds and ObjectUI produces five of them
-(`comment`, `field_change`, `task`, `event`, `system`). Authoring
-`types: ['approval']` parsed, typechecked, built, and rendered a permanently
-empty timeline with no diagnostic anywhere — a declared surface enforced by
-nothing, which reads to an author, or to an AI writing the metadata, as a
-working feature that simply has no data yet. The entry is still honoured exactly
-as authored and nothing that renders changes; what changes is that the emptiness
-is now diagnosable, on its own deduped channel beside the unrecognised-entry and
-unrecognised-`filterMode` warnings already in this block.
-
-The message keeps two populations apart, because reporting a decision as a
-defect is how a warning channel gets trained out of an author's attention:
-`record_create` / `record_delete` / `sharing` are named as **deliberately not
-adopted** — those rows map to `field_change` on purpose, since the record page
-and this block must agree what a `created` row is before either can move to the
-richer kind — while the remaining kinds are reported as having no producer, and
-the message stops there rather than calling that a gap. Whether somebody ruled
-against a kind and left no note is not recorded anywhere the renderer can read,
-and the wording says so instead of guessing.
-
-The producer census the diagnostic reads is derived from the producers
-themselves — the `sys_activity` map's range plus its unmapped fallback — so
-giving a kind a producer retires its warning in the same edit. The one producer
-that cannot be derived (`comment`, built from `sys_comment` rows by the console's
-record page, a package this one cannot import) is declared, and a new test
-re-runs the census over the whole repository so the declaration cannot go stale
-silently. The warning also names what it cannot see: a host that supplies the
-feed itself can produce kinds no census taken here bounds.
diff --git a/.changeset/record-alert-cta-label-i18n-4998.md b/.changeset/record-alert-cta-label-i18n-4998.md
deleted file mode 100644
index c42fa2e98f..0000000000
--- a/.changeset/record-alert-cta-label-i18n-4998.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is
-widened to `string | I18nLabel` in both copies (`properties.*` and the flat
-compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`.
-
-The renderer already resolves `action.label` through the same inline-locale-map
-`pickLocalized` call as `title` / `body` (`const ctaLabel =
-pickLocalized(props.action?.label, language)`), so a bare `string` declaration
-was narrower than the renderer's own runtime behavior — the same
-declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for
-`title` / `body` one level up in the same interface (objectui#4998).
-
-Type-only: the block's published authoring surface still declares `action` as
-a bare `object` with the member shape in prose only
-(`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet —
-that half stays parked on the `ComponentInput` member-shape question (PR
-#3795) and is out of scope here.
diff --git a/.changeset/record-alert-loading-frame-diagnostic-5776.md b/.changeset/record-alert-loading-frame-diagnostic-5776.md
deleted file mode 100644
index e6b72b9772..0000000000
--- a/.changeset/record-alert-loading-frame-diagnostic-5776.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/plugin-detail': patch
----
-
-`record:alert`'s `visible` predicate no longer logs a spurious `record is not
-defined` evaluation-failure warning during the record-detail loading frame.
-
-The predicate is evaluated on every render (Rules of Hooks), including the
-frame before `useRecordContext().data` has loaded. In that frame `record` is
-unset, `usePredicateRecordContext` binds an empty context bag by design (no
-`record` key at all), and a bare/`${…}` predicate referencing `record.*`
-faulted with `record is not defined` — logged via `console.warn` on every
-page load, even the correct, working ones, because the SAME predicate
-resolved fine one frame later once `record` populated. The banner's own
-visibility was never wrong (it already hides unconditionally while unloaded);
-only the diagnostic was permanently misleading on the happy path.
-
-The predicate is now skipped while `record` hasn't loaded — the same
-condition the banner's own "hide while unloaded" early return already used —
-since its verdict in that frame was never consulted anyway. A predicate that
-is genuinely broken (bad field, bad syntax) still faults, and still logs, on
-every frame once `record` is populated (objectui#5776).
diff --git a/.changeset/record-alert-row-binding-4807.md b/.changeset/record-alert-row-binding-4807.md
deleted file mode 100644
index ecca389daf..0000000000
--- a/.changeset/record-alert-row-binding-4807.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-'@object-ui/plugin-detail': minor
----
-
-`record:alert` binds the row through `usePredicateRecordContext`, so an
-author-declared `properties.visible` is actually consulted.
-
-`renderers/record-alert.tsx` was the last predicate face in the repo still
-handing `useCondition` a root-only `{ record }` bag. Every other row-scoped
-predicate — the four generic action renderers (objectui#4075) and app-shell's
-`DeclaredActionsBar` (objectui#4077) — binds the row through the shared
-`usePredicateRecordContext(record)` helper, which resolves the three spellings
-objectui#5330 ruled on: canonical `record.status`, the deprecated row-action
-shorthand `status`, and deprecated legacy `data.status`.
-
-Under the root-only bag only the canonical spelling worked, and the two others
-failed in **opposite** directions — both of them silently, because this call
-site is fail-soft:
-
-- **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator
- threw. The legacy `${…}` path answers a throw with its own source text, a
- non-empty and therefore truthy string, so the verdict was **SHOWN on every
- row**. A banner the author had gated was permanently on screen.
-- **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's
- ambient predicate scope (`providers/ExpressionProvider.tsx`) carries
- `data: {}`, so the predicate read that object instead of the row, compared
- `undefined`, and the verdict was a constant false — **never shown**.
-
-**Behaviour change, stated plainly:** a shipped `record:alert` whose `visible`
-was written in either deprecated spelling was inert and is now live. A banner
-that was permanently visible may begin to hide, and one that never appeared may
-begin to show — that is the point of the fix, but it is a verdict change rather
-than a no-op. Canonical `record.*` predicates are unaffected in verdict: they
-resolved before and resolve now, pinned on both polarities. An in-tree census
-found no `record:alert` `visible` predicate outside this package's own tests.
-
-A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`,
-with its own deliberate bindings (`data` is the data-source adapter there, not
-the row). This change does not touch it; the two still compose as AND.
-
-The renderer's header comment described the shared-scope behaviour it did not
-have. It now describes what the file does, including the fail-soft policy and
-the two-gate composition.
diff --git a/.changeset/record-detail-param-dialog-title-5610.md b/.changeset/record-detail-param-dialog-title-5610.md
deleted file mode 100644
index f8c16e205f..0000000000
--- a/.changeset/record-detail-param-dialog-title-5610.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-`RecordDetailView`'s param-collection dialog now titles itself from `action.label`
-alone — the unreachable `|| action?.title` fallback beside it is removed
-(objectui#5610).
-
-This was the second copy of the limb objectui#4282 removed from
-`useConsoleActionRuntime`. `RecordDetailView` builds its own action runtime rather
-than routing through that hook, so the two near-identical `paramCollectionHandler`s
-have drifted as a pair and the first fix could not reach this one.
-
-`title` is declared on no action surface in the ecosystem: it is absent from
-`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from
-`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS`
-inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts`
-`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`,
-`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand
-side of that `||` could not be reached by authored metadata: a fallback that cannot
-fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce.
-Nothing a user hits changes; the line now reads exactly one key, matching the
-`description` line directly below it.
-
-`RecordDetailView.paramDialogTitle.test.tsx` pins the reader so the alias cannot be
-reinstated silently: an action carrying `title` and no `label` must open an untitled
-dialog rather than a dialog named by a key no producer sets. A pin per reader is the
-only shape that covers both handlers, since the hook's own pin cannot see this site.
diff --git a/.changeset/record-handoff-card.md b/.changeset/record-handoff-card.md
deleted file mode 100644
index ca1c638ff0..0000000000
--- a/.changeset/record-handoff-card.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
-'@object-ui/app-shell': patch
----
-
-「打开这条记录 →」卡片——ask 的记录交接终于有了客户端的另一半
-
-服务端半边(cloud#1659 的 `open_record`)先落了地,实测发现它是**半活的**:agent 发出
-`status:'record_handoff'`、回答说「点击上方链接打开」,而上方根本没有链接——控制台
-有 `build_handoff` 的探测器,这个状态一处都不认识,信号被原样丢弃。
-
-按五步补齐:`detectRecordHandoff`(含持久化 `{type:'text',value}` 包裹形状——replay
-信封那课的规矩)→ live 映射提升 → 水合提升 → 卡片渲染 → 宿主回调。
-
-两个设计点:
-
-- **app 段点击时现场解析**。记录路由要 `/apps/:app/:object/record/:id`,交接载荷只有
- 对象和记录 id;宿主回调用一次同源元数据读取 `_packageId` 再导航,不给 agent 增加
- 它未必知道的参数。
-- **刻意不做「被取代」置灰**。builder 卡的旧 prompt 会过时,旧的记录链接不会——记录
- 不因为有新交接而失效。
-
-真机闭环验证:问「把《沉默的大多数》标记成已读」→ 卡片渲染
-(`沉默的大多数 — 把阅读状态改为已读`)→ 点击 → 落在
-`/apps/app.hdke/hdke_book/record/` 详情页,「编辑」在手边。
-
-缺任一 id 的交接在探测器就被丢弃,与服务端的拒绝对称——指向空处的卡片比散文更糟。
diff --git a/.changeset/record-picker-empty-text-i18n-5590.md b/.changeset/record-picker-empty-text-i18n-5590.md
deleted file mode 100644
index 8c53ac4a90..0000000000
--- a/.changeset/record-picker-empty-text-i18n-5590.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`element:record_picker`'s `emptyText` now resolves the inline per-locale map its
-contract has admitted since rc.6, and its published declaration says so
-(objectui#5590).
-
-`@objectstack/spec` widened this key to the `I18nLabel` union
-(`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA
-still carries it — measured, not assumed:
-`ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })`
-succeeds. The renderer honoured only the string arm, handing the map straight to a
-text node. React refuses a plain object in a child position rather than stringifying
-it, so an author writing the map form the contract accepts did not get a mis-rendered
-empty state — the whole picker subtree threw
-`Objects are not valid as a React child (found: object with keys {en, zh-CN})`.
-
-The read site now resolves through `pickLocalized`, the objectui-side helper the
-sibling text-node sites already read through (`element:text.content`,
-`element:button.label`, `page:card.title`), which spells a miss as `''` rather than
-the spec resolver's `undefined`. The default is applied before resolution, so
-`emptyText` absent still means "No records" and an authored empty string still
-renders empty.
-
-The `ComponentMeta` entry, which held a single `'string'` arm precisely because the
-renderer dropped the other one, now declares `['string', 'object']`. That narrowing
-was correct for exactly as long as it was true: with the map arm reaching the screen
-resolved, withholding it would be the false declaration in the other direction — the
-manifest gate reporting `type-mismatch` on a legal write the same input's own
-`description` teaches the author to make. The `apps/console` specimen that pinned the
-narrow arm named this release condition in its own words ("keeps its single `'string'`
-arm until the render site catches up") and is flipped here, keeping its controls.
-
-Three comments in the renderer deferred this gap to objectui#4163, which closed as
-completed on 2026-08-15 while the gap was still open; the file now carries no
-reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very
-key as its worked example of an arm deliberately withheld, and is corrected in the
-same change so the example stays true.
diff --git a/.changeset/record-picker-label-association-5771.md b/.changeset/record-picker-label-association-5771.md
deleted file mode 100644
index df693b93c5..0000000000
--- a/.changeset/record-picker-label-association-5771.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`element:record_picker`'s `label` is now programmatically associated with its
-`SelectTrigger` combobox — fixing a case where the caption the block's own
-registration prose advertised (`'Caption rendered above the picker, in a
- element'`) named nothing at all (objectui#5771).
-
-Pre-fix, the render body was `{label && {label} }`
-against a `SelectTrigger` with no `id`: neither end of the association carried
-any wiring. That is a step worse than the sibling gap objectui#5735 closed on
-`element:text_input` — there the `label` half was already correctly wired and
-only `description` was adrift; here the label element had no `htmlFor` and the
-trigger had no `id`, so the picker had no accessible name unless a
-`placeholder` happened to render text into the trigger's content.
-
-The fix follows the shape objectui#3341 already ruled on for the same defect
-class (`InlineCreateRelated`'s ``/` ` pair) and objectui#5735
-just landed on the complement block: `htmlFor`/`id` against the
-`SelectTrigger` — a `button` with `role="combobox"`, and therefore labelable,
-so no `aria-labelledby` is needed (Radix sets none on the trigger) — wired
-**when `schema.id` exists**, matching `element:text_input`'s precedent rather
-than an always-on `useId()` fallback. Only the author can supply `schema.id`,
-so a picker authored without one keeps its pre-fix, unassociated caption text
-exactly as before; nothing about this change mints an id the author did not
-provide. The `` element itself is also swapped for the shared `Label`
-(`@radix-ui/react-label`) primitive `element:text_input` already uses for the
-same wiring, rather than a bare `` with a `htmlFor` bolted on.
-
-The registration prose for `label` is corrected to describe what the code now
-does (tied to the control by `htmlFor` when the node carries an `id`) instead
-of a caption association the renderer never performed.
diff --git a/.changeset/record-picker-label-placeholder-i18n-5637.md b/.changeset/record-picker-label-placeholder-i18n-5637.md
deleted file mode 100644
index 58a25eaf35..0000000000
--- a/.changeset/record-picker-label-placeholder-i18n-5637.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`element:record_picker`'s `label` and `placeholder` now resolve the inline
-per-locale map their contract admits, and their published declarations say so
-(objectui#5637).
-
-These are the two remaining keys of the trio objectui#5590 fixed one of. All
-three members of `ElementRecordPickerPropsSchema` are the `I18nLabel` union
-(`string | Record< string, string >`) — measured on the installed
-`@objectstack/spec` 17.1.0 pin, where each resolves to
-`optional -> union -> string | record` and
-`safeParse({ object: 'account', : { en, 'zh-CN' } })` succeeds. The
-renderer honoured only the string arm on both, and the two keys failed in two
-different ways:
-
-- `placeholder` was read raw and handed to `SelectValue`. React refuses a plain
- object in that position rather than stringifying it, so the whole picker
- subtree threw
- `Objects are not valid as a React child (found: object with keys {en, zh-CN})`
- — the same harm objectui#5590 removed from `emptyText`.
-- `label` went through the file's local `toText`, whose object branch ends
- `String(o.label ?? o.name ?? o.title ?? o.en ?? '')`. Reaching `o.en`
- unconditionally is an English pick wearing locale resolution's clothes, so a
- `zh-CN` viewer was shown the English entry — and a map that simply omits `en`
- resolved to `''`, which the `{label && …}` render site drops, making the
- picker's label element DISAPPEAR with nothing thrown and nothing logged.
-
-Both keys now resolve at their own read site through `pickLocalized`, the same
-helper the settled `emptyText` shape uses. `toText` is deliberately unchanged:
-it is shared with the row values (`toText(row?.[labelField])`), which are record
-field values rather than `I18nLabel`, so teaching it locale resolution would
-have changed a second, unrelated call site. The `placeholder` default is applied
-before resolution, so an absent key still means "Select a record…" and an
-authored empty string still renders empty.
-
-The two `ComponentMeta` entries, which held a single `'string'` arm precisely
-because the renderer dropped the other one, now declare `['string', 'object']`
-— declared in the change that makes the arm render, never before, which is the
-order `ComponentInput.type` prescribes and the order `emptyText` set.
-
-KNOWN GAP, unchanged by this release: the sibling `label` read sites in
-`renderers/layout/containers.tsx` compose
-`translateLabel(pickLocalized(…), language)`, and that second helper is not
-applied here — `translateLabel` and its `KNOWN_LABEL_DICT` are module-private to
-that file. Only the locale-map resolution lands in this change; a plain-English
-string `label` is still rendered verbatim in every language, exactly as before.
diff --git a/.changeset/record-quick-actions-resultdialog-pin-5711.md b/.changeset/record-quick-actions-resultdialog-pin-5711.md
deleted file mode 100644
index 351cbd3b8a..0000000000
--- a/.changeset/record-quick-actions-resultdialog-pin-5711.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Test-only change — no published behaviour changes.
-
-Adds a component-level test pinning that a `record:quick_actions` action's
-declared `resultDialog` reaches the ambient ``'s shared
-`ActionRunner` and opens the reveal dialog with the response value (success
-toast suppressed), plus the negative leg — with no provider, the response is
-silently discarded and the documented `console.warn` fires. Closes the gap
-found while implementing objectstack#10681 (objectui#5711).
diff --git a/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md b/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md
deleted file mode 100644
index c9b1b8e39c..0000000000
--- a/.changeset/record-title-drop-undeclared-objectdef-titlefield-6531.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/core': patch
----
-
-`getRecordDisplayName`: stop consulting the undeclared object-level
-`titleField`, restoring `nameField` as the top of the object ladder
-
-Step 0 of the unified record-title resolver read
-`options?.titleField ?? objectDef?.titleField`. The second leg ranked an
-object-level `titleField` above `nameField` — the pointer ADR-0079 Phase 2 made
-canonical — and above the deprecated `displayNameField` alias and the legacy
-`titleFormat` template.
-
-`@objectstack/spec`'s object schema does not declare that key, and it is not
-merely undeclared: the schema is a `strictObject`, so
-`ObjectSchema.safeParse({ …, titleField: 'x' })` fails with `unrecognized_keys`
-— the same code a nonsense key gets — while `nameField`, `displayNameField` and
-`titleFormat` all parse and survive. A producer census across both repos found
-nothing that puts the key on an object-shaped payload: not the metadata, not any
-`getObjectSchema` implementation (the ObjectStack adapter stamps only reference
-keys and field-widget hints), not the lookup-chip path, not the
-search-candidate path, and not the platform's own server-side resolver
-(`@objectstack/objectql#titleFieldOf` reads `nameField` → `displayNameField`).
-Reading a key no producer can ship is a consumer-side alias — the shape
-Commandment #0.1 bans — and it inverted the governed-authority default on top of
-that.
-
-No authoring surface changes and no view loses its author-chosen title field:
-`titleField` remains a real, declared VIEW key (`ui/CalendarConfig`,
-`ui/GalleryConfig`, `ui/GanttConfig`, `ui/ListMapConfig`,
-`ui/ObjectKanbanProps`, `ui/TimelineConfig`), views hand it in as
-`options.titleField`, and that half of step 0 still wins over everything.
-The behaviour change is confined to an object payload that carried a key the
-contract rejects: it now resolves through the declared ladder instead.
diff --git a/.changeset/reference-rail-entries-stale-prose.md b/.changeset/reference-rail-entries-stale-prose.md
deleted file mode 100644
index 560c79cef5..0000000000
--- a/.changeset/reference-rail-entries-stale-prose.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Test-only comment refresh in `registry-inputs-spec-parity.test.ts`: the
-`record:reference_rail.entries` exemption note no longer narrates the
-`ReferenceRailEntry` `icon` divergence as an open contract question (ruled
-Option B 2026-08-22, objectui#5494 landed the derivation from
-`@objectstack/spec/ui`), and drops the "the renderer reads it" premise that was
-false when written. The exemption itself is unchanged — `inputs` is a flat
-scalar carrier and still cannot express an array of objects. No published
-behaviour changes.
diff --git a/.changeset/reference-rail-entry-spec-derived.md b/.changeset/reference-rail-entry-spec-derived.md
deleted file mode 100644
index fd9b93bab6..0000000000
--- a/.changeset/reference-rail-entry-spec-derived.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-"@object-ui/plugin-detail": minor
----
-
-**`ReferenceRailEntry` is now the spec's type, and the reference-rail `icon` key retires** (objectui#5494, maintainer ruling 2026-08-22).
-
-`ReferenceRailEntry` is owned by `@objectstack/spec` as of 17.1.0. The hand-written interface in `record-reference-rail.tsx` is replaced by a re-export of the spec's `ReferenceRailEntry` (derived from `ReferenceRailEntrySchema`, `$strict` over `{ objectName, relationshipField, title?, limit?, displayField? }`), and `buildDefaultPageSchema` no longer emits `icon` on the reference-rail entries it synthesizes.
-
-**Migration — if you write `icon` on `record:reference_rail` entries, remove it.** Be aware of what this does and does not change:
-
-- **Runtime validation does not move.** The spec's strict schema already refused `icon` at save — that refusal is unchanged. What was broken was objectui's declaration: the TS type advertised a key that could never be saved.
-- **Nothing ever rendered `icon`.** No render path in `RecordReferenceRailRenderer` has ever read the key (measured back to the file's first commit, and independently by the spec's `ui-reference-rail-unknown-keys-refused` migration entry). An authored `icon` that survived in unsaved/preview metadata was already a silent no-op — after this change the TS type says so instead of suggesting otherwise.
-- **The published TS surface narrows.** Code that imported `ReferenceRailEntry` from `@object-ui/plugin-detail` internals and set `icon` will now get a type error. That error is the contract speaking: remove the key.
-
-The reference-rail icon affordance is retired rather than proposed upstream: a stock scan of this repo, its examples and schema-catalog corpus, and the objectstack tree (examples, templates, docs) found no reachable authored usage of `icon` on reference-rail entries. Stored customer metadata is not reachable from this seat and was not scanned.
diff --git a/.changeset/registerfunction-casefold-doc-5363.md b/.changeset/registerfunction-casefold-doc-5363.md
deleted file mode 100644
index def190682d..0000000000
--- a/.changeset/registerfunction-casefold-doc-5363.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/core': patch
----
-
-`ExpressionEvaluator.registerFunction` now documents the case-fold it has always
-performed: the name is stored — and must be called — in UPPER CASE
-(objectui#5363).
-
-`registerFunction('formatCurrency', fn)` registers `FORMATCURRENCY`, because the
-method delegates to `FormulaFunctions.register`, which stores under
-`name.toUpperCase()`. That fold is correct for the spreadsheet-style built-in
-vocabulary (`SUM`, `IF`, `UPPER`) and is unchanged here — but nothing declared
-it on the public method, and two things keep it from being self-evident at the
-call site. The registry API stays case-insensitive, so `getFormulas().has()` and
-`.get()` both answer to the original spelling and never reveal the fold; only
-expressions see the stored key, because the evaluation scope is built from
-`FormulaFunctions.toObject()`, a plain object whose identifiers are matched
-case-sensitively. And a wrong-case call site does not raise: `evaluate()`
-catches, warns, and returns `defaultValue ?? expression`, so the template
-renders its own `${...}` source as literal text on screen rather than erroring.
-
-Behavior is untouched — this is the declaration catching up with what the code
-enforces. It ships as a patch rather than as an empty changeset because the
-JSDoc is emitted into the published `dist/evaluator/ExpressionEvaluator.d.ts`,
-so it is what consumers see on hover.
-
-`ExpressionEvaluator.test.ts` gains three cases pinning the half that was
-uncovered — that the given spelling does *not* resolve in an expression, that
-the failure renders the raw template source instead of throwing, and that the
-registry API stays case-insensitive underneath — so making registration
-case-preserving fails a test instead of silently invalidating the new JSDoc.
diff --git a/.changeset/release-lane-tests-and-concurrency-5404.md b/.changeset/release-lane-tests-and-concurrency-5404.md
deleted file mode 100644
index 31f50fe34e..0000000000
--- a/.changeset/release-lane-tests-and-concurrency-5404.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-CI only — this publishes nothing, declared explicitly with an empty frontmatter rather
-than left undeclared.
-
-Two changes to `.github/workflows/changeset-release.yml`, the workflow whose last step
-publishes to npm.
-
-**The duplicate `pnpm test` is gone.** It re-tested a commit already on `main`, so it
-could never keep anything out — it could only stop the release afterwards, and it did:
-runs #3606 and #3901, 2 of the last 27 release-PR merges, failed there and skipped the
-changesets step entirely. It cost 31m19s of a 33m22s job (medians over the 106 runs of
-#3712-#3911 that executed; install 6s, build 35s, the changesets action 24s). The
-backstop for `main` is `ci.yml`'s push lane, which runs the whole suite under coverage
-across four shards and enforces the thresholds on the merged report.
-
-**The concurrency group is now keyed by commit rather than by branch.** A group shared by
-every push to `main` does not queue: GitHub holds one pending run per group and cancels
-the rest, so 93 of those same 200 runs were `cancelled` with an empty jobs array — not
-one step ever ran. On a workflow that publishes, a discarded run is a discarded publish.
-Ordering moves into a fail-open wait step that holds a run until every older release run
-has finished, so runs are still serialised, but a contended lane now delays a release
-instead of dropping it.
diff --git a/.changeset/remove-inert-showrefresh-5567.md b/.changeset/remove-inert-showrefresh-5567.md
deleted file mode 100644
index 017478cc2f..0000000000
--- a/.changeset/remove-inert-showrefresh-5567.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-'@object-ui/plugin-view': minor
-'@object-ui/app-shell': patch
----
-
-Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567).
-
-The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key.
-
-Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected.
-
-`@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change.
diff --git a/.changeset/rename-command-palette-shortcuts-demo.md b/.changeset/rename-command-palette-shortcuts-demo.md
deleted file mode 100644
index 4effb7cfe8..0000000000
--- a/.changeset/rename-command-palette-shortcuts-demo.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Rename the `components-form-command/command-palette-with-shortcuts` catalog demo to
-`components-form-command/file-command-palette`, so its id, title and docs heading name what
-the fixture actually renders after objectui#6157 removed the invented `shortcut` key. The
-docs heading, the generated catalog index and the fixture test follow the rename.
-
-No published behaviour changes: the two `@object-ui/components` files touched are a code
-comment and a test name that spelled the old filename.
diff --git a/.changeset/render-publish-advisory-findings-5026.md b/.changeset/render-publish-advisory-findings-5026.md
deleted file mode 100644
index 659dd337e1..0000000000
--- a/.changeset/render-publish-advisory-findings-5026.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/data-objectstack': minor
-'@object-ui/app-shell': patch
-'@object-ui/i18n': patch
----
-
-Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save
-
-objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's.
-
-The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door.
-
-`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones.
-
-One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb.
-
-**BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group.
-
-Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) still discards per-draft advisories server-side — objectstack#9343, open and unruled — and nothing here compensates for that from the client side. A test pins the absence, so a later traversal of a batch-shaped `published[]` cannot be added without turning it red.
diff --git a/.changeset/replay-dispatch-error-5695.md b/.changeset/replay-dispatch-error-5695.md
deleted file mode 100644
index 8c014116b1..0000000000
--- a/.changeset/replay-dispatch-error-5695.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
-'@object-ui/app-shell': patch
----
-
-Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24.
diff --git a/.changeset/report-view-datasource-object-key-5116.md b/.changeset/report-view-datasource-object-key-5116.md
deleted file mode 100644
index 2d3435113c..0000000000
--- a/.changeset/report-view-datasource-object-key-5116.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-`ReportView` reads a report's data binding through the one key the contract declares — `dataSource.object`.
-
-The view accepted `resource` as a second spelling of `object`, in two places, and
-named that spelling in a warning the user could read:
-
-```
-:171 liveReport?.objectName || liveReport?.dataSource?.object
- || liveReport?.dataSource?.resource
-:273 dataFetchSource.dataSource.object || dataFetchSource.dataSource.resource
-:275 console.warn('ReportView: dataSource missing object/resource property')
-```
-
-`resource` is not on this binding. `ElementDataSourceConfig` declares `object`,
-`view?`, `filter?`, `sort?` and `limit?`; its `@objectstack/spec` twin
-`ElementDataSourceSchema` is a strict object, so an extra `resource` key is
-*rejected* there rather than ignored; and the binding's own predicate
-`isElementDataSourceConfig` decides on `object`. A `resource`-only binding
-therefore was never a binding on any other renderer in the system — it rendered
-here and silently produced nothing anywhere else, with neither end reporting a
-problem. That divergence is what a consumer-side alias buys: one renderer
-answering a question the contract says has no answer.
-
-`resource` is a real key on other surfaces — `CRUDSchema.resource`, the
-`DataSource` adapter's first parameter, `LiveExportOptions.resource` — and all
-three are untouched. None of them is this one.
-
-Behaviour, measured by rendering each input shape before and after. Only the
-`resource`-only shape moves:
-
-| binding | before | after |
-| --- | --- | --- |
-| `object` only | queries that object | unchanged |
-| `resource` only | queries it as if declared | not queried; named warning, no rows, fallback field list |
-| both | queries `object` | unchanged |
-| neither | not queried; warning | unchanged |
-
-So off-spec report metadata that used to render now fails loudly instead of
-appearing to work. A producer census found nothing that would notice: no site in
-this repository, and none in the `objectstack` framework repository, writes
-`resource` onto a report `dataSource`. The limb was speculative in the commit
-that introduced it, and per AGENTS.md #0.1 an off-spec spelling is corrected at
-the producer, never taught a second dialect by the renderer.
-
-The `:275` wording now names only `object`. A diagnostic that lists a key the
-contract does not declare is not a small thing: it is the system telling an
-author — increasingly, an author's code generator — that the wrong spelling is
-supported.
diff --git a/.changeset/retire-codecov-upload-5436.md b/.changeset/retire-codecov-upload-5436.md
deleted file mode 100644
index 66b7492a39..0000000000
--- a/.changeset/retire-codecov-upload-5436.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-CI only — this publishes nothing, declared explicitly with an empty frontmatter rather
-than left undeclared.
-
-`ci.yml`'s `Test (coverage)` job no longer uploads to Codecov. `CODECOV_TOKEN` was never
-set on this repository and Codecov no longer accepts tokenless uploads, so the step failed
-server-side (`Token length: 0`) on every push after objectui#5403 landed — a permanently
-red lane that trains everyone to ignore red. Per the maintainer ruling of 2026-08-22
-(Option B), the upload is retired rather than the secret set: the merged report is now
-published as the `coverage-report` build artifact (7 days, matching the shard blobs it is
-derived from) alongside the existing step summary.
-
-The coverage GATE is untouched and was never Codecov's: the configured
-`coverage.thresholds` are enforced by the merge step over the whole merged map, and the
-shard legs still override them to zero so that merge step remains the only place the gate
-runs. objectui#5403's loud verdict step is carried forward rather than deleted — it now
-reports `shards=… blobs=… merge=… report=…` and is green only when all four shards passed,
-all four blobs arrived, they merged, the thresholds were enforced over the result, and the
-report was published. A threshold breach is annotated separately from a lane that never
-delivered, because the two call for opposite actions.
-
-What is given up is the Codecov trend dashboard and its PR coverage comments.
diff --git a/.changeset/retire-components-action-param-dialog-5685.md b/.changeset/retire-components-action-param-dialog-5685.md
deleted file mode 100644
index 803fed5351..0000000000
--- a/.changeset/retire-components-action-param-dialog-5685.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/components': minor
----
-
-Retire `ActionParamDialog`: the `custom` barrel's second action-param dialog is
-removed, and the app-shell dialog is recorded as the surviving implementation
-(objectui#5685, maintainer ruling of 2026-08-22).
-
-**Breaking for any out-of-repo host that imported it** (declared `minor` per the
-repo's version-alignment rule — the major tracks `@objectstack`, never an
-API-break count): `@object-ui/components` no longer exports `ActionParamDialog`
-or `ActionParamDialogProps`. Measured at the branch point, the export had zero
-production consumers — its only in-repo importers were its own five test files,
-which retire with it, and no other repository in the organization imports the
-symbol from this package.
-
-This file was the repo's SECOND implementation of the action-param surface, and
-its audit trail is the reason it retires instead of being maintained: the last
-close look (objectui#4758) found per-option `visibleWhen` not evaluated at all,
-and the five hardcoded English strings this card originally recorded were the
-next drift installment. A dormant second dialect of a governed surface is one
-production import away from being live; removing it removes the whole drift
-class.
-
-FROM → TO for an out-of-repo host:
-
-- `import { ActionParamDialog } from '@object-ui/components'` — no drop-in
- replacement is published. The surviving implementation is
- `@object-ui/app-shell`'s `ActionParamDialog` (`src/views/ActionParamDialog.tsx`),
- rendered by app-shell's action runtime (`useConsoleActionRuntime`,
- `RecordDetailView`) rather than exported standalone. A host that needs its own
- param form builds on `@object-ui/fields`' shared field widgets
- (`resolveFormWidgetType` / `getLazyFieldWidget`, ADR-0059) — the same seam the
- surviving dialog renders through.
-
-The unreleased objectui#4758 changeset for this component (`select` options
-through the shared option evaluator) is withdrawn in the same change: the
-component retires before that fix ever ships, so the release notes carry the
-removal rather than new behaviour of a surface this release does not contain.
diff --git a/.changeset/retire-spec-bridge-6366.md b/.changeset/retire-spec-bridge-6366.md
deleted file mode 100644
index 86bdf64d64..0000000000
--- a/.changeset/retire-spec-bridge-6366.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-'@object-ui/react': minor
----
-
-refactor(spec-bridge): retire the spec-bridge — `SpecBridge`, `bridgeListView`, `bridgeFormView` (#6366)
-
-Removed under the 2026-08-27 maintainer ruling on #6366 (Option A — remove, and
-for `SpecBridge` as a WHOLE, not just the form half), on the same criteria the
-17.0.0 entry used to remove `bridgePage` / `bridgeDashboard`: no runtime
-consumer, and a route that could not deliver a working result even in
-principle. The stage-1 measurement (2026-08-26, PM-verified) the ruling rests
-on:
-
-- **Zero consumers at every reachable endpoint** — no non-test caller in this
- repo, none in the sibling `objectstack` repo (which does not depend on
- `@object-ui/react` at all), and a GitHub-wide public code search returning
- hits only inside this repository. External npm-private hosts are
- unmeasurable from here; the ruling records fallback C (keep + document the
- boundary) should evidence of one ever surface.
-- **The bridged form route was structurally unable to work**: a bridged node
- carries neither `objectName` nor `customFields`, so `ObjectForm` takes the
- branch its own code labels "cannot proceed" and renders, in the registry
- wrapper's words, "a field-less card in silence". The only working host
- recipe (`{...node, objectName: '...'} as any`) existed solely inside the
- list bridge's integration tests and was documented nowhere.
-
-This knowingly reverses the 17.0.0 changelog line "The `list` and `form`
-bridges are unaffected and remain the live authoring path" — by maintainer
-ruling, on the same measurement criteria that entry itself applied.
-
-#5898's restored-key work on the form-view bridge (the
-`FormViewSpecConformance` suite and the spec keys it carried onto the bridged
-node) is **superseded by this removal, not fixed** — the route it repaired is
-gone. #6366's measured type-vocabulary asymmetry (a bridged field carrying
-`text` where the normalizer produces `field:text`) is likewise mooted rather
-than repaired.
-
-The suites pinning the removed route go with it — retirement of a route
-nothing travels, not quarantine: the seven suites under
-`react/src/spec-bridge/__tests__/`, and plugin-grid's
-`specBridgeColumnSpelling` / `specBridgeExportFormats` render-integration
-suites. `ObjectGrid`'s own column-spelling, export-gate and density behavior
-keep their non-bridge pins (`columnDeclaredSpellingOnly`, `exportGate`,
-`exportServer`, `rowHeightOffSpecBoundary`, and core's
-`normalize-list-view` suite).
-
-BREAKING CHANGE: the public exports `SpecBridge`, `bridgeListView`,
-`bridgeFormView` and the types `BridgeContext` / `BridgeFn` / `ObjectDefLite`
-are removed from `@object-ui/react`. There is no replacement translation
-layer — author `object-grid` / `object-form` nodes directly (the live path is
-`app-shell`'s `ObjectView`, which builds them from the object's own metadata),
-exactly as the 17.0.0 entry already directed for pages and dashboards.
diff --git a/.changeset/retire-theme-component-schema-5489.md b/.changeset/retire-theme-component-schema-5489.md
deleted file mode 100644
index 121ce3a106..0000000000
--- a/.changeset/retire-theme-component-schema-5489.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer
-implemented (objectui#5489).
-
-`packages/types/src/theme.ts` declared a theme-manager **component** carrying
-`themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and
-`storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching
-Zod object as a member of `ThemeUnionSchema` and therefore of
-`AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no
-`ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`,
-and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS`
-(`packages/components/src/renderers/placeholders.tsx`), so it did not even
-resolve to a placeholder — a page declaring one got the registry's "Unknown
-component type" panel (OBJUI-001) instead of a theme manager. Declared-but-
-unenforced, removed under the maintainer ruling of 2026-08-21 on
-objectstack#10485 (option B).
-
-Removed from the published surface: the `ThemeComponentSchema` type
-(`@object-ui/types`), the `ThemeComponentSchema` Zod object
-(`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the
-`'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling
-`type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than
-accepted and then rendered as an error panel, which is pinned by a test.
-
-**The theme system is unchanged.** `Theme` (the spec's authoring theme
-document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine`
-(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and
-untouched — the same ruling retains them explicitly. Author a theme as a
-document handed to `ThemeProvider`; that path never went through the removed
-component kind.
diff --git a/.changeset/retire-theme-switcher-preview-5647.md b/.changeset/retire-theme-switcher-preview-5647.md
deleted file mode 100644
index 29633b09ba..0000000000
--- a/.changeset/retire-theme-switcher-preview-5647.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Retire `ThemeSwitcherSchema` (`type: 'theme-switcher'`) and
-`ThemePreviewSchema` (`type: 'theme-preview'`) — the two remaining theme
-component kinds, which no renderer implemented — together with
-`ThemeUnionSchema`, the union that after objectui#5489 held only these two
-members (objectui#5647).
-
-`packages/types/src/theme.ts` declared a theme-switcher control (`variant`,
-`showMode`, `showThemes`, `lightIcon`, `darkIcon`) and a theme-preview panel
-(`showColors`, `showTypography`, `showComponents`), and
-`packages/types/src/zod/theme.zod.ts` published the matching Zod objects as
-the two members of `ThemeUnionSchema` and therefore of `AnyComponentSchema`.
-Nothing rendered either: neither literal appears at any
-`ComponentRegistry.register(...)` / `registerLazy(...)` site in
-`packages/*/src` (202 registered keys enumerated; positive control on the same
-pipeline: `tooltip` → 1), nor in `PROTOCOL_COMPONENTS` /
-`PALETTE_PLACEHOLDER_BLOCKS`
-(`packages/components/src/renderers/placeholders.tsx`), and no fixture
-declares either kind (control: `"type": "form"` → 81) — so a page declaring
-one got the registry's "Unknown component type" panel (OBJUI-001), never a
-switcher or a preview. Declared-but-unenforced, removed under the 2026-08-21
-maintainer ruling (option B) on objectstack#10485, extended to these siblings
-by inheritance on identical evidence (objectui#5647).
-
-Removed from the published surface: the `ThemeSwitcherSchema` /
-`ThemePreviewSchema` types (`@object-ui/types`), the matching Zod objects and
-`ThemeUnionSchema` (`@object-ui/types/zod`), and the `ThemeSwitcherSchemaType`
-/ `ThemePreviewSchemaType` inference aliases. `zod/theme.zod.ts` now exports
-nothing and stands as the tombstone module. A schema spelling
-`type: 'theme-switcher'` or `type: 'theme-preview'` is now REFUSED by
-`AnyComponentSchema.safeParse` rather than accepted and then rendered as an
-error panel, which is pinned by a test.
-
-**The theme system is unchanged.** `Theme` (the theme document vocabulary,
-owned by `@object-ui/types` since objectui#5716), `ThemeEngine`
-(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are retained and
-untouched — the rulings retain them explicitly. Author a theme as a document
-handed to `ThemeProvider`; that path never went through the removed component
-kinds.
diff --git a/.changeset/retire-usebranding-hook-5368.md b/.changeset/retire-usebranding-hook-5368.md
deleted file mode 100644
index bf5f6e24d2..0000000000
--- a/.changeset/retire-usebranding-hook-5368.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Delete `apps/console/src/hooks/useBranding.ts`, a `@deprecated` wrapper with zero callers.
-
-Nothing published changes. The console's npm tarball ships only `dist`, `plugin.*` and
-`README.md` (`files` in `apps/console/package.json`) — `src` is never in it — and the
-package's single `exports` entry resolves to `plugin.js`, which is compiled from
-`plugin.ts` alone and imports nothing from `src`. With no importer anywhere in the repo,
-the hook was also absent from the built SPA bundle. Empty frontmatter is therefore the
-accurate declaration: a source deletion under a released package that releases nothing.
diff --git a/.changeset/retired-field-type-gate-4914.md b/.changeset/retired-field-type-gate-4914.md
deleted file mode 100644
index ca91ef84fd..0000000000
--- a/.changeset/retired-field-type-gate-4914.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'@object-ui/core': patch
-'@object-ui/fields': patch
-'@object-ui/components': patch
-'@object-ui/plugin-detail': patch
-'@object-ui/plugin-view': patch
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-list': patch
----
-
-A RETIRED field-type spelling is now refused — out loud, once — by every
-field-type predicate in the renderer, not just by the widget road
-(objectui#4914, maintainer ruling B of 2026-08-18).
-
-`@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs
-ahead of six predicate faces that previously granted a retired spelling
-first-class treatment: the filter builder's operator buckets and its value
-control (`@object-ui/components`), the detail page's highlight-strip picker
-(`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`),
-the dashboard's `$expand` whitelist and `isLookupType`
-(`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter
-control (`@object-ui/plugin-list`). Each one now fires the migration
-prescription on the console — once per spelling across all of them, never once
-per predicate — and then answers as it would for a spelling it does not
-recognise.
-
-This closes the whole CLASS rather than one word: the gate is quantified over
-`RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the
-day it lands. It is the shape objectui#4932 and objectui#4942 already
-established for the form and inline-edit roads.
-
-Measured before the change, and the reason the fix is a gate rather than a
-deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')`
-equalled the `user` bucket item for item, `computeLookupExpand` actively
-requested `$expand` for it, `isLookupType('owner')` was `true` alongside
-`reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as
-`picklist` does. Deleting the members alone would have traded a visible
-contradiction for a SILENT degradation — a filter picker collapsing to a bare id
-box, `$expand` quietly stopping so cells show raw foreign-key ids — which is
-verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent.
-The gate keeps that fallback and adds the half that was missing: the author is
-told.
-
-The boundary question is answered on record: `owner` arriving through a
-backend-vocabulary normalizer is an authoring error to refuse loudly, not
-legitimate foreign input to tolerate. The open backend vocabulary those
-normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`,
-`datetime_tz` and the rest are equally absent from the spec's closed `FieldType`
-and are equally unretired, so they classify exactly as before.
-
-`RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports`
-move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that
-package's published surface is unchanged apart from the newly ruled gate.
-`@object-ui/components` is a consumer of the gate and `@object-ui/fields`
-depends on it, so a single shared table could not live in `fields` — and a
-second copy would have meant a second dedupe set and two console lines for one
-spelling. No package gained a new dependency.
-
-A retired spelling never loses a stored value: `retypeFilterValue` is
-deliberately not gated, and the refused filter row stays operable rather than
-drawing a blank operator trigger.
diff --git a/.changeset/retired-lucide-spellings-5622.md b/.changeset/retired-lucide-spellings-5622.md
deleted file mode 100644
index aa246fa963..0000000000
--- a/.changeset/retired-lucide-spellings-5622.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/plugin-detail': patch
-'@object-ui/components': patch
-'@object-ui/plugin-list': patch
----
-
-Repair five retired lucide icon spellings that reach a record-reading resolver, and pin
-the names against the runtime `icons` record so the next lucide bump goes red instead of
-silently blanking a glyph (objectui#5622).
-
-lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it
-as a deprecated named export. A retired name therefore still imports, still type-checks,
-and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it
-is used as a STRING, because every string lookup here reads that record. Nothing goes red
-either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at
-implementation time.
-
-What a user sees change:
-
-- `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon
- again. Its items become an `action:bar` schema whose renderers resolve `icon` through
- `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been
- drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged.
-- The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in
- both the registration `icon` and the `name` input's `defaultValue`) resolves again: the
- designer palette entry's glyph was blank, and an `icon` dropped from that palette
- rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette
- looks exactly as it did.
-- `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn`
- (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The
- gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher
- landed in objectui#5586, so one view type no longer draws two different icons depending
- on which switcher is on screen.
-
-Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and
-alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather
-than resolvability, because every retired spelling repaired here is the SAME component
-object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`,
-`Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`,
-`XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the
-export, would pass on the broken name. That is the blindness that let this ship.
diff --git a/.changeset/richtext-cell-renderer-5452.md b/.changeset/richtext-cell-renderer-5452.md
deleted file mode 100644
index 8b1448c2dc..0000000000
--- a/.changeset/richtext-cell-renderer-5452.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-"@object-ui/fields": patch
----
-
-A populated `richtext` field no longer renders as a blank cell (objectui#5452).
-
-`richtext` stores HTML — the spec documents the type as "Formatted content with
-HTML/WYSIWYG", the showcase seed's own specimen is `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/rotten-pugs-invent.md b/.changeset/rotten-pugs-invent.md
deleted file mode 100644
index 752b948778..0000000000
--- a/.changeset/rotten-pugs-invent.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Page-block canvas: draw the real icon and colour tone for block types that render fine but are not offered in the palette.
-
-The canvas read its per-node icon and tone from `BLOCK_TYPE_META`, which is the palette's *offer* list ("what may an author drag in") — while the canvas is answering a different question ("what may an author already have in this page"). The two diverge for an alias pair: `record:discussion` and `record:chatter` are one renderer under two names, so exactly one spelling is always unoffered, and a page carrying it showed a plain unknown-block box and neutral grey instead of the message icon and the blue record tone its twin gets.
-
-`block-types.ts` now declares `BLOCK_RENDERER_ALIAS_GROUPS` — spellings that resolve to the same registered renderer — and the canvas resolves display chrome through `resolveBlockDisplayMeta()`, which falls back to an alias sibling's entry. Keyed on renderer identity, not on "is excluded": palette exclusions that genuinely are not page blocks (`element:text_input`, `element:record_picker`, `element:form`, `ai:chat_window`) keep the generic box, and nothing became draggable — the palette offer list is unchanged.
diff --git a/.changeset/row-predicate-phase2-record-only-5741.md b/.changeset/row-predicate-phase2-record-only-5741.md
deleted file mode 100644
index 2ce7fefa07..0000000000
--- a/.changeset/row-predicate-phase2-record-only-5741.md
+++ /dev/null
@@ -1,60 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/react': minor
----
-
-row predicates on runtime record surfaces resolve `record.*` only; the bare-field and `data.*` spellings are no longer bound
-
-Phase 2 of the row-predicate canon (objectui#5330, ruled 2026-08-20, option B;
-Phase 2 ruled 2026-09-02 and amended 2026-09-05 on objectui#5741). Until now a
-row predicate — `visible` / `disabled` / `enabled` on an action renderer, a row
-action, a `record:alert`, a `page:header` action, a conditional-formatting
-`condition` — bound the row three ways: canonical `record.status`, bare
-`status`, and `data.status`. The two non-canonical spellings are retired on
-every runtime record surface, in both evaluation tiers (`evalRowPredicate` /
-`partitionRowsByPredicate` in `@object-ui/core`; `usePredicateRecordContext` +
-`useCondition` in `@object-ui/react`) and for both dialects: a legacy
-`${data.x}` / `${x}` string on a row surface retires with the CEL spellings.
-
-**What a retired spelling does now: it faults, exactly as it already did on the
-server** (`buildScope({ record })` mounts exactly `['record']`, so `status` and
-`data` are unknown variables there), and each surface applies its EXISTING
-fault policy — no runtime detector, no "treat as absent" special case, no
-uniform override:
-
-- `evalRowPredicate` / `partitionRowsByPredicate` (row kebab, selection bar,
- `page:header` actions, conditional formatting): the caller's `fallback` —
- hidden / every row excluded / no style — reported once by the existing fault
- warning, which names the unknown variable (`Unknown variable: status`) and,
- on the fast route, carries the `record.` hint.
-- `useCondition` legs that opt into `throwOnError` (`action:button` and
- `action:menu` `visible`, `DeclaredActionsBar` `visible`): fail-closed —
- hidden on every row, reported once as `was hidden/disabled: its predicate
- threw — status is not defined`.
-- the non-throwing `useCondition` legs (`action:icon` / `action:group`
- `visible`, every `disabled` / `enabled`, `record:alert`): fail-soft — shown /
- greyed / enabled on every row, with the evaluator's own console line.
-- a host scope that carries its OWN `data` (app-shell's ambient `data: {}`) is
- left standing: `data.*` on a record surface then reads the host's object — a
- constant, silent `false` — which is what "no longer bound to the row" means.
-
-The Phase-1 deprecation warning is removed with the bindings:
-`warnNonCanonicalRowSpelling` and `resetRowPredicateCanonWarnings` are no
-longer exported from `@object-ui/core`. `detectNonCanonicalRowSpelling`,
-`ROW_PREDICATE_CANONICAL_ROOT` and the `NonCanonicalRowSpelling` type stay
-exported — the offline instrument for sweeping authored metadata.
-
-The layer rule is unchanged: `data` remains the canonical root on
-metadata-editing surfaces (ADR-0089 D3, `CANONICAL_ROOT_BY_LAYER`), and
-app-shell's metadata-admin `SchemaForm` / `predicate.ts` keep binding
-`{ data: row }` through their own evaluator.
-
-No stored-metadata survey, export or migration rewrite was run (the maintainer
-ruled the stored population out of scope, 「不考虑存量」); the Phase-1 warning
-period was the notice.
-
-Release note: Phase 1 (PR #5737 — the canon statement plus the warning) shipped
-in `@object-ui/core@17.6.0` (npm, 2026-08-24) although its changeset
-`.changeset/row-predicate-record-canon-5330.md` is still pending on `main`, so
-the next CHANGELOG section lists Phase 1 and this Phase 2 together: the warning
-it describes was live from 17.6.0 and is gone from this release on.
diff --git a/.changeset/row-predicate-record-canon-5330.md b/.changeset/row-predicate-record-canon-5330.md
deleted file mode 100644
index cfd76abde8..0000000000
--- a/.changeset/row-predicate-record-canon-5330.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-'@object-ui/core': minor
-'@object-ui/react': minor
----
-
-Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now
-warn once, and are unchanged otherwise.
-
-A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row
-scope, a `record:alert`) has bound the row three ways since objectui#4075 —
-`record.status`, bare `status`, and `data.status` — without any of them being
-declared the contract. The maintainer ruled that question on 2026-08-20
-(objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for
-the identical renderer-tolerance shape: **the canon is `record.*`**, and the
-other two enter a deprecation window.
-
-The canon states the **server's** accept set, which was this card's first
-measurement and turns out to be strictly narrower than the renderer's. Measured
-against `@objectstack/formula@17.1.0`, the engine the server evaluates with:
-
-| spelling | server runtime | server authoring oracle |
-|---|---|---|
-| `record.status` | `{ ok: true, value: true }` | accepted |
-| bare `status` | `Unknown variable: status` | refused |
-| `data.status` | `Unknown variable: data` | **silently accepted** |
-
-`buildScope({ record })` mounts exactly `['record']` — `data` is never bound and
-the row's fields are never flattened to top level. The three-way binding is a
-client tolerance with no server counterpart, which is why the warning belongs on
-this side.
-
-`data.*` is the dangerous one, and the reason the warning exists. `data` is in
-`@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle
-waves it through — that list is a deliberately generous "never faults" lint
-baseline, not the runtime accept set. A `data.*` row predicate therefore passes
-every authoring gate the platform has and then binds nothing at runtime: not an
-error, a constant `false`. A `visible` that is constantly false is a button that
-silently never appears — the objectui#4075 fail-closed signature.
-
-What ships:
-
-- `@object-ui/core` exports `detectNonCanonicalRowSpelling`,
- `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and
- `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`,
- which carries the canon statement and the measurement.
-- Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and
- `useCondition` (react, for bags bound by `usePredicateRecordContext`).
-- Detection reuses the server's own oracles (`collectCelRootIdentifiers`,
- `firstUndeclaredReference`) rather than a regex, so no second dialect
- judgement is invented client-side.
-
-**No spelling is removed and no behaviour changes.** Every predicate that
-resolved before resolves now — the ruling defers removal behind a stored-metadata
-survey, and the warning is what makes that survey possible (ADR-0078: a
-tolerance nothing ever reports can never be retired).
-
-The deprecation is scoped to the **runtime record layer**. `data` remains the
-canonical root one layer over, in a metadata-editing form (ADR-0089 D3
-`CANONICAL_ROOT_BY_LAYER`), and the detector stands down there.
diff --git a/.changeset/rowcolor-prototype-guard-6295.md b/.changeset/rowcolor-prototype-guard-6295.md
deleted file mode 100644
index de76be8132..0000000000
--- a/.changeset/rowcolor-prototype-guard-6295.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@object-ui/plugin-grid': patch
----
-
-Guard `useRowColor`'s two object-literal lookups with `Object.prototype.hasOwnProperty.call`.
-
-Both `config.colors` (the authored map) and the module's `COLOR_TO_CLASS` literal inherit
-`Object.prototype`, and both were reached with a bare index. A record whose colour field
-held `constructor`, `toString`, `valueOf` or `hasOwnProperty` resolved to an inherited
-function: the `if (!color)` guard passed it (functions are truthy) and `colorToClass` then
-called `.startsWith` on it, throwing a `TypeError` inside the row-className resolver during
-render — a grid crash triggered by record data rather than by metadata. The same shape one
-call deeper in `colorToClass` did not throw; it handed an `Object.prototype` member back as
-the row's `className`, which reached React as a class attribute. Both now resolve to
-`undefined`, as an undeclared value always did.
diff --git a/.changeset/runtime-config-server-side-pointer.md b/.changeset/runtime-config-server-side-pointer.md
deleted file mode 100644
index 835e851994..0000000000
--- a/.changeset/runtime-config-server-side-pointer.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Comment-only change to `app-shell`'s `runtime-config.ts`: the "Server-side" pointer block
-now anchors on exported symbol + package (`RuntimeConfigPlugin` from
-`@objectstack/cloud-connection`, `RuntimeConfigPlugin` from `@objectstack/objectos-runtime`,
-`createStudioRuntimeConfigPlugin` from `@objectstack/service-cloud`) instead of file paths,
-two of which had gone stale. No published behaviour changes.
diff --git a/.changeset/runtime-served-error-reporting-dsn.md b/.changeset/runtime-served-error-reporting-dsn.md
deleted file mode 100644
index 8ffc86943a..0000000000
--- a/.changeset/runtime-served-error-reporting-dsn.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/console': minor
----
-
-Console error reporting is now configured entirely by the **server**: the DSN and every
-knob that travels with it arrive on `GET /api/v1/runtime/config`, and the build-time
-`VITE_SENTRY_DSN` path is retired (objectui#5522, consumer half of objectstack#12681).
-
-ObjectStack's users consume a **prebuilt** Console. Under the previous two-key gate —
-a build-time DSN **and** a runtime permission — a build-time key was unreachable for
-them, so a self-hosting operator could not enable client error reporting at all: the
-permission was reachable and the source was not. The maintainer named it on 2026-08-27:
-
-> 「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」
-
-```
-send ⇔ the runtime served a DSN
-```
-
-**The DSN's presence IS the grant.** There is no companion permission flag, and the one
-that briefly existed is removed rather than paralleled. Two knobs in two places produced
-two silent dead states — "permission on, no DSN" and "DSN in, permission off" — that look
-identical from the browser; one knob cannot disagree with itself. Turning reporting off is
-unsetting the server DSN, and there is deliberately no build-time force-off left, because
-nobody consuming a prebuilt console could reach one.
-
-The fail-closed posture is unchanged and structurally stronger. Absence of a *source* is
-not a value that can be misread, so a runtime predating the key, a third-party host, a
-404, a network failure, a malformed body and a config that has not arrived yet all read as
-off — where the boolean needed a strict `=== true` plus a written argument about why a
-negative `disabled` flag would have been vacuous on exactly the runtimes that were leaking.
-
-### What moved, and the one thing that did not
-
-`sendDefaultPii`, `environment`, `tracesSampleRate` and the error-session replay rate move
-into the runtime payload. They were build-time variables, so a prebuilt-console consumer
-could set none of them — including the one deciding whether IP and User-Agent leave their
-network. This is not new surface; it is the same surface moved to the side that can
-operate it.
-
-`VITE_SENTRY_RELEASE` **stays build-time**, and is now the only `VITE_SENTRY_*` variable
-that exists. A release identifies which bundle produced a stack trace and must match the
-source maps that bundle's pipeline uploaded — a property of the build, which no server can
-know. `VITE_SENTRY_ENABLED`, `VITE_SENTRY_ENVIRONMENT`, `VITE_SENTRY_TRACES_SAMPLE_RATE`
-and `VITE_SENTRY_REPLAY` are retired along with `VITE_SENTRY_DSN`.
-
-### Breaking
-
-| FROM | TO |
-|:--|:--|
-| `VITE_SENTRY_DSN=…` in the Console build environment | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=…` on the ObjectStack runtime |
-| `VITE_SENTRY_SEND_DEFAULT_PII=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true` |
-| `VITE_SENTRY_ENVIRONMENT=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT=…` |
-| `VITE_SENTRY_TRACES_SAMPLE_RATE=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE=…` |
-| `VITE_SENTRY_REPLAY=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1` |
-| `VITE_SENTRY_ENABLED=false` | unset the runtime DSN |
-| `isClientErrorReportingAllowed(): boolean` | `getClientErrorReporting(): RuntimeClientErrorReporting \| null` |
-| `resolveSentryGate(env, runtimeAllows)` | `resolveSentryGate(runtimeErrorReporting)` |
-| `RuntimeTelemetry.allowClientErrorReporting: boolean` | `RuntimeTelemetry.errorReporting?: RuntimeClientErrorReporting` |
-
-One-line fix for a deployment: move your `VITE_SENTRY_*` values onto the ObjectStack
-server as the `OS_TELEMETRY_CLIENT_ERROR_REPORTING_*` variables above, and drop them from
-the Console build environment. One-line fix for a consumer of `@object-ui/app-shell`:
-`const sink = getClientErrorReporting(); if (sink) …` in place of
-`if (buildTimeDsn && isClientErrorReportingAllowed())` — the build-time conjunct is gone,
-because the server now supplies the source.
-
-**Landing order is safe in both directions.** A Console built before this change meets a
-new server, reads an absent `allowClientErrorReporting` and stays off; a Console built
-after it meets an old server, reads an absent DSN and stays off. Neither half can turn
-reporting on by itself, so the two repos' PRs can land in any order.
-
-The `committed-telemetry-endpoint.test.ts` ratchet is unchanged in rules and unchanged in
-job: nothing endpoint-shaped may be committed to this repo. Its rules key on the
-variable's suffix and on the value rather than on the `VITE_` prefix, so they already
-cover the runtime-side spelling — now pinned, so a later tidy-up cannot narrow them to the
-retired names and reopen the hole under a new one.
diff --git a/.changeset/safe-field-label-identity-5564.md b/.changeset/safe-field-label-identity-5564.md
deleted file mode 100644
index b7ae0e40f1..0000000000
--- a/.changeset/safe-field-label-identity-5564.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-`useObjectLabel` now keeps a stable identity when no i18next instance is bound,
-so the memoization it advertises holds on the no-provider path too
-(objectui#5564).
-
-react-i18next's `useTranslation` builds its return value out of a fresh `{}` on
-every render when it has nothing to bind to (`const finalI18n = i18n || {}`,
-which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived
-with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`,
-so the memo never held: measured 4 distinct returned objects across 4 renders
-with no instance, against 1 with one. That is the wrong way round — the memo
-exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in
-heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case
-as the one it exists to serve.
-
-Both memo dependencies are now pinned to module-level constants while no
-instance is bound. The substitution is unobservable rather than merely
-convenient: every `t()` call in the module sits inside a
-`for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]`
-under exactly the same "is there a usable instance" predicate — so while the
-substitution is in effect, the closures cannot read either value. When an
-instance appears the dependencies become the live values again, so a provider
-mounting after first render recomputes the object exactly once and resolves
-real translations from then on.
-
-No API change: no new exports, no signature changes, and the returned surface is
-identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside
-`useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the
-memo's own docstring names.
diff --git a/.changeset/scatter-unplaceable-points.md b/.changeset/scatter-unplaceable-points.md
deleted file mode 100644
index aa619cb442..0000000000
--- a/.changeset/scatter-unplaceable-points.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-Scatter now says when it cannot place a row, instead of drawing an empty axis.
-
-Scatter is the only two-measure positional chart in the renderer: `xAxisKey` feeds
-a numeric X axis and `series[0]` a numeric Y axis, so a point exists only when
-both are numbers. Measured in real Chromium, rows it could not place produced a
-tile byte-identical to a scatter handed no rows at all, and six different
-authoring failures shared one image. A chart with one placeable row among three
-was 99.75% pixel-identical to a genuinely one-row scatter.
-
-Handed rows it cannot place any of, a scatter now renders the file's refusal
-shell under `data-chart-error="no-plottable-points"`, naming both keys. When some
-rows place and some do not it draws as before with a `data-chart-note="unplotted-points"`
-footnote carrying the count. Charts whose rows all place are byte-identical to
-before, and no wrapper element is added to them.
-
-The predicate is positional, not magnitude-based: zero and negative coordinates
-are ordinary scatter data and keep drawing.
diff --git a/.changeset/schema-input-bridge-permanent-4622.md b/.changeset/schema-input-bridge-permanent-4622.md
deleted file mode 100644
index 4b78051281..0000000000
--- a/.changeset/schema-input-bridge-permanent-4622.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-'@object-ui/react': patch
----
-
-`toRenderableSchema`'s header now says the bridge is permanent, instead of instructing
-callers to remove it (objectui#4622).
-
-No executable line changes — but the artifact is **not** unchanged, and that is worth
-stating plainly rather than rounding to "comment-only". This package builds with plain
-`tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc
-is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both
-what an editor shows on hover at every call site and bytes that ship.
-
-Measured by building the package the way the repo builds it, at both revisions:
-`dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850
-to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19
-differing lines in the emitted file are JSDoc continuations and the three executable lines
-are byte-identical, so the growth is the paragraph and nothing else. The trade is
-deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old
-paragraph's instruction produced once already.
-
-The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have
-not been reconciled" and that "when it lands, the call sites using this can go back to
-forwarding directly". Both halves went false when PR #4608 merged, and the second half is
-the harmful one: it is an instruction whose trigger condition has now fired, sitting
-directly above the function a future author is about to call.
-
-The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of
-`@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring
-an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548
-ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So
-a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a
-permanent crossing between two intentionally different types rather than scaffolding
-awaiting a merge.
-
-Following the old instruction has a measured cost: five `apps/site` call sites were
-forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly
-five hours until PR #4621 routed all five through this function (objectui#4617).
diff --git a/.changeset/sdui-parser-inline-whitespace-5661.md b/.changeset/sdui-parser-inline-whitespace-5661.md
deleted file mode 100644
index 7affbea3dc..0000000000
--- a/.changeset/sdui-parser-inline-whitespace-5661.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-'@object-ui/sdui-parser': patch
----
-
-`kind:'html'` page sources keep the space between a text run and an adjacent
-inline element: `A x page` now compiles to `A `/``/`
-page` and renders as `A x page` rather than `Axpage` (objectui#5661).
-
-The parser collapsed each text run's whitespace to a single space — correct, and
-what HTML itself does — and then `.trim()`ed it, which is not: HTML collapses a
-whitespace run to one space, it does not delete it. The deleted space was
-precisely the separator between a run and its inline sibling, so every authored
-sentence carrying emphasis or a link in the tier the guide recommends by default
-rendered with its words run together. It was silent: the page rendered, the
-structure was right, no diagnostic fired.
-
-The rule is deliberately mechanical rather than a block/inline taxonomy invented
-for a schema tree that has none: keep one leading space when a sibling precedes
-the run, and one trailing space when a sibling element follows it. The parent's
-own start and end still drop their edge space, so ` hi
` is unchanged.
-
-Its one bounded cost: a whitespace-only run BETWEEN two siblings survives as a
-single space, so a pretty-printed `` gains one `' '` string child per gap
-between its ``s — one space per gap, never the source's newline and
-indentation, never at the container's own edges, and never inside an item's own
-text. That bound is pinned by a test rather than left as a claim.
diff --git a/.changeset/sdui-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md
deleted file mode 100644
index f14b09da71..0000000000
--- a/.changeset/sdui-preview-page-source-tailwind-5470.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
----
-
-Publishes nothing — declared with an empty frontmatter rather than left undeclared.
-
-The changed files are `apps/console`'s three ADR-0080 browser preview harnesses
-(`src/sdui-*-preview.tsx`) plus a test. They are dev-server-only: `apps/console`'s
-vite config declares no `build.rollupOptions.input`, so the build's only entry is
-`index.html` (resolved config, measured: `input` unset) — the three
-`sdui-*-preview.html` entries and the modules behind them never enter `dist/`.
-`@object-ui/console`'s `files` omits `src`, and its `exports` map has exactly one
-entry (`.` → `./plugin.js`, built from `tsconfig.plugin.json` including
-`plugin.ts` alone, which imports nothing from `src/`). Nothing here reaches a
-consumer.
-
-Behind the change: page `source` is runtime metadata, and the console's Tailwind
-is compiled at build time by scanning the console's own `src` with no safelist, so
-a utility class authored in real page metadata produces no CSS and no error
-(ADR-0065; ADR-0080's 2026-06-30 amendment). `sdui-tiers-preview.tsx` — the
-harness making an explicit authoring claim — now styles with each tier's real
-primitive (the html tier with ``'s structured props plus JSON `style`
-objects, the react tier with inline `style` objects, colours as
-`hsl(var(--token))`), so it demonstrates what authors are told to write and now
-follows the theme in light and dark. The two renderer-plumbing harnesses keep
-their Tailwind and declare the exception in their headers; a test pins both
-halves against the shipped `page-source-className-tailwind` rule.
diff --git a/.changeset/secret-widget-spellings-5375.md b/.changeset/secret-widget-spellings-5375.md
deleted file mode 100644
index 2f405f2118..0000000000
--- a/.changeset/secret-widget-spellings-5375.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/components': minor
-'@object-ui/core': minor
----
-
-Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch.
-
-Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path
-(no `registerAllFields()`), before and after objectui#5322's fix:
-
-```
-type registry hit rendered type
-ui:password true text
-secret false text
-field:secret false text
-```
-
-Two halves, per the maintainer ruling of 2026-08-20:
-
-- **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring
- ERROR.** A form field's widget id (`widget`, else `type`) may name the
- `field:` namespace or a bare name; any other namespace resolves no field
- widget (objectui#5254) and used to degrade silently to a plain text box.
- `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and
- `assertValidSchema` throws. Behaviour change: a schema that previously
- validated with e.g. `type: 'ui:password'` is now invalid — inventing a
- plausible-looking widget id fails loudly instead of rendering clear text.
- `field:` ids stay valid whether or not the widget is registered, since
- registration is a runtime fact an authoring-time validator cannot see.
-- **`@object-ui/components` — the known secret types cover the remaining
- spellings.** Bare `secret` and `ui:password` render the native masked input,
- and `field:secret` is refused outright like `field:password`. Existing authors
- need no migration.
-
-`ui:password` **is** registered — as an SDUI node renderer for a top-level
-`{ type: 'email' }`-style node — so an author who checked whether it resolved
-got a yes and still got a clear-text box on the field path. No producer emits
-any of the three; all are reachable only through a hand-authored standalone
-form schema, which is exactly the surface where the author is the producer and
-no normalizer sits in between.
diff --git a/.changeset/self-import-gate-scan-once-5402.md b/.changeset/self-import-gate-scan-once-5402.md
deleted file mode 100644
index 32ece0aee5..0000000000
--- a/.changeset/self-import-gate-scan-once-5402.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
----
-
-CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared.
-
-`scripts/check-package-self-import.mjs` now separates the SCAN from the JUDGEMENT:
-`scanRepository(root)` performs the expensive TypeScript parse once, and `judgeScan(scan,
-exemptions)` is a pure filter over its result. `analyze()` keeps its signature and its
-behaviour exactly (differentially verified against the previous implementation over both a
-fixture tree and this repository, across all six exemption-table shapes), and the CLI's
-output is byte-identical.
-
-The reason is `scripts/__tests__/check-package-self-import.test.ts`, which asserts this
-repository is green under the repository's own exemption table AND under no exemptions at
-all. Those were two calls to `analyze(repoRoot)` — the same full parse of ~3,100 files and
-~30 MB of source performed twice — and one of them sat inside a 15-second `it()`. That
-fits uninstrumented (measured 8.8 s) and does not fit under v8 coverage (measured 41.3 s),
-so `ci.yml`'s `Test (coverage)` job failed 100% of the time from 2026-08-16 — 51 completed
-jobs, 0 successes, 50 of them this one file, every one `Test timed out in 15000ms` — and
-Codecov received nothing for four days (objectui#5402).
-
-It is a constant factor, not a race. `@vitest/coverage-v8` arms
-`Profiler.startPreciseCoverage({ callCount, detailed })` in the worker BEFORE test modules
-and their dependencies compile; V8 emits those block counters at compile time and does so
-isolate-wide, so `node_modules/typescript` is instrumented too — `coverage.exclude` filters
-the report, never the instrumentation. Measured here, the identical parse costs 4.1-4.9 s
-uninstrumented and 32-35 s when coverage was armed first, and starting coverage AFTER the
-same code is compiled costs nothing at all.
-
-The test file now scans once at module scope and judges it per assertion: the timeout-prone
-test drops from 41,268 ms to 1 ms under coverage, and the file's total work halves. No
-timeout was raised, nothing is skipped, and coverage is not disabled for anything.
-
-No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is
-nothing here for a consumer to upgrade to.
diff --git a/.changeset/sentry-undisableable-telemetry-5522.md b/.changeset/sentry-undisableable-telemetry-5522.md
deleted file mode 100644
index 11941d4db1..0000000000
--- a/.changeset/sentry-undisableable-telemetry-5522.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
-'@object-ui/console': patch
----
-
-Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in
-(objectui#5522).
-
-`@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from
-`apps/console/.env.production` — is what the hosted SaaS console and the on-premises /
-air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the
-bundle as a frozen object literal, so the DSN committed there was a live third-party
-telemetry endpoint compiled into artifacts that land inside customer networks. It could
-not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off
-that same frozen literal, so on a shipped bundle it is `undefined` forever and editing
-env vars on the deployed host does nothing. An air-gapped deployment was measured
-sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by
-the customer.
-
-- `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`,
- `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never
- imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched.
-- `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so
- IP address and User-Agent are never the inherited default of a build that did not ask
- for them.
-- The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not
- send. The direction is deliberately inverted from the usual — an unreported error is
- recoverable, PII leaving an air-gapped deployment is not.
-
-**Action required for deployments that want error reporting** (the hosted SaaS/demo
-console): inject `VITE_SENTRY_DSN` from your build environment, the same way
-`VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you
-still want IP/User-Agent on events. Nothing else changes for builds that opt in.
diff --git a/.changeset/settled-schema-resolution-hook-6482.md b/.changeset/settled-schema-resolution-hook-6482.md
deleted file mode 100644
index 7f3f4345fe..0000000000
--- a/.changeset/settled-schema-resolution-hook-6482.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/react': minor
----
-
-New export: `useSettledSchema` — the settled-schema RESOLUTION half shared by
-`ObjectKanban` / `ObjectView` / `ObjectCalendar`'s fetch-gate hand copies
-(objectui#6482, maintainer ruling Option A). It tracks whether an object's
-definition has finished resolving FOR THE KEY THE CURRENT RENDER IS ASKING
-ABOUT, returning `{ ready, def }` from one piece of internal state so `ready`
-and `def` can never be observed inconsistently and a stale key can never read
-as ready — the structural fix for the `ObjectTree` defect (objectui#6481)
-where a definition and a separate, one-way-latched "settled" boolean could
-disagree for a render after the object changed.
-
-Gate PLACEMENT — which effect branch actually waits on `ready` — stays a
-per-component decision and is not part of this hook; see the hook's own doc
-comment. Existing hand copies are migrated on their own subsequent cards, not
-by this change.
diff --git a/.changeset/shard-coverage-job-5403.md b/.changeset/shard-coverage-job-5403.md
deleted file mode 100644
index 8e30f8848f..0000000000
--- a/.changeset/shard-coverage-job-5403.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-CI only — this publishes nothing, declared explicitly with an empty frontmatter rather
-than left undeclared. `ci.yml`'s coverage lane is sharded 4 ways with a blob-report
-merge, and the Codecov upload can no longer go missing in silence: the merge job states
-on every path whether Codecov received a report for the commit, and is red when it did
-not.
diff --git a/.changeset/shared-zod-wrapper-keys-6923.md b/.changeset/shared-zod-wrapper-keys-6923.md
deleted file mode 100644
index 317c2a9eff..0000000000
--- a/.changeset/shared-zod-wrapper-keys-6923.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Internal only — no user-visible change, nothing to release.
-
-objectui#6923: the Zod wrapper-key list that five test/gate sites each spelled
-out by hand now lives once, in `packages/test-support` (a `private: true`,
-never-published package), and is read by both the TypeScript suites and the
-`.mjs` CI gates. Only test files, CI gate scripts and the private
-`test-support` package change; no released package's runtime code is touched.
diff --git a/.changeset/signout-purge-per-key-try-5777.md b/.changeset/signout-purge-per-key-try-5777.md
deleted file mode 100644
index 8b5252c493..0000000000
--- a/.changeset/signout-purge-per-key-try-5777.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-`purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's
-`objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a
-single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777).
-
-The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n`
-aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed —
-`AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are
-the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198
-classifies a surviving entry as a cross-principal disclosure on a shared browser, not
-mere staleness — so a partial sweep here is the sharper half of the same defect class
-objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`).
-
-`Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on
-its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is
-not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()`
-(objectui#5731) quarantines a key: this function does not own reads for the metadata
-seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to
-guard and nothing to quarantine — adding read-back verification would be a general
-storage-error-handling refactor of the module, out of this card's scope. What is
-mirrored is the reporting channel: a key whose `removeItem` throws is named in a
-`console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is
-discoverable instead of silent.
-
-Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata
-key, asserting every other metadata key on both sides of it is still swept and unrelated
-non-matching keys are untouched, plus a control that the warning fires only on an actual
-failure.
diff --git a/.changeset/simple-form-consults-declared-submit-handler.md b/.changeset/simple-form-consults-declared-submit-handler.md
deleted file mode 100644
index 74dfa9e5ef..0000000000
--- a/.changeset/simple-form-consults-declared-submit-handler.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-"@object-ui/plugin-form": patch
----
-
-`SimpleObjectForm`: consult a declared `submitHandler` before the inline-fields carve-out
-
-`ObjectFormSchema.submitHandler` is documented as handing the collected values to the host INSTEAD of calling `dataSource.create` / `dataSource.update`, so a form that declares it has a submit target with or without an adapter. `SimpleObjectForm.handleSubmit` nevertheless opened with the inline-fields carve-out (`hasInlineFields && !dataSource`), which returned before the persistence chain: a host that had declared it owns the write was never asked, and `onSuccess` confirmed a write that never happened (measured `onSuccess 1 / submitHandler 0`).
-
-The carve-out now fires only when no `submitHandler` is declared, and the "no submit target" refusal moved into the persistence chain after the seam — the shape the five variant renderers already use, reusing their shared refusal from `submitTarget.ts` rather than a private copy. A form with inline fields and no seam is unchanged: its `onSuccess` is still the write.
diff --git a/.changeset/six-donkeys-shake.md b/.changeset/six-donkeys-shake.md
deleted file mode 100644
index 3a674dcb50..0000000000
--- a/.changeset/six-donkeys-shake.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change to three `apps/console` test files: the `vi.mock('@object-ui/app-shell', …)`
-factories no longer call `importOriginal()`, which was transforming the whole source-aliased
-barrel graph (measured 10019 ms per file) to reach a handful of real exports. They now spread
-only the app-shell submodules those exports live in. No published behaviour changes.
diff --git a/.changeset/skill-provider-envelope-teaching-5372.md b/.changeset/skill-provider-envelope-teaching-5372.md
deleted file mode 100644
index 4e6dd9e2a7..0000000000
--- a/.changeset/skill-provider-envelope-teaching-5372.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
----
-
-Published-skill teaching only — this publishes nothing, declared explicitly with an
-empty frontmatter rather than left undeclared. No package `src/` is touched: the
-change is confined to `skills/objectui/**` (the published skill package) plus one new
-test under `packages/components/src/__tests__/`, which pins the corrected teaching to
-the real renderer.
-
-The rules told authors that the `properties` / `props` envelope belonged to the
-`element:*` namespace and that every other key "lives on the node". Measured on a real
-`SchemaRenderer` inside a `SchemaRendererProvider`, `properties` is evaluated and then
-hoisted onto the node in *every* namespace — so it was the only spelling that reached a
-`data-table`'s rows from a provider `dataSource`, while the node-level and `props`
-spellings rendered a header over the empty state with nothing thrown and nothing logged.
-The guides now record that measurement instead of contradicting it.
diff --git a/.changeset/sparse-predicate-warning-cause-5399.md b/.changeset/sparse-predicate-warning-cause-5399.md
deleted file mode 100644
index adebbd2ba1..0000000000
--- a/.changeset/sparse-predicate-warning-cause-5399.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/components': patch
----
-
-The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it
-states what it actually measured (objectui#5399).
-
-When an action's `visible` predicate references a `record.` the bound payload
-does not carry, the warner names the missing key and then explained the cause:
-
-> Hidden (hidden: true) fields are stripped from detail payloads server-side, so a
-> predicate gating on one may evaluate to a hide-by-default verdict.
-
-That cause is false, and it names a mechanism this repo does not own. `hidden` is a
-UI concern — the framework spec describes it as "Hidden from default UI"
-(`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against
-the framework checkout rather than taken on trust: ObjectQL's own strip for the
-`__search` companion documents that the `hidden` / `readonly` / `system` markers are
-"None of them is a PROJECTION rule", which is precisely why a dedicated strip rule
-had to be written for that one column; drivers answer a query with no `fields` using
-`SELECT *`; and `metadata-protocol` enumerates what the read path does drop —
-`internal: true` columns and the `__search` companion, and nothing else. The only two
-read-side uses of `field.hidden` in the framework are auto-view/auto-form column
-generation and companion-source eligibility, neither of which removes a key from a
-record body.
-
-So an author who read this diagnostic went hunting for a `hidden` flag they would
-either not find, or find on a field the payload demonstrably still returns — while
-the real source of the sparseness (a projected or partial read) went unexamined. A
-confidently wrong cause in a diagnostic is worse than no cause, because it is
-actionable in the wrong direction.
-
-The replacement states the fact this surface can actually see and the consequence it
-does own: the page bound a payload without those keys, a projected or partial read
-will not carry them, and the predicate therefore fails closed and hides the action.
-The measured half of the message — action name, missing fields, predicate source —
-is unchanged, and nothing about what triggers the warning changed.
-
-Message text only. The same false claim also sat in this warner's own doc comments
-and in the comments of the test that pins the message; both are corrected here, and
-the docstring now carries an explicit note against re-attributing the cause to
-`hidden: true`. No other call site was swept.
diff --git a/.changeset/spec-17-3-0-lockfile-bump.md b/.changeset/spec-17-3-0-lockfile-bump.md
deleted file mode 100644
index 167ba120b8..0000000000
--- a/.changeset/spec-17-3-0-lockfile-bump.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Dev-time only: move the `@objectstack/spec` resolution in `pnpm-lock.yaml` from
-17.2.0 to 17.3.0. No published package changes — every manifest's declared range
-(`^17.0.0` / `^17.1.0` / `^17.2.0`) already admitted 17.3.0, so no floor moved and
-no package's dependency declaration differs by a byte. Empty frontmatter is the
-deliberate "no release" declaration for a change that publishes nothing.
diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md
deleted file mode 100644
index 261be17ee2..0000000000
--- a/.changeset/spec-pin-17-1-0-5328.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@object-ui/data-objectstack': minor
-'@object-ui/plugin-list': minor
-'@object-ui/plugin-view': minor
-'@object-ui/core': minor
-'@object-ui/app-shell': minor
----
-
-Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves.
-
-The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history.
-
-**A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause.
-
-The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating.
-
-`@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately.
-
-**A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved.
-
-**The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes.
-
-**The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes.
diff --git a/.changeset/spec-refresh-17-2-0-5668.md b/.changeset/spec-refresh-17-2-0-5668.md
deleted file mode 100644
index 541883958e..0000000000
--- a/.changeset/spec-refresh-17-2-0-5668.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'@object-ui/console': patch
----
-
-Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains.
-
-**The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`.
-
-The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists.
diff --git a/.changeset/spellgantt-revoked-proxy-exclusion.md b/.changeset/spellgantt-revoked-proxy-exclusion.md
deleted file mode 100644
index 164a41f979..0000000000
--- a/.changeset/spellgantt-revoked-proxy-exclusion.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
----
-
-Docs and tests only for `@object-ui/plugin-timeline`. `spellGanttDateValue`'s
-docblock claimed every branch was total; measured in-render, `Array.isArray`
-throws on a revoked `Proxy`, so the claim is now scoped to the input set that
-is actually exercised and the exclusion is pinned as rows in the existing
-adversarial set. The same measurement falsified the wider claim that
-`Array.isArray` is the last non-total operation on the gantt date path — five
-reads upstream of the helper throw first, recorded in objectui#7153. No
-published behaviour changes.
diff --git a/.changeset/spotty-pages-drop-leaks.md b/.changeset/spotty-pages-drop-leaks.md
deleted file mode 100644
index 4bd3fc7907..0000000000
--- a/.changeset/spotty-pages-drop-leaks.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@object-ui/components': patch
----
-
-fix(components): the `page` wrapper now filters its DOM attributes through the shared whitelist instead of a hand-maintained list
-
-`PageRenderer` stripped PageSchema's descriptor keys with a hand-maintained
-destructure list and spread the remainder onto its wrapper ``. Whenever
-that list fell behind the schema, an authored key was not dropped — it was
-forwarded, and React stringifies unknown attributes in silence, so an authored
-`actions: [{…}, {…}]` reached the DOM as `actions="[object Object],[object
-Object]"`.
-
-The renderer now calls `toDomProps` from `@object-ui/core` — the same
-whitelist every converged SDUI widget already uses (objectui#4425). Twenty-five
-attributes measured leaking off the wrapper stop being emitted, including the
-`context` bag the console injects into every page it renders. Everything the
-wrapper legitimately carries is unchanged: `class`, `style`, `id`, `role`,
-`tabindex`, `data-page-type`, `data-obj-id`, `data-obj-type`, and the open
-`data-*` / `aria-*` families.
-
-This changes no schema's accept/reject behaviour — `BaseSchema.passthrough()`
-is untouched — and adds no read point for any previously leaking key.
diff --git a/.changeset/spotty-pianos-shave.md b/.changeset/spotty-pianos-shave.md
deleted file mode 100644
index a32380b233..0000000000
--- a/.changeset/spotty-pianos-shave.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: removed two `eslint-disable-next-line react-hooks/static-components` directives that the rule no longer needs in the record-picker i18n test files. No published behaviour changes.
diff --git a/.changeset/spotty-pins-cover-fields.md b/.changeset/spotty-pins-cover-fields.md
deleted file mode 100644
index f05f863e4b..0000000000
--- a/.changeset/spotty-pins-cover-fields.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
----
-
-`scripts/__tests__/plugin-published-stylesheet.test.ts` now runs its per-package
-assertions over `@object-ui/fields` as well as the two plugin sheets. Test-only:
-no published package's source changes, so this declares "no release" explicitly
-rather than bumping anything.
-
-Fields is the package whose stylesheet has actually been shipping to consumers
-since objectui#4059 — the defect the test's own header cites as the reason the
-shape exists — and it was the one supplement sheet the suite did not cover. Its
-`dist/index.css` was guarded only by the four write-time assertions inside the
-shared builder, which run during a build; CI runs this suite on an unbuilt
-worktree, so nothing in the test run inspected it. It could not be a subject
-before objectui#6405 re-pointed it at the shared
-`createPluginStylesheetBuilder`, because it ran its own copy and exported no
-module surface.
-
-The `CARD_THEMED` entry for fields is derived from the sheet the build actually
-emits — the surviving classes whose declarations resolve a `--color-*` token
-from components' unpublished `@theme` block — rather than read back off fields'
-own `MUST_SURVIVE`, which would have pinned nothing.
diff --git a/.changeset/stale-4163-pointers-5591.md b/.changeset/stale-4163-pointers-5591.md
deleted file mode 100644
index 7b77930b57..0000000000
--- a/.changeset/stale-4163-pointers-5591.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/i18n': patch
----
-
-`setLocalized`'s published docblock states the single-locale write rule that is
-actually in force, instead of deferring the multi-locale-authoring question to a
-closed card (objectui#5591).
-
-The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163
-closed as completed on 2026-08-15 with that product question still unanswered, so the
-parenthetical pointed at nothing — and it read as though the question had been settled
-somewhere a reader could go and check. This is the failure mode objectui#5428
-demonstrated is not harmless: there, a dangling deferral of exactly this shape let an
-expired justification sit unread for a release cycle at two surfaces.
-
-The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in
-force (`setLocalized` reaches only the entry for the locale the author is in), keep the
-open product question open **in place**, and record why there is deliberately no tracker
-reference — so the next reader cannot restore one. Re-pointing is how the class
-regenerates, because the next card closes too. The same wording form already landed in
-`plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`.
-
-Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with
-`pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`.
-
-Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per
-package rather than assumed, and the two packages this change touches differ:
-
-- `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches
- the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is
- `composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count:
- `dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js`
- `06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so
- it publishes something.
-- `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between
- declarations and a test docblock, neither attached to an exported declaration.
- `dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild
- (`93252e8cdf5a6faa…` both sides). The only artifact that moved is
- `dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above
- the declarations; no declaration text changed. Nothing user-visible publishes from that
- package, so it is not named here.
diff --git a/.changeset/stale-disable-directives-4853.md b/.changeset/stale-disable-directives-4853.md
deleted file mode 100644
index 5cb6141c27..0000000000
--- a/.changeset/stale-disable-directives-4853.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Deleted two dead `eslint-disable` directives, each naming a rule that is not
-configured for the file it sits in. Comment-only: no runtime source, no test
-assertion and no published contract changes, so this releases nothing.
diff --git a/.changeset/standard-widgets-phone-coverage.md b/.changeset/standard-widgets-phone-coverage.md
deleted file mode 100644
index b6f027dcab..0000000000
--- a/.changeset/standard-widgets-phone-coverage.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only change to `@object-ui/fields`: adds the missing `PhoneField` cases to
-`standard-widgets.test.tsx` and documents where `DateField` and `TextAreaField`
-coverage already lives. No published behaviour changes.
diff --git a/.changeset/standing-pending-drafts-bar-5694.md b/.changeset/standing-pending-drafts-bar-5694.md
deleted file mode 100644
index f1d669f659..0000000000
--- a/.changeset/standing-pending-drafts-bar-5694.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-AI build surface gains a standing 「未发布改动」 bar (#5694): while the conversation's bound package has pending drafts, a bar floats above the composer — surviving scrolling — counting the unpublished changes and publishing them through the same governed `publish-drafts` route as the inline card button, with probe findings surfaced instead of a blind success toast. Renders nothing when the count is zero or the conversation is unbound.
diff --git a/.changeset/studio-jargon-and-tool-card-i18n.md b/.changeset/studio-jargon-and-tool-card-i18n.md
deleted file mode 100644
index c1fddafc88..0000000000
--- a/.changeset/studio-jargon-and-tool-card-i18n.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-'@object-ui/plugin-chatbot': minor
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-Studio workbench and AI tool cards speak the author's language (objectui#7254)
-
-- The Interfaces breadcrumb, canvas caption and navigation rail show the
- metadata label plus a translated kind; the internal `type · name` pair moves
- to the tooltip. An unlabelled nav leaf now falls back to its object name
- instead of rendering an empty row.
-- The Studio top-bar package switcher reads the package's human name from
- either position the packages endpoint serves it in, instead of degrading a
- registry-shaped entry to its reverse-domain id.
-- The dashboard property panel is localized: the spec's authoring form is
- overlaid through the platform's own `metadataForms.
` convention, so
- section headings, field labels, hints and the `header` composite's sub-fields
- render in Chinese (developer vocabulary such as "Tailwind units" is replaced
- with something an author can act on, not transliterated).
-- AI tool cards: tool titles resolve through `chatbot.tool.` (all thirty
- platform-provided tools, ten locale packs), the header status badge is
- localized, and the plan count strip is a real plural family instead of an
- English `+ "s"` concatenation.
-- The tool card's header badge and its body badge now come from one producer:
- a proposal that has been confirmed, built or published no longer keeps a
- header reading "Awaiting Approval".
diff --git a/.changeset/studio-metadata-form-ux-5416.md b/.changeset/studio-metadata-form-ux-5416.md
deleted file mode 100644
index 551450b9e7..0000000000
--- a/.changeset/studio-metadata-form-ux-5416.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button.
-
-Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on
-the first surfaces a new author sees.
-
-**Validation no longer runs on mount.** `新建软件包` opened with both required
-fields already red: the create draft is `{ version, type }`, so
-`ManifestSchema.safeParse` reported `name` and `id` missing on the very first
-render, and the dialog then jumped a line height per field as each error
-cleared. `SchemaForm` now defers the error *line* until the row has been
-touched — first focusout anywhere in it, or the field's own first edit — for
-create forms only. The rule itself did not move: `issues` still reaches the host
-unchanged, so the submit button is gated on exactly the same validation, and
-edit/view forms still report from mount, where the issues describe stored values
-rather than something half-typed.
-
-**The package form's help text is translated.** The labels came from the
-metadata-admin i18n bundle and the help line under each one came straight from
-`ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over
-"Human-readable package name". `getPackageForm` now reads each field's help
-through the same bundle as its label, via a new `tOptional` that returns
-`undefined` rather than echoing the key back. Only zh entries exist: an en-US
-console finds nothing and keeps falling through to the spec's own sentence, so
-the English keeps exactly one producer — `@objectstack/spec` in the framework
-repo — and this repo never holds a copy of it to drift.
-
-**The publish panel's primary button opens clear of the toast stack.** The
-console mounts its toaster bottom-right and `DraftChangesPanel` is a
-`side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way
-there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed
-out. Opening the panel now clears the stack the surface the author just left had
-raised. Repositioning the toaster was measured and rejected: the draft preview
-bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar`
-anchors bottom-centre and the nav rail owns the left edge, so a move only
-relocates the same collision onto a different primary control.
-
-Not fixed here, and not fixable here: the other strings the card names are
-produced outside this repo. `Owning Business Unit` (`packages/spec`),
-`Search Index` (`packages/objectql`) and the `Revise Window` flow node's name
-and description (`packages/plugins/plugin-approvals`) all come from the
-framework, which owns their translation catalogue; patching them in the console
-would create a second source of truth that diverges at the next framework
-release.
diff --git a/.changeset/studio-nav-canonical-keys.md b/.changeset/studio-nav-canonical-keys.md
deleted file mode 100644
index 73ce02c260..0000000000
--- a/.changeset/studio-nav-canonical-keys.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio Interfaces: the nav-leaf binding reads the canonical target key only
-
-`resolveSurface` fell back to the bare spellings `page` / `object` /
-`dashboard` / `report`, and carried a `case 'view'`. Every
-`NavigationItemSchema` member is a `strictObject`, none of those bare
-spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is
-not one of the union's nine members — so all of them are keys `AppSchema`
-answers with `unrecognized_keys`, and every one of those branches could only
-fire on an app that cannot be saved. The Studio nav item inspector's object
-picker likewise read `node.object ?? node.objectName`, preferring the rejected
-spelling over the canonical one; it now reads the canonical key first.
-
-No shape that parses today stops parsing: this narrows the designer back to
-what the contract already declares.
diff --git a/.changeset/studio-new-object-asks-for-owd-5418.md b/.changeset/studio-new-object-asks-for-owd-5418.md
deleted file mode 100644
index 05f2f1e0cc..0000000000
--- a/.changeset/studio-new-object-asks-for-owd-5418.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/i18n': minor
----
-
-Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it.
-
-Creating an object through Studio collected exactly two things — display name and
-identifier — and saved a draft that declared no `sharingModel`. The draft saved
-happily, the form designer worked, and the object was then refused at 发布 →
-全部发布 by `security-owd-unset`: a required decision the surface never asked
-for, delivered by failing, as English ADR prose in a toast that then vanished on
-a timer. The one actionable word in it named a control three clicks away that
-nothing routed to.
-
-The publish gate is correct and is unchanged — an org-wide default has to be an
-authored decision, not an accident. What changes is when the console asks and
-when it answers:
-
-- **The create dialog asks.** A third field collects the baseline, pre-selected
- to `private` and glossed with the Settings tab's own strings, so a new object
- is publishable by construction. `buildObjectSkeleton` now takes the value as a
- required parameter — a future create path cannot omit the baseline without
- failing to type-check. `controlled_by_parent` is deliberately not offered at
- creation: it derives access from a master relation a brand-new object does not
- have yet, so offering it would trade one publish refusal for another.
-- **The review sheet reports it.** The pending-changes panel now runs the
- framework's own `validateSecurityPosture` over the pending object drafts and
- names any blocking finding, with its fix-it hint, next to the Publish button.
- It mirrors the producer's rule rather than re-deriving it, and it reports
- without blocking — the server door stays the authority.
-- **The Settings tab stops calling an unset baseline safe.** It described unset
- as "defaults to Private", which answers what the runtime does and not whether
- the object can ship. It now reads as the publish-blocking problem it is,
- styled like the external-wider warning beside it.
diff --git a/.changeset/studio-route-entry-capability-5519.md b/.changeset/studio-route-entry-capability-5519.md
deleted file mode 100644
index bed8579047..0000000000
--- a/.changeset/studio-route-entry-capability-5519.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-'@object-ui/console': patch
----
-
-The Console now gates the `/studio/*` routes on the `studio.access` ENTRY
-capability, not just on the backend's refusal of the writes behind them
-(objectui#5519).
-
-`/_console/studio/` rendered the full Studio pillar builder — Data /
-Automations / Interfaces / Access, with Publish and Save draft — to any
-authenticated principal who typed the URL, on deployments where the Studio nav
-tile is deliberately absent and every metadata write is refused. A plain tenant
-user was walked through the entire "new package" form and only refused at
-submit (403). The lockdown criterion for that deployment shape is two-part — UI
-entry hidden AND API refused — and only the API half was met; what stood on
-this side was a write-level gate where an entry-level one belongs.
-
-The whole `/studio` subtree now hangs off one route element that reads
-`systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this
-app already consumes) and admits only a principal whose LOADED set carries
-`studio.access` — the capability declared as "Enter the Studio metadata-design
-surfaces", which a tenant org owner does not hold by design. Everyone else is
-sent to `/home` without the builder ever mounting.
-
-The fail direction is deliberately inverted from this app's other capability
-gates: those fail OPEN on an unknown answer because their bad outcome is a
-holder losing a button, whereas a route gate's bad outcome is a non-holder
-seeing the builder. So the loading window renders the console splash (never the
-builder), an outright fetch failure renders the retryable error splash, and a
-`200` that carries no `systemPermissions` at all is refused rather than waved
-through. The server-side refusals are untouched.
diff --git a/.changeset/studio-simplify-5813.md b/.changeset/studio-simplify-5813.md
deleted file mode 100644
index c5860b40c1..0000000000
--- a/.changeset/studio-simplify-5813.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-Studio simplification, first cut (#5813): the Data pillar's object sub-tabs collapse to 记录/表单 with the five power panels (验证/钩子/操作/API/设置) behind one 「高级」 menu (the open panel's name wears the active pill, so the collapsed default never hides where you are); drafts now AUTO-save (debounced 1.5s, CEL-blocking gates the timer per the objectui#4306 rule, a failed save waits for the next edit) and the four 保存草稿 buttons are retired in favor of a quiet saving/last-saved hint; the 权限 pillar moves from the top-level row into a 「更多」 overflow whose items carry the same dirty-guard as the primary pillars — the /studio/:pkg/access route and every page behind it are untouched.
diff --git a/.changeset/submithandler-variant-forms.md b/.changeset/submithandler-variant-forms.md
deleted file mode 100644
index 84007e9a29..0000000000
--- a/.changeset/submithandler-variant-forms.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/plugin-form": patch
----
-
-Honour the declared `submitHandler` seam in every form variant, not just the simple one.
-
-`ObjectFormSchema.submitHandler` is documented as the seam a host uses to own persistence: the form validates and hands the collected values over instead of calling `dataSource.create` / `dataSource.update`. `ObjectForm` forwarded the key into every variant it routes to, but only `SimpleObjectForm` read it — `TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` persisted directly.
-
-**Behaviour change on a persistence path.** A master-detail parent half rendered `tabbed` (or `split`) now commits through the atomic `batchTransaction` together with its child collections, instead of writing the parent independently through `dataSource.create`. Previously the child leg was never attempted on those layouts: the parent was committed alone, the entered line items were silently discarded, no compensation ran, and a success toast confirmed the save. A failing child leg now leaves no committed parent, on every layout that renders the parent half inline.
-
-`WizardForm` additionally skips its own default success toast / redirect arms when a `submitHandler` is present, matching `ObjectForm`, so a host that owns the write also owns the outcome.
-
-The `object-master-detail-form.formType` vocabulary is unchanged and stays `simple | tabbed`.
diff --git a/.changeset/surface-context-send-1610.md b/.changeset/surface-context-send-1610.md
deleted file mode 100644
index 0b92a22514..0000000000
--- a/.changeset/surface-context-send-1610.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/app-shell': minor
-'@object-ui/plugin-chatbot': patch
-'@object-ui/i18n': patch
----
-
-The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding.
diff --git a/.changeset/surface-deeplink-live-channel-5476.md b/.changeset/surface-deeplink-live-channel-5476.md
deleted file mode 100644
index 9515eb71b3..0000000000
--- a/.changeset/surface-deeplink-live-channel-5476.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio's pre-publish security block can now take you to the object it names.
-
-The pending-changes sheet reports what the publish door would refuse — as
-`object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under
-Settings → Record sharing". Naming it was the half that shipped; reaching it was not.
-The `?surface=:` deep-link that would have carried the author there
-captures the URL exactly ONCE, at mount, and the sheet is opened over an
-already-mounted pillar — so writing the param changed the URL and moved nothing.
-
-That mount-time capture is deliberate and stays exactly as it was: the mirror half
-rewrites the param on every in-pillar selection, so a capture that followed the URL
-would re-trigger its restore on each one. What was missing is a third half — a live
-target delivered BESIDE the URL, which is what a producer already inside the pillar
-needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity
-(`{type, name}`), the host routes cross-pillar requests back through the URL (that
-pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the
-author declines over unsaved edits, and the mounted pillar applies the rest.
-
-Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next
-rail reload would drag the author back off whatever they had since selected — the
-regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test`
-pins a hand-picked object surviving a package switch, and
-`surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it
-still ignores every URL change after mount, and a live request never moves it.
-
-The sheet's other home is the Home / draft-preview bar, where the Studio object editor
-is not a reachable destination at all. Reachability is answered structurally — the
-producer hook returns `null` when no host published the channel — so off-Studio the
-item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both
-directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment.
-
-Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its
-signature, its return and its behaviour, and only the Data pillar subscribes to the
-new channel. The channel is its own React-only module on purpose — importing the hook
-into the sheet would have pulled `nav-selection` and the App-nav inspector into the
-console's eager graph.
diff --git a/.changeset/sweep-store-per-key-try-5763.md b/.changeset/sweep-store-per-key-try-5763.md
deleted file mode 100644
index 615b71b68f..0000000000
--- a/.changeset/sweep-store-per-key-try-5763.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-`sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage`
-state on a change of session user (objectui#5664 part 3) — now costs one key when a
-single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763).
-
-The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key
-`n` aborted the walk, so keys `n+1..end` were never swept, and the failure was
-swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt`
-believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the
-next un-namespaced key — one nobody has written yet — cannot re-open the cross-user
-pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys
-survived depended on `Object.keys` iteration order rather than on anything bounded. The
-previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their
-permission-filtered app list, a cross-principal disclosure per objectui#5198) could all
-land on the wrong side of the abort.
-
-`Object.keys(store)` — the reason a guard exists here at all — stays guarded on its
-own; only the per-key guard is new, so one uncooperative key now costs exactly that key.
-
-Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is
-NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its
-one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a
-quarantine are what keep a failed `clear()` from handing the value straight back.
-`sweepStore` walks keys it does not own reads for — another package's recents cache, a
-metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding
-read-back verification for keys this function does not otherwise touch would be a
-general storage-error-handling refactor of the module, which this card is scoped away
-from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is
-named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is
-discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in
-path, inside an `AuthProvider` effect) still cannot act on the failure and must not
-throw either.
-
-A working `localStorage`/`sessionStorage` behaves exactly as before: every
-non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist
-(`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected.
diff --git a/.changeset/switch-org-declared-reresolve-5750.md b/.changeset/switch-org-declared-reresolve-5750.md
deleted file mode 100644
index 0a76461413..0000000000
--- a/.changeset/switch-org-declared-reresolve-5750.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-`switchOrganization` now re-resolves identity for the organization it just switched
-to by its own explicit decision, instead of depending on an accidental
-`TokenStorage` side effect to notice the switch (objectui#5750).
-
-`AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity
-re-resolved across a switch only because `POST /organization/set-active` happens to
-return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from
-the UNSIGNED `session.token` spelling `getSession()` normally stores — so
-`TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription
-calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That
-signed spelling is deterministic on the raw session token, not on the organization:
-two switches with no `get-session` landing in between produce the identical signed
-value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity
-is left answering for whichever organization the FIRST switch targeted even though
-`activeOrganization` already reads as the new one.
-
-Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage"
-link on an org card, plus its own "Back to organizations" button) — ordinary
-client-side navigation with no full-page reload and nothing debouncing repeat
-switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not
-reachable paths for this: both force `window.location.href` immediately after a
-successful switch, and the resulting fresh `AuthProvider` mount always performs an
-authoritative `loadSession()` regardless of how the race above resolved.
-
-`switchOrganization` now tracks the organization it last resolved to itself and
-re-resolves explicitly whenever a switch's target differs from that, while
-suppressing the (now redundant) rotation notification for its own `set-active`
-call — so the common single-switch path still spends exactly one `get-session`, not
-two. A generation guard discards a still-in-flight, now-superseded switch's answer
-rather than let it clobber a later switch's fresher one.
diff --git a/.changeset/switch-org-identity-reresolve-5719.md b/.changeset/switch-org-identity-reresolve-5719.md
deleted file mode 100644
index 48c7be0598..0000000000
--- a/.changeset/switch-org-identity-reresolve-5719.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
----
-
-Tests only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared. No package `src/` behaviour moves; the only files added are
-`packages/auth/src/__tests__/switchOrgIdentityReresolve-5719.test.tsx` and this changeset.
-
-Pins that console identity **re-resolves across an organization switch**, and pins the
-mechanism that actually provides it.
-
-objectui#5719 observed that `AuthProvider.switchOrganization` never calls `loadSession()`,
-while the framework derives `user.positions[]` inside `customSession` from
-`session.activeOrganizationId` — so leg 3 of `useWorkspaceAdminStatus` would keep answering
-for the PREVIOUS organization, and because that hook is `leg1 || leg2 || leg3` with
-`isResolved` short-circuited to true on any positive leg, a stale `org_owner` reads as a
-CONFIRMED admin in the new organization. The card was filed as an observation because its
-last step could not be settled from this repo.
-
-Measured against better-auth 1.6.28 driven standalone with the organization and bearer
-plugins the framework always enables: `POST /organization/set-active` **does** hand back a
-`set-auth-token`, so the objectui#4467 rotation subscription already re-resolves identity
-and there is no defect. The mechanism, however, is an accident worth naming: the switch does
-not mint a new session — `updateSession` writes only `activeOrganizationId` — it hands back
-the **signed** `token.signature` spelling, while `createAuthClient.getSession()` stores the
-**unsigned** `session.token`. Two spellings of one session, and `TokenStorage.set` notifies
-on a value change.
-
-Nothing declared that. Normalising either lane onto a single spelling — a reasonable-looking
-tidy-up — would silently stop identity re-resolving across an org switch, with the
-over-permissive consequence #5719 described and no test anywhere to catch it. These cases
-are that test, including the owner-of-both control (so the pin cannot pass on code that
-merely stopped reporting admin) and a counterfactual that fixes what the guarantee rests on.
-
-No explicit `refreshSession()` was added: the rotation path already spends exactly one
-`get-session` per switch, so an explicit call would make every org switch cost two.
diff --git a/.changeset/table-renderer-declared-column-contract-5350.md b/.changeset/table-renderer-declared-column-contract-5350.md
deleted file mode 100644
index a405857ba3..0000000000
--- a/.changeset/table-renderer-declared-column-contract-5350.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/components': minor
----
-
-The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling.
-
-`renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a
-cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared
-on `TableColumn`, which declares `header` and `accessorKey` — both required
-(`packages/types/src/data-display.ts`). This was the fourth site of the
-column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid`
-(objectui#5350).
-
-Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the
-family direction — *retire the consumer-side alias; unify the producers* — and it
-names this site: the declared `header`/`accessorKey` contract wins.
-
-What makes this site different from its three siblings is that the alias was not
-merely tolerated, it was **published**. `content/docs/api/schema-reference.md`
-§TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a
-property row reading *"Column definitions with `name`, `label`, …"*, while
-`packages/types` declared the opposite pair. Docs and type disagreed about one
-type they both call `TableColumn`, each internally consistent. Retiring the alias
-without correcting the page would have turned a documented, working example into a
-silently broken one, so both halves land together: the page now authors
-`accessorKey`/`header`. The same row also advertised a `render` property that
-`TableColumn` has never declared — the renderer's hook is `cell` — and that claim
-is dropped rather than re-spelled.
-
-The failure mode of a now-unresolvable column is worth stating, because it is
-quiet: the column keeps its slot and its neighbours are unaffected, the header or
-the cells simply render empty, and nothing throws. This renderer keys its cells by
-index rather than by accessor, so unlike `data-table` it does not even produce
-React's generic missing-key warning — a retired-spelling column is fully silent.
-Whether that silence should become an authoring diagnostic is objectui#5349's
-question; no diagnostic is added here.
-
-The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so
-re-introducing an undeclared alias on this renderer is now a type error rather
-than a reviewer's catch.
diff --git a/.changeset/tall-tools-shake.md b/.changeset/tall-tools-shake.md
deleted file mode 100644
index 7c4cfb5558..0000000000
--- a/.changeset/tall-tools-shake.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: drop eight unused schema imports from `packages/types/src/__tests__/phase2-schemas.test.ts`, so an import-based coverage census no longer reads them as pinned by that suite. No published behaviour changes.
diff --git a/.changeset/tenant-header-edge-contract-5279.md b/.changeset/tenant-header-edge-contract-5279.md
deleted file mode 100644
index 3ba15b9414..0000000000
--- a/.changeset/tenant-header-edge-contract-5279.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-'@object-ui/auth': patch
----
-
-Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the
-unstamped-first-request window in which it is not sent (objectui#5279). Documentation
-only — no behaviour changes.
-
-The header had no written contract anywhere, and the shape of the missing information was
-actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a
-search confined to this repo and the framework (`objectstack`) returns zero readers and
-reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was
-held until a cloud-side reading came back non-empty. Without the contract written down,
-the next person to grep reaches the same false conclusion and deletes a live routing
-input.
-
-`packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means
-(a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim,
-not an authorization input, not what scopes rows), who stamps it and under exactly which
-condition, who reads it, and what a reader may and may not assume. The framework half is
-stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the
-API-key principal or `session.activeOrganizationId` and from no header — alongside
-`plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability.
-The configuration half is quoted from the contract this package can actually resolve,
-`TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the
-default of a configurable `tenantHeaderName` and `header` ranks second of six
-identification sources behind `subdomain`.
-
-The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is
-filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` ->
-`getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at
-all. What a reader observes is documented as **absent, never present-and-empty**, with the
-five situations that open the window and the instruction to fall through to the next
-identification source rather than fail closed. The gap is recorded, deliberately not
-closed: the cloud readers observe today's behaviour, so changing when the header first
-appears is its own decision.
-
-Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes
-about the wire — no active organization means no header at all, the stamp is not gated on
-`/api/` the way `Authorization` is, and the active organization overwrites a caller-set
-`X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed.
diff --git a/.changeset/text-input-description-aria-describedby-5735.md b/.changeset/text-input-description-aria-describedby-5735.md
deleted file mode 100644
index af1dc6abe2..0000000000
--- a/.changeset/text-input-description-aria-describedby-5735.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`element:text_input` now ties its authored `description` to the field with
-`aria-describedby`, so assistive tech announces the helper text as the input's
-accessible description instead of leaving it as an unassociated paragraph
-beside the field (objectui#5735).
-
-Before this the paragraph and the input were siblings with no programmatic
-relationship: a screen reader moving to the field announced the label and the
-value and never the helper text. The `label` half of the same block was already
-wired (`htmlFor` against the input's `id`), which is what made the gap specific
-to `description` rather than a general absence of a11y wiring — and the
-identical key authored on a field INSIDE `renderers/form/form.tsx` has been
-announced all along, so one authoring key behaved two ways depending on which
-container the author reached for. It no longer does.
-
-The paragraph's id is minted per instance with `React.useId()` — the same source
-`FormItem` mints the form renderer's description id from — and deliberately not
-derived from `schema.id`. The two associations in this block need ids on
-opposite ends: `htmlFor` names the INPUT, whose id only the author can supply,
-so that wiring still holds only when they gave the node an `id`; `aria-describedby`
-names the PARAGRAPH, which the renderer owns, so the description association
-holds unconditionally and cannot collide when two nodes share an authored id.
-The attribute is emitted only when a paragraph is actually rendered — an absent
-or empty `description` leaves the input with no `aria-describedby` rather than a
-dangling reference.
-
-The key's published `ComponentInput` description, which documented this gap in
-so many words, is rewritten in the same change. Its closing advice — prefer
-`label` for an instruction a user must not miss — is kept rather than deleted,
-on a new basis: a description is announced after the field's name and screen
-readers gate description text behind verbosity settings a user can turn down, so
-it remains the half of the announcement most likely to go unheard.
diff --git a/.changeset/text-input-i18n-label-arms-5717.md b/.changeset/text-input-i18n-label-arms-5717.md
deleted file mode 100644
index f8cbd403a3..0000000000
--- a/.changeset/text-input-i18n-label-arms-5717.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/components': patch
----
-
-`element:text_input` now DECLARES the inline-translation arm on `label`,
-`placeholder` and `description`, so the manifest gate stops reporting
-`type-mismatch` on a locale map its own renderer has always resolved correctly
-(objectui#5717).
-
-`@objectstack/spec` types all three keys as the `I18nLabel` union
-(`string | Record`) — measured on the installed 17.1.0 pin, per
-key, from the schema's own verdicts — and `text-input.tsx` has resolved all
-three through `pickLocalized` at their read sites since it was written. Only the
-`ComponentMeta` entries stayed at a single `'string'` arm. Driven through the
-same `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the
-save gate use, an author writing `{ en: 'Owner', 'zh-CN': '负责人' }` got three
-warnings on a write that renders correctly in the viewer's language:
-
-```
- prop "label" expected a string
- prop "placeholder" expected a string
- prop "description" expected a string
-```
-
-That is the INVERSE of the direction the rest of this family moved in.
-`element:record_picker.emptyText` (objectui#5590) and that block's `label` /
-`placeholder` (objectui#5637) each held one arm for exactly as long as their
-render site dropped the map, and gained the object arm in the change that taught
-the render site to resolve it — never declare an arm the renderer drops. Here
-the render site was never behind, so the same rule's other half applies:
-withholding an arm the renderer resolves is the false declaration in the other
-direction, and noise on legal writes trains authors (AI authors included) to
-dismiss the `unknown-prop` and `type-mismatch` reports that are real.
-
-Per-key, not blanket. `defaultValue` on this same block keeps `['string',
-'number']` — its contract has no object arm, measured the same way — and the
-console specimen file keeps asserting that an `I18N_MAP` there still reports
-`type-mismatch`. That control is what makes this a widening of three keys rather
-than of a component.
-
-All three keys also gain a `description` written from what the renderer does
-rather than from restating the contract: where each one lands in the rendered
-output (a `` above the field, the native `placeholder` attribute inside
-it, a `` below it), when each is omitted, and the locale fallback chain the
-read site follows. No renderer behaviour changed.
diff --git a/.changeset/theme-clientvalidation-dead-entry-5715.md b/.changeset/theme-clientvalidation-dead-entry-5715.md
deleted file mode 100644
index 0f5648934d..0000000000
--- a/.changeset/theme-clientvalidation-dead-entry-5715.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Removed the dead `theme:` entry from `clientValidation.ts`'s `LOADERS` table, which
-read `ThemeSchema` off `@objectstack/spec/ui` — a symbol the spec retired upstream
-(objectstack#10485 / PR objectstack#10695, which deleted the whole `ui/theme.zod.ts`
-module). `theme` was never a registered metadata type, so metadata-admin never asked
-for it (objectui#5715).
-
-This is dead-code hygiene, not a bug fix, and it changes nothing an author can
-observe. Two measurements say why, and both cut against the more dramatic reading:
-
-- The entry is not broken *today*. objectui's own `@objectstack/spec` pin (17.1.0)
- still publishes `ThemeSchema` as a working strict Zod schema — measured by
- importing the `ui` subpath and by ablation, where restoring the entry made
- `validateMetadataDraft('theme', ...)` actively reject a draft. The skew is with
- objectstack's `main`, not with what objectui installs, which is exactly why no
- gate here ever went red.
-- It could never have crashed. `getSchemaForType` duck-checks the loader's result
- (`typeof schema.safeParse === 'function'`) and wraps the call in `try/catch`, so an
- `undefined` export degrades to a silent no-op validator, never a throw. After a
- future spec bump past the retirement that is the branch it would have taken.
-
-The one thing that was genuinely off: `hasClientValidator` answers from key presence
-alone, so it returned `true` for `theme` while the validator behind it would have
-judged nothing. Per its own docblock that combination makes `ResourceEditPage` treat
-live client issues as the error source and suppress the server's `_diagnostics` — a
-stored item rendering as clean. Unreachable in practice, but it is the reason the
-entry is worth removing rather than leaving inert.
-
-The line is replaced by an absence note in the shape the file already uses for
-`workflow` and `approval`, because re-adding this entry from the spec would
-type-check and go green against the current pin. A metadata-admin theme editing
-surface remains a separate capability decision.
diff --git a/.changeset/theme-schema-doc-rewrite-5648.md b/.changeset/theme-schema-doc-rewrite-5648.md
deleted file mode 100644
index 52c05730ce..0000000000
--- a/.changeset/theme-schema-doc-rewrite-5648.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Docs-only: rewrite `content/docs/core/theme-schema.mdx` around the retained theme
-system (`Theme` from `@object-ui/types`, `ThemeEngine`, `ThemeProvider`) and delete
-the page's three `check-doc-component-types.mjs` exemption entries (objectui#5648).
-No published package changes.
diff --git a/.changeset/theme-types-localized-5716.md b/.changeset/theme-types-localized-5716.md
deleted file mode 100644
index 85b3fede0e..0000000000
--- a/.changeset/theme-types-localized-5716.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/types': minor
-'@object-ui/providers': minor
----
-
-Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages.
-
-Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider):
-
-- `Typography` — the shape lives on as the inline `Theme['typography']` member.
-- `BorderRadius` — lives on as inline `Theme['borderRadius']`.
-- `Shadow` — lives on as inline `Theme['shadows']`.
-- `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`.
-
-Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`).
-
-The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace.
-
-`@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`).
diff --git a/.changeset/theme-zod-retired-spec-reexports.md b/.changeset/theme-zod-retired-spec-reexports.md
deleted file mode 100644
index 68023be60c..0000000000
--- a/.changeset/theme-zod-retired-spec-reexports.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@object-ui/types': minor
----
-
-Remove the six retired `@objectstack/spec/ui` theme-schema re-exports from `@object-ui/types/zod` — `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`, `ThemeDefinitionSchema` (the spec's `ThemeSchema`) — plus their `…SchemaType` inference helpers, and the `theme` / `mode` props of `ThemePreviewSchema` (zod and interface) that consumed them.
-
-objectstack#10485 (ADR-0049 enforce-or-remove, PR objectstack#10695) retired the spec's whole `ui/theme.zod.ts` module, and the maintainer's ruling on objectstack#10856 (Options A + C) has objectui remove the dangling imports first so the Console Pin Gate can build objectui against the framework tree; restoring the spec exports (Option B) was explicitly not taken. Breaking in effect for anyone importing those six names from `@object-ui/types/zod`: there is no replacement — the validators retired upstream with no successor. The theme TYPE surface (`Theme`, `ThemeMode`, `ColorPalette`, … re-exported from `@object-ui/types`) and the ThemeEngine/ThemeProvider runtime are unchanged.
diff --git a/.changeset/tidy-buttons-repeat.md b/.changeset/tidy-buttons-repeat.md
deleted file mode 100644
index 30b4700642..0000000000
--- a/.changeset/tidy-buttons-repeat.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-Studio package lookup now renders a producer-marked `error.userMessage` from the failure
-envelope, preferring it over the diagnostic `error.message`.
-
-`fetchFullPackage` — the `/api/v1/packages` read behind "Package info & settings" and the
-managed-snapshot refresh — read only `error.message` and `error.code`. In the 5xx band the
-platform withholds the producer's prose and substitutes the generic `Internal server error`
-into `message`, while the marked `userMessage` rides through untouched, so an author met a
-500/503 that carried a sentence written for them and was shown the generic one instead.
-Per the envelope contract, presence of `userMessage` IS the producer's marking and a
-consumer that sees it renders it verbatim; `error.code` still travels alongside, and an
-unmarked refusal is unchanged.
diff --git a/.changeset/tidy-donkeys-attack.md b/.changeset/tidy-donkeys-attack.md
deleted file mode 100644
index a7314fd9e4..0000000000
--- a/.changeset/tidy-donkeys-attack.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-"@object-ui/plugin-form": patch
----
-
-`navigateOnSuccess` now honours a mounted host, and says so when its destination is refused
-
-`ObjectForm` and `WizardForm` consume `navigateOnSuccess` through
-`resolveSuccessNavigate`, and both arms travelled to an accepted destination with a bare
-`window.location.assign`. A rooted path such as `/apps/x/o/record/{id}` assigned that way
-resolves against the ORIGIN root, so under a host mounted at a sub-path (the framework CLI
-configures one for every embedded deployment) an authored in-app destination left the
-application. Both arms now route an app-relative destination through the injected
-navigation seam both components already held for `submitBehavior.url`, so a mounted host's
-basename is applied. With no host seam the behaviour is byte-for-byte what it was — a host
-with no router has no basename, so origin-rooted resolution is already correct there. A
-same-origin ABSOLUTE destination also keeps browser-level navigation: the seam's declared
-input is an application-relative path, and an author who spelled out a whole address asked
-for that address.
-
-A declared `navigateOnSuccess` whose destination is refused — a mistyped value, or a written
-record carrying no usable id — used to produce a success toast identical to the one a form
-with no `navigateOnSuccess` produces, so the navigation failed with nobody told. That toast
-now carries a note that the declared navigation did not happen, and the template the author
-wrote is logged for them. The write genuinely succeeded, so this stays a success rather than
-becoming an error state.
-
-Which destinations are ACCEPTED is unchanged: the same-origin guard, the `{id}` /
-`{recordId}` dialect and the unescaped interpolation are the subject of an open contract
-question and are deliberately untouched here.
diff --git a/.changeset/tidy-donkeys-shave.md b/.changeset/tidy-donkeys-shave.md
deleted file mode 100644
index 28ff741df2..0000000000
--- a/.changeset/tidy-donkeys-shave.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-"@object-ui/components": patch
----
-
-`ui:breadcrumb` and `ui:command` now resolve a child item's authored `icon` to a glyph instead of drawing nothing.
-
-`BreadcrumbItem.icon` and `CommandItem.icon` are declared keys of the protocol — present in `@object-ui/types`, mirrored in the Zod schemas, and documented on both components' pages — and neither renderer referenced `icon` at all. The catalog fixture literally named `components-data-display-breadcrumb/with-icons` rendered no icons, and the nine names the two `command` fixtures declare all drew nothing. Both names are now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `action:*`, `ui:dropdown-menu` and `ui:context-menu` resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph.
-
-`breadcrumb` resolves once per item and draws the glyph above the page/link split, so the last crumb (rendered as the current page) carries it as well as the linked crumbs before it.
-
-Two fixtures declared retired lucide spellings that are absent from the runtime `icons` record and would therefore have drawn nothing: `components-data-display-breadcrumb/with-icons` declared `layout`, now `panels-top-left`, and `components-form-command/command-menu` declared `smile`, now `face-slightly-smiling` — in both cases the live key the retired export resolves to by identity.
-
-`ui:button-group` is NOT included: its fixtures author `icon` on `buttons[]`, but `ButtonGroupButton` declares no such key in `@object-ui/types` or its Zod mirror, so wiring it would first mean widening the published item contract. Routed for a decision rather than chosen (objectui#5931).
diff --git a/.changeset/tidy-forms-carry-spec-keys.md b/.changeset/tidy-forms-carry-spec-keys.md
deleted file mode 100644
index 5cb3c5052c..0000000000
--- a/.changeset/tidy-forms-carry-spec-keys.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/react': patch
----
-
-fix(react): stop the form-view bridge silently dropping 18 spec keys
-
-`spec-bridge/bridges/form-view.ts` promised (#2545) that "every serializable
-spec key is either mapped onto the `object-form` node or listed here with an
-explicit reason for being ignored". Measured against `@objectstack/spec` 17.2.0
-the promise was false for 18 keys, because the conformance test enforcing it ran
-its completeness loop over its own hand-written fixture rather than the
-contract's key set.
-
-Seventeen of them now reach the node, at the destinations the receiving layer
-already reads:
-
-- `FormViewSchema.buttons` / `.defaults` — `ObjectFormSchema` declares both and
- `ObjectForm` folds them at render (action-button visibility/labels, and
- create-mode initial values).
-- `FormSection.pane` — explicit split-pane placement; without it a spec-authored
- split form fell back to the positional rule, so reordering sections moved them
- across the divider.
-- `FormSection.visibleOn` — the deprecated spelling now folds onto `visibleWhen`,
- matching the contract's own parse-time normalisation and the field path.
-- Thirteen `FormFieldSchema` keys — `maxLength`, `minLength`, `min`, `max`,
- `precision`, `scale`, `multiple`, `immutable`, `span`, `language`, `keyField`,
- `disclosure`, `fields` — so authored constraints, composite config and field
- width survive the bridge instead of ending there.
-
-`publicPicker` is deliberately not carried and now says so: it is a server-side
-public-lookup authorization opt-in with no client destination.
-
-The conformance test's key set is now derived from the contract's own shape at
-all three levels, so a spec key that is neither mapped nor explained fails the
-suite by construction.
diff --git a/.changeset/tidy-forms-refuse-target-less-submit.md b/.changeset/tidy-forms-refuse-target-less-submit.md
deleted file mode 100644
index 2a4f20ce58..0000000000
--- a/.changeset/tidy-forms-refuse-target-less-submit.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-'@object-ui/plugin-form': patch
----
-
-Variant forms refuse a submit that has nowhere to go, instead of reporting success
-
-`TabbedForm`, `WizardForm`, `SplitForm`, `DrawerForm` and `ModalForm` each opened
-`handleSubmit` with `if (!dataSource) { await schema.onSuccess?.(data); return data; }`
-— a success signal emitted without consulting a declared `submitHandler` and without
-persisting anything. Through `MasterDetailForm`, whose parent schema declares both
-`submitHandler: submitViaBatch` and `onSuccess: handleSaved`, that produced a success
-toast and, in create mode, a form reset clearing values nobody wrote.
-
-All five now answer the question the same way `SimpleObjectForm` and the `object-form`
-element gate already do. A form has a submit target when it has a `dataSource` or a
-declared `submitHandler`; with neither, the one legitimate shape is inline fields —
-a non-empty `customFields`, or `sections` whose fields are all inline runtime
-`FormField` objects — whose `onSuccess` is the write. Anything else throws
-`DataSource is required for form submission (inline mode not configured)`, which
-reaches `schema.onError` and is rethrown. A declared `submitHandler` is consulted
-first, so a host that owns the write is never bypassed for want of an adapter it
-never needed.
diff --git a/.changeset/tidy-jars-confirm.md b/.changeset/tidy-jars-confirm.md
deleted file mode 100644
index d0e53b6461..0000000000
--- a/.changeset/tidy-jars-confirm.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Comment-only change in `@object-ui/core`: `ConfirmationHandler`'s declaration now
-names the out-of-runner producer of its `options` bag, so a runner-scoped liveness
-sweep no longer reads the parameter as declared-but-never-produced. No behaviour,
-no type surface and no published output changes.
diff --git a/.changeset/tidy-pans-repeat.md b/.changeset/tidy-pans-repeat.md
deleted file mode 100644
index 9ccb0c823b..0000000000
--- a/.changeset/tidy-pans-repeat.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Tests only (objectui#7234): pin that an object-declared `list_toolbar` action
-reaches the DOM from the served `/meta/object` payload, and that ADR-0066 D4's
-`requiredPermissions` capability gate — not the relay — is what hides a
-declared-but-ungranted action at every location. No runtime behaviour changes,
-so nothing is released.
diff --git a/.changeset/tidy-permissions-provider-ctx-identity.md b/.changeset/tidy-permissions-provider-ctx-identity.md
deleted file mode 100644
index 217495d986..0000000000
--- a/.changeset/tidy-permissions-provider-ctx-identity.md
+++ /dev/null
@@ -1,35 +0,0 @@
----
-'@object-ui/permissions': patch
----
-
-Both permission providers now build their context value where React cannot discard it
-
-`PermissionProvider` built its context value in a `useMemo` over four
-`useCallback`s, and `MePermissionsProvider` in a `useMemo` over six. Neither
-carries a semantic guarantee: React is permitted to discard the cache and
-recompute even when the dependency list compares equal, and every one of those
-factories builds a fresh object. A discard would therefore hand
-`PermCtx.Provider` a NEW context value with every permission it carries
-unchanged — which moves the key `usePermissions()` caches on, and re-runs the
-consumer chain that names it: `ListView`'s data-fetch effect (an extra
-`dataSource.find`), `DetailView`'s gatedSchema, `ObjectForm`, `ModalForm`,
-`ObjectGrid` and `RelatedList`.
-
-⚠️ This is **hardening, not a repair**. Nothing misbehaves today: on this
-repo's pinned React 19.2.8 the cache is not discarded spontaneously — 51
-re-renders with no provider, 51 with one and 42 under `StrictMode` each
-returned one identity — and there is no `Activity`/Offscreen subtree here,
-which is the documented case where React does throw memo caches away. What is
-removed is the dependency on React continuing not to exercise a licence it
-holds.
-
-Each cached member and each context value is now keyed on the identities of the
-inputs it is derived from, in a module-level `WeakMap` React has no say over —
-the same technique that made `usePermissions()`'s own return discard-proof one
-link down the chain. The dependency sets are unchanged, so nothing churns more
-often than it did, and a genuine permission change still publishes a new
-context value to every consumer. Two providers given the same inputs now share
-one context value, which is stricter than the per-instance memo it replaces.
-
-No published export changes, and the context carries exactly what it carried
-before.
diff --git a/.changeset/tidy-pugs-invent.md b/.changeset/tidy-pugs-invent.md
deleted file mode 100644
index c8c1fab44b..0000000000
--- a/.changeset/tidy-pugs-invent.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Pin the shared inbox feed's cadence (objectui#7249): one declared poll interval owned by the feed, however many consumers mount, plus the hidden-tab throttle and the return-to-tab refresh. Tests only — no runtime change, nothing to release.
diff --git a/.changeset/tidy-pumas-repeat.md b/.changeset/tidy-pumas-repeat.md
deleted file mode 100644
index 77aec4c8ba..0000000000
--- a/.changeset/tidy-pumas-repeat.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Catalog-scoped ratchet on deprecated component types (`div` / `span`) in
-`examples/schema-catalog`. Test-only: no published package's source changes, so
-this declares "no release" explicitly rather than bumping anything.
-`@object-ui/example-*` is in the changeset `ignore` list, and
-`scripts/check-changeset-presence.mjs` reports "No source of a released package
-changed in this range, so no changeset is owed" — this file is the declaration,
-not a version bump.
diff --git a/.changeset/timeline-shared-safe-field-label-5623.md b/.changeset/timeline-shared-safe-field-label-5623.md
deleted file mode 100644
index 58af0449fb..0000000000
--- a/.changeset/timeline-shared-safe-field-label-5623.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Internal dedup: `ObjectTimeline` now uses the shared `useSafeFieldLabel` from
-`@object-ui/react` instead of its own local `useSafeObjectLabel` wrapper /
-`OBJECT_LABEL_FALLBACK` (objectui#5623). No exported symbol changed and the
-timeline's `fieldOptionLabel` call site behaves identically on both the
-bound and unbound i18n paths — no published behaviour changes.
diff --git a/.changeset/toc-anchor-parity-cite-by-content.md b/.changeset/toc-anchor-parity-cite-by-content.md
deleted file mode 100644
index d275831347..0000000000
--- a/.changeset/toc-anchor-parity-cite-by-content.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only: `toc-anchor-parity` now cites its live instances by heading content instead of by line address (objectui#7913). No published behaviour changes.
diff --git a/.changeset/tool-title-i18n-channel.md b/.changeset/tool-title-i18n-channel.md
deleted file mode 100644
index 1b2b1d13af..0000000000
--- a/.changeset/tool-title-i18n-channel.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@object-ui/plugin-chatbot': patch
----
-
-工具卡片的名字终于有了 i18n 通道(此前中文界面里必然是英文)
-
-实测(cloud#1658,全中文环境):
-
-```
-统计一下每个阅读状态各有多少本书
- Describe object 已完成 执行过程 ← 工具名英文
- Visualize data 已完成 执行过程 ← 工具名英文
- 已统计完成,各阅读状态的书本数量如下:… ← 其余全中文
-```
-
-卡片上每一处都本地化了——状态、动作、回答——**唯独工具名不能**,因为
-`humanizeToolName` 是个纯英文构词器(`describe_object` → `Describe object`),
-名字从未经过翻译,任何语言包都够不着它。而"它现在在做什么"恰恰是用户最需要读懂的一步。
-
-现在它接受一个可选的 `translate`(形状即 `useSafeTranslate()`),按
-`chatbot.tool.` 查;查不到就回落到与今天完全一致的英文标题。
-
-**这一步只打通通道,不改变任何现有显示**:不传 translate 时行为逐字不变(测试的第一组
-就在钉这一点),语言包也还没有条目。后续两件事各自独立、可分别推进:
-把两个调用点接上 `useSafeTranslate()`;以及按需往语言包里补 `chatbot.tool.*`。
-先落通道是因为——在通道存在之前,翻译工作根本无处可放。
-
-回落刻意交给英文标题而非原始名:语言包缺条目时显示 `Describe object`(与今天相同),
-而不是 `describe_object`(比今天更差)。
diff --git a/.changeset/truthfulness-guard-import-set-7117.md b/.changeset/truthfulness-guard-import-set-7117.md
deleted file mode 100644
index 1b55eb01a8..0000000000
--- a/.changeset/truthfulness-guard-import-set-7117.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change (objectui#7117): `exclusion-reason-truthfulness.test.ts` now imports the six `views/*-renderer.tsx` leaves and `@object-ui/plugin-detail`, so the three `PALETTE_EXCLUSIONS` keys that have a renderer outside the old import set (`app:launcher`, `global:notifications`, `record:chatter`) are actually visible to the guard, and a new renderer leaf that is not imported reds the suite instead of shrinking its coverage silently. No published behaviour changes.
diff --git a/.changeset/ui-text-variant-align-enforce.md b/.changeset/ui-text-variant-align-enforce.md
deleted file mode 100644
index f736debef5..0000000000
--- a/.changeset/ui-text-variant-align-enforce.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-"@object-ui/components": patch
----
-
-`ui:text` now honours `TextSchema.variant` and `TextSchema.align`
-
-The nine published variants (`h1`-`h6`, `body`, `caption`, `overline`) each map to
-a distinct typographic class, and `h1`-`h6` render the heading element they name;
-the four published alignments (`left`, `center`, `right`, `justify`) reach the DOM
-as their Tailwind class. The registration's `inputs` list declares both keys, so
-the JSX-page prop whitelist stops reporting `unknown-prop` for them.
-
-The published enum is unchanged — the renderer catches up to it, and a spelling
-outside the enum is still refused by `safeValidateSchema` rather than quietly
-ignored. Behaviour note: a document that already authored `variant: 'h1'` and saw
-no heading will now see one. That is the declared behaviour arriving. A node that
-never authored `variant` renders exactly as before.
diff --git a/.changeset/unbundled-node-boundary-5384.md b/.changeset/unbundled-node-boundary-5384.md
deleted file mode 100644
index cefdbb0617..0000000000
--- a/.changeset/unbundled-node-boundary-5384.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
-'@object-ui/plugin-map': patch
-'@object-ui/app-shell': patch
----
-
-Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect.
-
-`@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module
-scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`;
-`@object-ui/app-shell` reaches the first of those through the static
-`@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has
-no loader for `.css` at all, so all three resolve and then die during evaluation:
-
-```
-TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css"
- for .../react-grid-layout/css/styles.css
-```
-
-Nothing about how these packages load has changed — every supported host bundles
-them (Vite, webpack, or Next with the package in `transpilePackages`), and that is
-still the only supported way to consume them. What changed is that the boundary is
-now written where a consumer meets it, instead of being learned from a red import.
-
-objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying
-plugin packages — permanently, over the three packages as a group — rather than
-moving the stylesheet imports out of module scope. No unbundled-Node consumer
-exists, and buying permanent machinery to close a capability gap nobody is pulling
-on was the trade the ruling declined. A real consumer request reopens it as a
-design question, not as a defect: the READMEs say so and name the issue.
diff --git a/.changeset/unified-pending-drafts-5801.md b/.changeset/unified-pending-drafts-5801.md
deleted file mode 100644
index 126a5442a5..0000000000
--- a/.changeset/unified-pending-drafts-5801.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-One pending-drafts source of truth (#5801): `usePendingDrafts` / `fetchPendingDrafts` replace the five hand-rolled `GET /api/v1/meta/_drafts` copies behind the home banner, Studio topbar 变更/待发布 buttons, chat pending-drafts bar, chat draft-card resolver, and the preview watermark bar. Every successful publish path (`usePublishAllDrafts`, Studio `doPublish`, the chat bar) now emits the assistant bus's metadata-refresh pulse, and a chat turn that staged or replayed drafts emits it on completion — so a publish or an agent-staged draft anywhere converges every surface at once, and the home banner no longer needs its post-publish `window.location.reload()`. An errored drafts read now uniformly reports UNKNOWN (null), never a fake zero.
diff --git a/.changeset/uniqueness-deprecation-jsdoc-4765.md b/.changeset/uniqueness-deprecation-jsdoc-4765.md
deleted file mode 100644
index 64fc315d67..0000000000
--- a/.changeset/uniqueness-deprecation-jsdoc-4765.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-'@object-ui/types': patch
----
-
-Corrects the `@deprecated` prescription on `UniquenessValidation` in
-`packages/types/src/data-protocol.ts`, which pointed authors at spellings the platform
-no longer accepts (objectui#4765).
-
-Comment-only — no runtime behaviour changes. `patch` rather than an empty frontmatter
-because the JSDoc sits on an **exported** declaration and therefore ships to consumers:
-measured with the package's own build (`tsc`, and `tsconfig.base.json` deliberately sets
-`removeComments: false`), `dist/data-protocol.d.ts` goes 40218 → 41781 bytes and the new
-prose is present in the emitted `.d.ts`. What a consumer reads on hover changes, so it
-is declared. The emitted `dist/data-protocol.js` is byte-identical (sha256
-`a3de34c5…`, 207 bytes both ways) — that file is a types-only module whose entire JS
-output is the license banner plus `export {}`, so a comment on an erased `interface`
-reaches the declaration file and nothing else.
-
-Two of the three spellings it prescribed were wrong, measured against the installed
-`@objectstack/spec@17.0.0` (the report was written against `17.0.0-rc.6`):
-
-- **`indexes[].partial`** was retired in spec 17.0.0 under ADR-0049. It is a tombstone
- (`z.never()`) that the parse rejects at any value, so "`partial` for a scoped
- constraint" named a key that cannot be declared. A predicated unique constraint is
- built at the database layer by a runtime migration issuing
- `CREATE UNIQUE INDEX … WHERE`; the prescription now says so.
-- **`{ fields, unique: true }`** on `ObjectSchema.indexes` is the deprecated positional
- spelling of `unique: 'global'` under ADR-0120 — lint `unique/unscoped-declared-index`
- warns in 17.x and protocol 18 rejects it. The prescription now states the scope:
- `unique: 'global' | 'organization'`.
-
-The measurement also refined the report, and the refinement is the reason the rewrite is
-not a uniform find-and-replace. The third spelling — **field-level** `unique: true` — is
-NOT deprecated. `unique` is scope vocabulary shared by two surfaces on which the same
-bare `true` means different things: at index level it stays verbatim (`isGlobalUnique`
-and `isOrganizationUnique` both return `false`), which is why it is the positional
-spelling of `'global'` and is being retired; at field level it is the positional spelling
-of `'organization'` and, in the spec's own words, "stays valid indefinitely … no trap".
-Rewriting both occurrences the same way would have replaced one piece of false guidance
-with another, so the comment now names the per-surface difference explicitly.
-
-The interface's own deprecation is untouched and remains correct: `ValidationRuleSchema`
-rejects `type: 'unique'` at the discriminator (accepted discriminants are `script`,
-`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`), so a rule in
-this shape cannot reach the server.
-
-The replacement closes with what would falsify it — `UniqueScopeSchema` and
-`IndexSchema` in `@objectstack/spec` — so the next reader checks the schemas rather than
-trusting the paragraph. This is the fourth piece of false guidance found in this
-campaign (strictness ledger finding 18), and prose that cannot be checked is how the
-first three survived.
diff --git a/.changeset/unknown-component-type-case-suggestion-5247.md b/.changeset/unknown-component-type-case-suggestion-5247.md
deleted file mode 100644
index 0bfd69d29f..0000000000
--- a/.changeset/unknown-component-type-case-suggestion-5247.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-"@object-ui/react": patch
-"@object-ui/cli": patch
----
-
-Name the case-only spelling when a component type misses the registry.
-
-Registry lookup is exactly case-sensitive, so a node typed `Page` misses a registered `page` and falls through to the OBJUI-001 "Unknown component type" panel. Because the mistake is usually uniform across a document, the symptom is not one broken widget — it is the whole page rendering as error panels, with nothing in the message pointing at the cause.
-
-Both surfaces that report the miss now name the spelling that would have resolved. `SchemaRenderer`'s panel reads `Unknown component type: Page — did you mean 'page'?`, and `objectui check` reports `Unknown schema type "Page" in — did you mean "page"?`. When no known type differs by case alone, neither says anything extra — `zzz` gains no bogus suggestion, and this is case matching, not an edit distance, so `pge` suggests nothing either.
-
-**Lookup itself does not change.** `Page` still misses, still fails, and still renders the panel; only the message teaches. Normalising the lookup was considered and rejected (objectui#5247, maintainer ruling 2026-08-19): it would make two spellings valid everywhere, permanently, and legalise the typo class (`PAGE`, `pAge`) along with the PascalCase convention.
-
-Each surface reads its candidates from the set it can actually trust — the renderer from the live `ComponentRegistry` (including pending lazy stubs), the CLI from the registration-derived `KNOWN_SCHEMA_TYPES` snapshot — so neither can suggest a type nothing registers.
diff --git a/.changeset/utilities-index-phantom-provider-5360.md b/.changeset/utilities-index-phantom-provider-5360.md
deleted file mode 100644
index 4c7a34666a..0000000000
--- a/.changeset/utilities-index-phantom-provider-5360.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
----
-
-Docs and gate ledger only — this publishes nothing, declared explicitly with an empty
-frontmatter rather than left undeclared.
-
-`content/docs/utilities/index.md`'s "Data Integration" section taught
-`import { ObjectStackProvider } from '@object-ui/data-objectstack'`, a React context
-provider on a package that is headless and exports no such thing. Compiled against the
-built `dist/index.d.ts` by the same harness `scripts/check-doc-snippet-types.mjs` uses,
-the block read
-`TS2724: '"@object-ui/data-objectstack"' has no exported member named 'ObjectStackProvider'`,
-so a reader who copied it did not get a runtime bug — they got a compile error. #4124 had
-already established the finding and PR #4129 fixed it on the sibling page
-`content/docs/utilities/data-objectstack.mdx`; the identical phantom survived one file
-over, in the utilities index, outside that card's file surface.
-
-The section now teaches the same shape PR #4129 established: `createObjectStackAdapter`
-returning a plain `DataSource`, injected at the renderer boundary through
-`@object-ui/react`'s `SchemaRendererProvider`. The block is also self-contained — it
-imports every name it uses and types its schema literal as the real `ObjectGridSchema` —
-so it compiles exactly as a reader who copies that one block experiences it.
-
-Because that was the page's only `ts`/`tsx` block and it now produces zero diagnostics,
-`content/docs/utilities/index.md` LEAVES `check-doc-snippet-types.mjs`'s `UNGATED_DOCS`
-ledger instead of getting a re-measured reason: the page is compiled by the gate from
-here on, and no entry on that ledger names a missing export any more.
diff --git a/.changeset/vacuous-dts-justification-5673.md b/.changeset/vacuous-dts-justification-5673.md
deleted file mode 100644
index 37fe29375b..0000000000
--- a/.changeset/vacuous-dts-justification-5673.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-'@object-ui/core': patch
----
-
-Withdraw the entry-surface justification on core's `SchemaNode` re-export, and put
-the gauge that can actually fail in its place (objectui#5673).
-
-`packages/core/src/types/index.ts` carried, as the justification for the #4580
-re-export convergence, *core's own entry surface is unchanged (`dist/index.d.ts` is
-byte-identical across the change — measured, both rounds)*. The reading was real and
-it certified nothing. `core/dist/index.d.ts` is emitted from a barrel that only
-FORWARDS the symbol, and forwarding never restates a shape — not `export *`, and not
-the `export type { … }` line that names this one. Only the module that DECLARES the
-symbol can move, so that file is byte-identical under any change to a re-exported
-declaration's shape, and it could not have failed for the change class it was quoted
-against.
-
-Measured for this change rather than argued. One optional key was injected into
-`BaseSchema` — the shape `SchemaNode` publishes — both packages were rebuilt from a
-cleared `dist/` and a cleared `tsconfig.tsbuildinfo`, then the probe was dropped and
-both rebuilt again:
-
-| emitted file | base | with probe | probe dropped |
-|---|---|---|---|
-| `@object-ui/types` `dist/base.d.ts` — declares the shape | `31b5a01d…` | `8487500e…` **moved** | `31b5a01d…` |
-| `@object-ui/core` `dist/types/index.d.ts` — forwards it | `0e64c8c6…` | `0e64c8c6…` | `0e64c8c6…` |
-| `@object-ui/core` `dist/index.d.ts` — entry barrel, names it | `5cca207a…` | `5cca207a…` | `5cca207a…` |
-
-The corrected block states that calibration as a recipe with its failure mode, so the
-next reader inherits a gauge that can be checked instead of a sentence that cannot.
-The `ComponentRendererProps` block below it already reached the right verdict, but
-gave a narrower reason for it — that core's entry is an `export *` barrel — which is
-not the mechanism, and is wrong for a symbol the barrel names on its
-`export type { … }` line; it now states the forwarding rule.
-
-Documentation only, in a published declaration file: these docblocks sit on export
-specifiers, so `core/dist/types/index.d.ts` carries them into the tarball, while
-`core/dist/index.d.ts` does not move for them either — the same insensitivity,
-demonstrated once more on this very change. No type moves and no runtime behaviour
-changes.
diff --git a/.changeset/value-datasource-ast-filter-vocabulary.md b/.changeset/value-datasource-ast-filter-vocabulary.md
deleted file mode 100644
index 42954f8092..0000000000
--- a/.changeset/value-datasource-ast-filter-vocabulary.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@object-ui/core': patch
----
-
-fix(core): `ValueDataSource` applies the filters it is given instead of returning every row
-
-`matchesASTFilter` recognised only two node shapes — a logical `and` / `or` head
-and a three-element comparison — and answered `true` for everything else. Three
-consequences, all silent: a legacy flat implicit-AND array (`[[…], […]]`) applied
-no filter at all, at top level and as a nested child of `and` / `or` alike; the
-null-ness operators had no arm, so `is_null` / `is_not_null` selected every row;
-and 16 of the spec's 20 canonical view operators — `equals`, `greater_than`,
-`starts_with` among them, the spellings `toFilterNode` lowers a stored view's
-rules into — fell through the same way.
-
-The matcher now canonicalises operators through the spec's own
-`canonicalAstOperator` and reads all four shapes `FilterArraySchema` declares, so
-an in-memory `provider: 'value'` list applies the same filter the wire would. An
-operator or shape it cannot execute now excludes the row and logs once per
-`find()`, rather than passing every row with no signal anywhere.
diff --git a/.changeset/vi-mock-inherit-guard-6849.md b/.changeset/vi-mock-inherit-guard-6849.md
deleted file mode 100644
index 417385b551..0000000000
--- a/.changeset/vi-mock-inherit-guard-6849.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Test/tooling only; nothing published changes.
-
-Adds `scripts/check-vi-mock-inherit.mjs`, a ratchet that rejects a `vi.mock` factory
-which hand-lists the exports of a covered workspace specifier instead of inheriting the
-real module's export surface. The only `src/` file it touches is a plugin-view test
-file, converted to the inheriting form — no runtime behaviour, no public API, no
-published output changes.
diff --git a/.changeset/vi-mock-inherit-slice1.md b/.changeset/vi-mock-inherit-slice1.md
deleted file mode 100644
index a0667e06ba..0000000000
--- a/.changeset/vi-mock-inherit-slice1.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only: three `vi.mock` factories now inherit the real export surface of `@object-ui/plugin-markdown`, `@object-ui/data-objectstack` and `@object-ui/plugin-report` instead of hand-listing it, and those three specifiers join `check-vi-mock-inherit`'s covered set. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md b/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md
deleted file mode 100644
index 6296f60200..0000000000
--- a/.changeset/vi-mock-inherit-slice10-plugin-chatbot.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: 11 `vi.mock` factories under `packages/app-shell` now inherit the real
-`@object-ui/plugin-chatbot` export surface instead of hand-listing it, and that specifier
-joins `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs`. No published behaviour
-changes — no product source and no `package.json` was touched.
diff --git a/.changeset/vi-mock-inherit-slice11-plugin-designer.md b/.changeset/vi-mock-inherit-slice11-plugin-designer.md
deleted file mode 100644
index cce2fcea0d..0000000000
--- a/.changeset/vi-mock-inherit-slice11-plugin-designer.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: the ten `vi.mock('@object-ui/plugin-designer', ...)` factories in the
-`AppContent.*` sibling family now inherit the real module's export surface instead of
-freezing three of its 29 exports, and the specifier joins the covered set of
-`scripts/check-vi-mock-inherit.mjs`. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice12-plugin-list.md b/.changeset/vi-mock-inherit-slice12-plugin-list.md
deleted file mode 100644
index d58c3eb263..0000000000
--- a/.changeset/vi-mock-inherit-slice12-plugin-list.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
----
-
-Test-only change: the nine `vi.mock('@object-ui/plugin-list', ...)` factories in the
-`ObjectView` / `InterfaceListPage` / `ObjectDataPage` family under
-`packages/app-shell/src/views` now inherit the real module's export surface instead of
-freezing one of its twelve exports, and the specifier joins `COVERED_SPECIFIERS` in
-`scripts/check-vi-mock-inherit.mjs`. No published behaviour changes — no product source
-and no `package.json` was touched.
diff --git a/.changeset/vi-mock-inherit-slice13-fields.md b/.changeset/vi-mock-inherit-slice13-fields.md
deleted file mode 100644
index 590c936ec6..0000000000
--- a/.changeset/vi-mock-inherit-slice13-fields.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only change: eight `vi.mock` factories in `@object-ui/app-shell` now inherit
-the real `@object-ui/fields` export surface instead of freezing it, and the
-`check-vi-mock-inherit` gate covers that specifier. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice2.md b/.changeset/vi-mock-inherit-slice2.md
deleted file mode 100644
index fbe8d6d86b..0000000000
--- a/.changeset/vi-mock-inherit-slice2.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: the four frozen `vi.mock` factories on `@object-ui/plugin-charts`
-and `@object-ui/plugin-dashboard` now inherit the real module's export surface, and
-those two specifiers join `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs`
-(objectui#6892 slice 2). No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice3.md b/.changeset/vi-mock-inherit-slice3.md
deleted file mode 100644
index cf7cd96961..0000000000
--- a/.changeset/vi-mock-inherit-slice3.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change: the 102 frozen `vi.mock` factories on `@object-ui/auth` now inherit
-the real module's export surface, and that specifier joins `COVERED_SPECIFIERS` in
-`scripts/check-vi-mock-inherit.mjs` (objectui#6892 slice 3). The barrel was measured
-inert at module scope before converting, so nothing new runs at import time. No
-published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice4.md b/.changeset/vi-mock-inherit-slice4.md
deleted file mode 100644
index ac720d0ec6..0000000000
--- a/.changeset/vi-mock-inherit-slice4.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-infrastructure only: sweep `@object-ui/collaboration`'s 33 frozen `vi.mock`
-factories to the inheriting form and add the specifier to the
-`check-vi-mock-inherit` gate's covered set. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice5.md b/.changeset/vi-mock-inherit-slice5.md
deleted file mode 100644
index b285f11f6d..0000000000
--- a/.changeset/vi-mock-inherit-slice5.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-infrastructure only: sweep `@object-ui/plugin-form`'s frozen `vi.mock`
-factories to the inheriting form across `plugin-view`, `app-shell` and
-`plugin-designer` — 31 derived on the merge base plus one more that landed on
-`main` mid-slice — and add the specifier to the `check-vi-mock-inherit` gate's
-covered set. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice6-components.md b/.changeset/vi-mock-inherit-slice6-components.md
deleted file mode 100644
index 0b05e15256..0000000000
--- a/.changeset/vi-mock-inherit-slice6-components.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: the `@object-ui/components` `vi.mock` factories in `app-shell`,
-`plugin-detail` and `plugin-timeline` now inherit the real module's export
-surface instead of freezing a hand-written list, and the specifier joins the
-`check-vi-mock-inherit` gate's covered set. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice7-plugin-grid.md b/.changeset/vi-mock-inherit-slice7-plugin-grid.md
deleted file mode 100644
index 2551942772..0000000000
--- a/.changeset/vi-mock-inherit-slice7-plugin-grid.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only change: the 25 `vi.mock` factories that hand-listed `@object-ui/plugin-grid`'s
-exports now inherit the real barrel's export surface, and the specifier joins the
-`check-vi-mock-inherit` guard's covered set. No published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice8-permissions.md b/.changeset/vi-mock-inherit-slice8-permissions.md
deleted file mode 100644
index bccdff0367..0000000000
--- a/.changeset/vi-mock-inherit-slice8-permissions.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
----
-
-Test-only change: 24 `vi.mock('@object-ui/permissions', ...)` factories now inherit
-the real module's export surface instead of freezing a hand-listed one, and the
-specifier joins `COVERED_SPECIFIERS` in `scripts/check-vi-mock-inherit.mjs`. No
-published behaviour changes.
diff --git a/.changeset/vi-mock-inherit-slice9-plugin-detail.md b/.changeset/vi-mock-inherit-slice9-plugin-detail.md
deleted file mode 100644
index b5f8f8856d..0000000000
--- a/.changeset/vi-mock-inherit-slice9-plugin-detail.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: the 13 `vi.mock('@object-ui/plugin-detail', ...)` factories that hand-listed the barrel's exports now inherit the real module and override after it, and `@object-ui/plugin-detail` joins the `check-vi-mock-inherit` gate's covered set. No published behaviour changes.
diff --git a/.changeset/view-column-io-canonical-identity-5725.md b/.changeset/view-column-io-canonical-identity-5725.md
deleted file mode 100644
index 51eab73b65..0000000000
--- a/.changeset/view-column-io-canonical-identity-5725.md
+++ /dev/null
@@ -1,39 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-The metadata designer's View **column list** now reads a column's identity —
-row label and bound field name — in the ObjectStack canonical spelling only:
-`field` and `label`. The legacy TanStack aliases `accessorKey` / `header` are no
-longer consulted (objectui#5725). This is the editor-side half of the read
-objectui#5344 retired in the column inspector one file over; the two surfaces
-render inside the **same panel** and until now gave the author two different
-answers about what a column is called.
-
-Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both
-legacy keys by name (`unrecognized_keys`), so a column carrying them has no
-field key and no label the spec recognises. Two consequences the list surface
-had that the inspector did not:
-
-- **The label chain was inverted, not merely tolerant.** It read
- `label ?? header ?? field ?? accessorKey`, preferring the undeclared `header`
- **over** the declared `field`. A perfectly canonical column that also carried
- a stray `header` key displayed the alias' value *instead of* its own declared
- identity.
-- **The field-name read backs more than a label.** It feeds `usedFieldNames()`,
- which the Add-field picker consults, so a spec-refused column reserved a field
- name and the picker tagged that field "Added" — for a column no accepted
- document actually binds.
-
-**What an author sees:** a stored column shaped `{ accessorKey, header }` is now
-named positionally in the list (`col 1`) rather than by its refused spelling,
-and the row stays selectable exactly as before — the positional fallback already
-existed at the end of that chain. The Add-field picker no longer reports such a
-column's field name as taken. Canonical `{ field, label }` columns and
-bare-string columns are untouched, and a declared `label` / `field` now outranks
-any stray alias beside it.
-
-**What is deliberately NOT changed:** the write path. These helpers still
-reorder and splice the raw `columns` array without normalising it, so no stored
-document is rewritten by the act of viewing or editing it — the same fence
-objectui#5344 held.
diff --git a/.changeset/view-overlay-write-patch-only-5233.md b/.changeset/view-overlay-write-patch-only-5233.md
deleted file mode 100644
index 427d2ae8a1..0000000000
--- a/.changeset/view-overlay-write-patch-only-5233.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/app-shell': patch
----
-
-A view personalization overlay now stores **only the patch** — a column drag no
-longer freezes the source view's filter into it (objectui#5233).
-
-`ObjectView`'s `persistViewPatch` wrote `{ ...baseViewDef, ...patch }`, so an
-overlay written by a mere sort/density/column change copied the view's *current
-effective* `filter` — along with its `columns`, `label`, `type`, `isDefault` —
-into the stored row. The display merge is `{ ...source, ...override }`, so that
-copy then outranked the source view forever: an admin edited the view's filter
-and every user who had once resized a column silently kept the old filter, with
-nothing anywhere reporting it.
-
-The write now goes through the exported seam `buildPersistedViewBody`. For a
-**system view's overlay** the body is the patch plus `viewKind` when the active
-tab carries one — identity, not content, and the same line the read side already
-draws (`VIEW_OVERLAY_IDENTITY_KEYS`); `object`, `name` and the overlay marker are
-stamped by the adapter. A **saved view's own row** is deliberately still written
-whole: for it the body *is* the view, there is no source underneath it for a
-copied key to shadow, and the write is a whole-document PUT — narrowing it would
-delete the user's view rather than narrow it.
-
-This is the write half of the maintainer's 2026-08-12 ruling (objectstack#7494).
-It was blocked until `columnState` was admitted to the view-metadata surface as
-an explicitly runtime-only overlay key (objectstack#9933, released in
-`@objectstack/spec` 17.1.0) — before that a `columnState`-only patch was refused
-`422 INVALID_METADATA`, and the fat copy was the only thing supplying a
-recognized key. The read half (`narrowPersonalizationOverlay`) shipped earlier
-and stays: rows written before this land are still tolerated on read, and because
-the write replaces the whole document, the next toolbar toggle also strips such a
-row at rest. No migration.
diff --git a/.changeset/view-switcher-chart-gantt-icons-5586.md b/.changeset/view-switcher-chart-gantt-icons-5586.md
deleted file mode 100644
index 257d32d34d..0000000000
--- a/.changeset/view-switcher-chart-gantt-icons-5586.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-'@object-ui/plugin-view': patch
----
-
-`ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both
-icon maps in the package now name only spellings lucide still resolves
-(objectui#5586).
-
-`ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up
-in lucide's runtime `icons` record. lucide retires a spelling by dropping it
-from that record while KEEPING it as a deprecated named export, so a retired
-name still imports, still type-checks and still renders as a component — and
-silently resolves to nothing as a string. `ObjectView` composes the switcher
-from names, and two of them had been retired on lucide-react 1.31.0:
-`chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a
-label with no icon at all while every sibling type had one, and nothing went red
-because no lucide symbol appears in that map for the compiler to check. Measured
-against the installed package: `BarChart3` and `GanttChart` are absent from
-`icons`, while `ChartColumn` and `ChartGantt` are present.
-
-- `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`,
- `gantt-chart` → `chart-gantt`.
-- `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named
- deprecated aliases move to the names the record carries —
- `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`,
- `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the
- aliases already pointed at, so those two glyphs are unchanged; the `gantt`
- default picks up the plain gantt glyph, which is what `iconMap` now supplies
- for that view type.
-
-The regression pin widens from `tree` alone to EVERY name both maps supply: a
-pin scoped to the two names that broke would not have caught this and would not
-catch the next lucide bump.
diff --git a/.changeset/viewcolumn-identity-canonical-read-5344.md b/.changeset/viewcolumn-identity-canonical-read-5344.md
deleted file mode 100644
index 4a7fcd06eb..0000000000
--- a/.changeset/viewcolumn-identity-canonical-read-5344.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-'@object-ui/app-shell': minor
----
-
-The metadata designer's View column inspector now reads a column's identity —
-field key and label — in the ObjectStack canonical spelling only: `field` and
-`label`. The legacy TanStack aliases `accessorKey` / `header` are no longer
-consulted (objectui#5344).
-
-Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both
-legacy keys by name (`unrecognized_keys`), so a column carrying them has no
-field key the spec recognises. The inspector nevertheless read them and
-displayed `accessorKey`'s value in the field-key box, presenting a spec-refused
-key as though it were a valid column identity — the same consumer-side
-tolerance alias `ObjectGrid` retired in objectui#5068, surviving one layer up in
-the authoring tool, which is the surface that is supposed to teach the correct
-shape.
-
-**What an author sees:** a stored column shaped `{ accessorKey, header }` now
-shows an EMPTY field key and an empty header, and is re-authored rather than
-silently carried. Canonical `{ field, label }` columns and bare-string columns
-are untouched.
-
-**What is deliberately NOT changed:** the writeback. `patchIdentity` still
-re-emits whichever spelling it was handed, so no stored document is rewritten
-by the act of editing it. Normalising on write was ruled out once the
-maintainer confirmed there is no population of legacy stored documents to
-migrate; such a column stays unsaveable against the spec both before and after
-an edit, exactly as it did before this change. What this removes is the
-invisibility of that state, not the state itself.
diff --git a/.changeset/visiblewhen-record-binding-5454.md b/.changeset/visiblewhen-record-binding-5454.md
deleted file mode 100644
index 8cf9b2e4b5..0000000000
--- a/.changeset/visiblewhen-record-binding-5454.md
+++ /dev/null
@@ -1,45 +0,0 @@
----
-'@object-ui/react': minor
----
-
-Bind `record` into the node-level visibility evaluator, and stop a hoisted
-`properties.visible` swallowing a declared `visibleWhen`.
-
-`@objectstack/spec` has declared since ADR-0089 that a page component's
-`visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`,
-`current_user`, `page.`"*. `SchemaRenderer` bound no `record` at all. Its
-evaluator was built from the ambient predicate scope, `data: dataSource` (the
-connector **adapter**, not the row) and `page: pageVariables`; the row lives in
-`RecordContext`, which that evaluator never read.
-
-Because the surface is fail-soft, a `record.*` predicate did not misfire — it
-resolved to **shown**. Both polarities of the same predicate returned the same
-verdict, so a visibility gate silently did not gate, on every block on every
-record page. Measured on `record:alert`, `record:path`, `page:card` and
-`element:text`.
-
-Three changes, all in `SchemaRenderer`'s evaluation memo:
-
-- **`record` is bound**, as the `record` root only — the three roots the
- describe promises and nothing more. Not as bare fields, and never over
- `data`, which is what `${data.*}` in a props bag resolves against. Bound
- conditionally, so "no row" binds nothing rather than shadowing a `record` a
- host supplied through the ambient scope.
-- **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*`
- onto the node, so a node carrying `properties.visible` short-circuited the
- declared node predicate — the one key the spec tells authors to write was the
- one key that could be silently ignored. The two deprecated aliases
- (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into
- `visibleWhen` at parse, so a spec-parsed page never reaches them.
-- **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this
- predicate is broken" and "this predicate said yes" with the same word. The
- verdict is unchanged on every path — `evaluateCondition` already returned
- `true` for every unevaluable predicate, including the non-negated `hidden` /
- `hiddenOn` legs where that `true` means HIDE — so only the silence moved.
-
-**Behaviour change, stated plainly:** a shipped page whose node-level
-`record.*` predicate was previously inert now evaluates. A block that was
-permanently visible may begin to hide — which is the point, but it is a verdict
-change, not a no-op. `properties.visible` is unaffected in verdict: an
-in-tree census found **zero** node-level `record.*` predicates on page
-components, so nothing in this repository changes verdict.
diff --git a/.changeset/vitest-config-unification-3240.md b/.changeset/vitest-config-unification-3240.md
deleted file mode 100644
index 6bc68a43d8..0000000000
--- a/.changeset/vitest-config-unification-3240.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
----
-
-Repo tooling only — no published behaviour changes.
-
-objectui#3240 deletes the 17 per-package `vitest.config.ts` files (plus
-`examples/schema-catalog`'s) so the root `vitest.config.mts` is the repo's single
-test config, and rewrites every package `test` script to reach it by path filter
-(`vitest run --root ../.. /`). The three files this touches under a
-released package's `src/` are COMMENTS in test files, each naming a config or
-setup file this change deletes; no runtime or type surface moves. The collected
-test population is byte-identical before and after — 2459 (project, file) pairs,
-diffed programmatically.
diff --git a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md b/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md
deleted file mode 100644
index 61e50a177f..0000000000
--- a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
----
-
-Test tooling only — this publishes nothing, declared explicitly with an empty frontmatter
-rather than left undeclared.
-
-`assertCanonicalVitestInvocation` refuses a package-cwd Vitest run because such a run uses
-a different config than CI does, so its green says nothing about CI. Its docstring claimed
-that "every per-package `vitest.config.ts` re-exports the root config, and a package
-without one resolves upward to it, so no package-level path skips this file". Measured, by
-running `pnpm exec vitest run` from every directory that carries a config: 8 package
-configs import the root config and were refused, 2 packages carry no config and resolve
-upward and were refused, and **11 standalone configs never mention the root file at all**
-(`plugin-calendar`, `-charts`, `-detail`, `-form`, `-gantt`, `-grid`, `-kanban`, `-list`,
-`-map`, `-timeline`, `-view`) — nothing imported the guard from there, so it never ran.
-From `packages/plugin-grid`, one such run printed `Test Files 1 passed (1)` /
-`Tests 5 passed (5)` and exited 0, under a config carrying no `@object-ui/*` alias table
-at all where the root config maps ~40 specifiers at sibling `src/`. The guard's hole sat
-exactly where the divergence — and therefore the false-green risk — was largest.
-
-Those 11 configs now call the guard themselves, through a new
-`repoRootFrom(import.meta.url)` landmark search rather than a hand-counted `../..` (which
-resolves to a real directory when the count is wrong, so the guard would keep issuing
-verdicts computed against the wrong root). The docstring and the root config's call-site
-comment now describe the three routes a config can take instead of asserting one of them,
-and the claim is enforced rather than restated: the guard's own test walks every
-`vitest.config.*` in the repo and fails on any that takes neither route.
-
-No package `src/` is touched and the configs' test semantics are unchanged, so no
-`@object-ui/*` package changes behaviour and there is nothing here for a consumer to
-upgrade to.
diff --git a/.changeset/widget-config-panel-locale-map-5301.md b/.changeset/widget-config-panel-locale-map-5301.md
deleted file mode 100644
index 511b33d162..0000000000
--- a/.changeset/widget-config-panel-locale-map-5301.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@object-ui/plugin-dashboard': patch
-'@object-ui/i18n': minor
----
-
-`WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales.
-
-The dashboard widget config panel carried a private `resolveLabel` documented as
-resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference
-form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline
-per-locale map `I18nLabelSchema` actually admits has neither limb, so
-`{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy
-of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`,
-`MetricWidget` and `MetricCard`.
-
-This was not a display bug. The resolved value seeds the panel's editable draft,
-so a widget whose stored title was a map opened with an **empty** Title field and
-the next save wrote `''` over the author's map — on the ordinary path, not an
-exotic one: open the widget, change anything, save.
-
-Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301:
-
-- **Reading** goes through `pickLocalized(value, language)`, so the panel shows
- the active locale like every sibling surface post-objectui#4032.
-- **Writing** replaces only the active locale's entry and carries every other
- locale across. A title the author never touched round-trips the stored object
- itself through an unrelated config edit; an edited one merges into the entry
- that was displayed. The live-update callback (`onFieldChange`) forwards the
- merged map for the same reason — hosts feed it back into the widget the panel
- re-opens from, so a bare string there dropped the map before a save ever ran.
-
-`@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side
-inverse of `pickLocalized`, so the rule is stated once instead of re-derived per
-panel. It follows `pickLocalized`'s first three limbs — exact tag, base language,
-region-qualified sibling — and deliberately stops there: the `default` / `en` /
-first-value limbs are display fallbacks that hand back *another* locale's string,
-and writing to one would let an author editing in `fr` overwrite English. With no
-entry for the active locale the edit adds one. The pairing
-`pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a
-write that lands where the read does not look is how a "saved" string disappears.
-
-A full multi-locale editing UI remains out of scope (objectui#4163).
diff --git a/.changeset/widget-input-control-vocabulary-5675.md b/.changeset/widget-input-control-vocabulary-5675.md
deleted file mode 100644
index 43d23f920d..0000000000
--- a/.changeset/widget-input-control-vocabulary-5675.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-'@object-ui/types': patch
----
-
-`WidgetInput.type` now uses the shared arm vocabulary `ComponentInputControlType`
-instead of restating its eleven literals inline (objectui#5675).
-
-That vocabulary has had a name since objectui#3832, and objectui#4972 converged the
-last structural copy of the surrounding `ComponentInput` interface onto one
-declaration. `WidgetInput` was the remaining site spelling the arms out — a third
-copy of one list, with nothing tying it to the other two.
-
-**No value a widget author may write changes.** The inline restatement was measured
-member-equal to the shared declaration in both directions before it was replaced
-(eleven arms each, same set, and in the same order), so this is a convergence rather
-than a widening or a narrowing. `WidgetInput.type` also stays the SINGLE-kind form:
-`ComponentInput.type` additionally accepts an array of arms for a union-typed key
-(objectui#3832), and importing that capability here would have been a widening, so
-it was deliberately left out — the same disposition objectui#4972 recorded when it
-left this face alone.
-
-What the convergence buys is that one of the two drift directions was **silent**.
-`WidgetRegistry.load()` in `@object-ui/core` translates each `WidgetInput` into a
-`ComponentInput` and passes `type` straight through, so an arm REMOVED from the
-shared vocabulary would have broken that assignment loudly at compile time — but an
-arm ADDED to it produced no error anywhere. Widget authoring would just have stayed
-narrower than component registration, with nothing in the tree saying so. After this
-change neither direction is expressible.
-
-The pin is source-text, deliberately: a TS type alias erases at runtime, so a
-member-identical restatement is indistinguishable from the derived type by any
-assignability or runtime check. Both kinds of assertion are kept in
-`__tests__/widget-input-control-vocabulary.test.ts`, and the ablation showing the
-value assertions stay green on the defect while the identity pin turns red is quoted
-in the PR body.
-
-Two divergences between `WidgetInput` and `ComponentInput` are deliberately **not**
-repaired here, and are now recorded in `WidgetInput`'s doc block instead of living
-only in a closed card's body: the enum slot is spelled `options` on one face and
-`enum` on the other (adapted at the `WidgetRegistry` seam, so nothing fails to
-arrive), and `ComponentInput` carries five keys — `inputType`, `min`, `max`, `step`,
-`placeholder` — that a widget manifest cannot express. Both are surface questions
-about published keys and are raised on objectui#5675 rather than answered by this
-change.
diff --git a/.changeset/wild-charts-explain-themselves.md b/.changeset/wild-charts-explain-themselves.md
deleted file mode 100644
index e44172901c..0000000000
--- a/.changeset/wild-charts-explain-themselves.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-'@object-ui/plugin-charts': patch
----
-
-`ObjectChart` now renders a self-describing empty state when its query succeeds
-and returns no rows, instead of falling through to a bare chart frame.
-
-The frame was measured in a browser rather than assumed: recharts derives its
-ticks from the data, so with an empty result the bar and line families emit two
-hairline axis rules and no `text` nodes at all, and pie/donut emit nothing —
-there are no labelled axes to tell the reader what would have been plotted.
-Beside the component's own red "Failed to load chart data" box, a blank tile
-gives the reader nothing to distinguish a young chart from a broken one.
-
-The copy is the one `plugin-dashboard` already shows on the dataset-bound path
-("No data yet" / the load succeeded / the source name), so the same chart over
-the same empty result no longer reads two different ways depending on which
-widget drew it. Charts with inline authored data are unchanged — they ran no
-query to report on.
diff --git a/.changeset/wild-donkeys-shake.md b/.changeset/wild-donkeys-shake.md
deleted file mode 100644
index e50f7c70c5..0000000000
--- a/.changeset/wild-donkeys-shake.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-"@object-ui/plugin-map": patch
----
-
-`ObjectMap` no longer serializes its data config on every render.
-
-`getDataConfig(schema)` was called bare in the render body and its result
-re-serialized with `JSON.stringify` on every render, purely to give `dataConfig`
-the stable identity its fetch effect depends on. `getDataConfig` is a pure
-function of `schema`, so `useMemo(() => getDataConfig(schema), [schema])` gives
-the same identity with no serialize and no per-render rebuild.
-
-This also fixes a crash. `JSON.stringify` throws on a value it cannot serialize,
-and the config's passthrough branch returns the author's own `schema.data`
-object verbatim — inline rows included. A map handed inline records carrying a
-back-reference (an `$expand`-ed lookup) or a `BigInt` id threw from the render
-body and took the whole map subtree down with it. Comparing identities never
-serializes, so the config no longer has to be serializable at all.
diff --git a/.changeset/wild-tables-shout.md b/.changeset/wild-tables-shout.md
deleted file mode 100644
index bcd0b80d11..0000000000
--- a/.changeset/wild-tables-shout.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
----
-
-Test-only change: `RelatedList.lookupLabelResolution` waits for the batch lookup-label map with a side-effect-free predicate and renders the cell once afterwards, instead of rendering inside the `waitFor` callback. No published behaviour changes.
diff --git a/.changeset/wire-dropped-fields-shape-6889.md b/.changeset/wire-dropped-fields-shape-6889.md
deleted file mode 100644
index 51a6de28fd..0000000000
--- a/.changeset/wire-dropped-fields-shape-6889.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@object-ui/data-objectstack': patch
----
-
-Parse a `droppedFields` wire entry's `fields` elements and its `object` at the
-write-warning boundary instead of asserting them.
-
-The structural gate checked `Array.isArray(fields) && fields.length > 0` and then
-asserted the entry into a type declaring `fields: string[]` and a required
-`object: string` — reading neither. A response carrying `fields: [42]` reached
-`onWriteWarning` subscribers typed as a field name (the shell rendered it as the
-label `42`), and an entry that omitted `object` arrived claiming a string that was
-not there.
-
-Now the wire type declares only what the gate establishes, and the notice is
-parsed: non-string `fields` elements are refused, an entry naming no field at all
-is dropped as `fields: []` already was, and a missing or non-string `object` is
-healed from the object the write targeted. Warnings are never silenced for a
-field the server really did name. No published type changes — `WriteWarningEvent`
-and `DroppedFieldsNotice` keep their shapes, and a subscriber's `fields: string[]`
-is now true rather than asserted.
diff --git a/.changeset/wise-pandas-pin.md b/.changeset/wise-pandas-pin.md
deleted file mode 100644
index 0a9ba4fdbc..0000000000
--- a/.changeset/wise-pandas-pin.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
----
-
-Test-only: pin that `record:alert`'s CTA honours an action's `resultDialog` through the
-surrounding provider's shared `ActionRunner`, and that with no provider the CTA falls back
-to a local, unwired runner that discards the value and warns. No published behaviour changes.
diff --git a/.changeset/zod-array-element-reader-5872.md b/.changeset/zod-array-element-reader-5872.md
deleted file mode 100644
index 0fdacdbfcd..0000000000
--- a/.changeset/zod-array-element-reader-5872.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
----
-
-Internal test-support only: `@object-ui/test-support` gains `arrayElementSchema`
-(objectui#5872 class (2)), and the three disagreeing hand-written array-element
-walks in `plugin-detail`, `app-shell` (previews) and `app-shell`
-(clientValidation) converge onto it, along with one `.unwrap().options` read in
-`data-objectstack`. No published runtime source changes: the diff is test files
-plus a `private: true` workspace package that ships in no bundle, so this
-declaration carries an EMPTY frontmatter — nothing to version.
diff --git a/.changeset/zod-base-schema-mirror-parity-4605.md b/.changeset/zod-base-schema-mirror-parity-4605.md
deleted file mode 100644
index 1d0ba8c9fe..0000000000
--- a/.changeset/zod-base-schema-mirror-parity-4605.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-'@object-ui/types': minor
----
-
-The zod `BaseSchema` mirror now accepts everything its TypeScript declaration
-declares — five keys had drifted narrower (objectui#4605).
-
-`@object-ui/types/zod` is a published runtime validator hand-written to mirror the
-`BaseSchema` interface. As the interface widened, the mirror did not, so five keys
-refused at parse time a spelling the published types invite and the renderer
-implements — "declared = enforced" inverted. `.passthrough()` rescued none of them:
-passthrough admits UNDECLARED keys, and all five are explicitly declared, so the
-narrow declaration won.
-
-Measured against the unmodified mirror before the change, these were the refusals:
-
-| key | authored input | old mirror said |
-|---|---|---|
-| `visible` | `'${data.status === "open"}'` | `expected boolean, received string` |
-| `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` |
-| `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` |
-| `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` |
-| `description` | `{ en: 'The record owner' }` | `expected string, received object` |
-
-`visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts,
-no wider. `ariaLabel` takes the KEYED reference through a new exported
-`KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema`
-BY REFERENCE, so a change to the spec's label contract is picked up rather than
-re-typed. Every spelling that parsed before still parses.
-
-The two i18n vocabularies are kept apart rather than merged into "some object".
-`label`/`description` are the spec's INLINE locale map (resolved by
-`resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by
-`resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render
-an EMPTY aria-label). Widening both slots to accept either shape would have
-reproduced objectui#4167's confusability hazard inside the validator that exists to
-catch it, so each slot admits only its own vocabulary and both cross pairings are
-pinned as rejections.
-
-The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own
-`.shape` and compares each key against the declaration, so the next widening of
-`base.ts` that forgets this file turns it red with no list to maintain. It reads
-`.shape` and not `keyof z.input<…>` because that spelling was measured vacuous —
-`.passthrough()` collapses the inferred key union to bare `string`, and a pin written
-over it resolved `never` while five keys were demonstrably narrow. Two guards pin the
-derivation against both degenerations (`never` and `string`).
diff --git a/.changeset/zod-internals-reader-confinement.md b/.changeset/zod-internals-reader-confinement.md
deleted file mode 100644
index 4fc9998c3e..0000000000
--- a/.changeset/zod-internals-reader-confinement.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change. Four spec-parity suites each carried a byte-for-byte identical
-hand-written walk into Zod's internals to read a key's enum vocabulary; they now
-import one `shapeEnumOptions` reader from the private, never-published
-`@object-ui/test-support`. No published package's runtime behaviour changes —
-the only edits to released packages are a `devDependencies` entry each.
diff --git a/.changeset/zod-mirror-parity-population-5684.md b/.changeset/zod-mirror-parity-population-5684.md
deleted file mode 100644
index e1b81105b1..0000000000
--- a/.changeset/zod-mirror-parity-population-5684.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
----
-
-Test-infrastructure only, no shipped code touched: the derived anti-drift parity
-construction that objectui#5680 built for `BaseSchema` now covers every hand-written
-zod mirror in `@object-ui/types`'s `src/zod/` directory instead of that one schema.
-
-A mirror restates a TypeScript declaration by hand, and when the declaration widens
-and the mirror does not follow, the published validator refuses a spelling the
-published types invite — `declared !== enforced` on a shipped surface. Two instances
-were found independently before anything looked for them (objectui#4605, #5186);
-nothing detected the class itself.
-
-`packages/types/src/__tests__/zod-mirror-parity.test.ts` registers 164 mirror/declaration
-pairs and applies one construction to all of them: it reads each mirror's own `.shape`
-and compares every key against the declaration, so a widening that forgets a mirror
-turns red with no key list to maintain. A runtime census reads the directory off disk
-and fails when an exported const is in neither the registry nor the reasoned exclusion
-list, so a mirror added later cannot join the population silently. 18 pairs carry
-measured drift today and are pinned to their exact drifted key sets as a ratchet — new
-drift fails, and so does a stale entry once the drift is fixed.
-
-`base-schema-zod-mirror-parity.test.ts` keeps its objectui#4605 runtime pins and no
-longer restates the type-level construction; `BaseSchema` is one registered row.
diff --git a/.changeset/zod-mirror-parity-population-derived.md b/.changeset/zod-mirror-parity-population-derived.md
deleted file mode 100644
index 5dea8c3b6e..0000000000
--- a/.changeset/zod-mirror-parity-population-derived.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
----
-
-Test-only change: `packages/types/src/__tests__/zod-mirror-parity.test.ts` now derives its
-pair-population figure and both of the differences its header quotes, instead of restating
-them as prose literals that nothing checked. `@object-ui/types` publishes nothing from
-`__tests__/` — the package `tsconfig.json` excludes the directory by name, and the build
-program lists zero files under it — so no published behaviour changes.
diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md
index 1454601a3a..8a235b2040 100644
--- a/QUICK_REFERENCE.md
+++ b/QUICK_REFERENCE.md
@@ -124,7 +124,7 @@ release path runs the sync itself: `changeset:version` bumps every manifest and
this block in the same commit, so a release can no longer leave the block a version
behind (objectui#5394 — that had happened once per release, three times).
-- **Version:** 17.6.0 (the version every `@object-ui/*` manifest carries — they are one
+- **Version:** 17.7.0 (the version every `@object-ui/*` manifest carries — they are one
`fixed` group in `.changeset/config.json`, so a release moves all of them together)
- **Spec:** `@objectstack/spec` ^17.0.0 (declared by the root `package.json` and by
`apps/console/package.json`)
diff --git a/apps/console/CHANGELOG.md b/apps/console/CHANGELOG.md
index 8ef1818ba4..77ddc413ff 100644
--- a/apps/console/CHANGELOG.md
+++ b/apps/console/CHANGELOG.md
@@ -1,5 +1,1248 @@
# @object-ui/console
+## 17.7.0
+
+### Minor Changes
+
+- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the
+ dataset captions.
+
+ **The maker's start chips now promise only what ADR-0112 v1 builds
+ (cloud#1984).** Two of the five asked for automation the first version has no
+ flows or actions for — the ticket chip said 「状态流转」, the inventory chip said
+ 「低库存预警」 — and the measured behaviour was not a refusal but a silent
+ degrade: a status kanban and a low-stock view. The chip promised an alert and
+ delivered a page. All five are reworded in all ten packs (and in the call-site
+ `defaultValue` fallbacks, which are a second copy of the same strings) to ask
+ for objects, fields, views, pages, dashboards and sample data, keeping each a
+ real business scenario — the ticket chip now asks for a status field and a board
+ grouped by it, the inventory chip for a view that filters below the reorder
+ point. A note beside the keys says to revert when v2 re-adds flows.
+
+ **Five newer AI tools get their step labels (objectui#7481).** A zh conversation
+ read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」:
+ `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`,
+ `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are
+ newer than the pinned spec's tool registry, so they had no `chatbot.tool.*`
+ entry in any pack and fell through to the English title-caser.
+
+ **The assistant rail follows the thread when you send (objectui#7480).** The
+ rail and the full-page maker are the same component; what differs is width. A
+ reply that still ends on screen in the wide column runs two or three times
+ taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the
+ user types and the new bubble, the tool steps and the streaming answer all land
+ below the fold. Every send path now re-arms the lock — including the plan-card
+ "Build it" and 确认修改 approvals, whose own code comments already named this
+ miss. Message APPENDS deliberately do not, so a user reading back through the
+ thread mid-answer is never yanked to the bottom.
+
+ **Console toasts move off the assistant composer (objectui#7482).** 「客户更新
+ 成功」 sat on the ChatDock composer's send button and stayed there. One defect,
+ two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override
+ that predates ADR-0057 P3a — so a toast both covered the button and, because
+ sonner pauses a toast's dismiss timer while the pointer is inside the toaster
+ region, never got to run its 4s timer with a pointer resting on the composer
+ underneath. The override is gone; the console takes `ConsoleToaster`'s own
+ documented top-right anchor, and the 4s success duration is now pinned.
+
+ **Built-in aggregate captions follow the locale everywhere (objectui#7534).**
+ objectui#7258 taught `buildChartSeries()` to resolve a server-minted default
+ measure through the locale map, so a chart legend read `计数` while the table
+ beneath it, the KPI caption, the pivot header and the dataset preview still
+ printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()`
+ takes the same optional `builtinAggregateLabels`, resolving through the one
+ `resolveMeasureLabel` order, and the five call sites pass it. Omitting the
+ argument reproduces the previous output byte for byte, and an author-declared
+ measure still keeps its own label verbatim (objectui#4106).
+
+ **The activity feed stops asking for an object the environment does not have
+ (objectui#7476).** A tenant environment has no `sys_activity`, so every page
+ load issued a request that 404'd. Everything downstream was already correct —
+ the adapter memoizes the missing collection, its logger demotes the failure, the
+ feed retires as an ANSWER and the panel renders its earned empty state — so what
+ is left is the request itself, and `data-objectstack` states the rule for it:
+ the cure for a doomed request is not issuing it. New `useObjectPresence` reads
+ the object registry the shell loads for the nav anyway; only a registry that has
+ ANSWERED and lists other objects without this one skips the read. Every
+ uncertainty — no provider, empty registry, still loading, errored — reads as
+ before, because a wrong skip would cost a real deployment its feed.
+- 26fbe09: Console telemetry can now be hard-disabled on an already-built artifact
+
+ `/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting`
+ (objectstack#11382), and the Console now reads it. The Sentry decision becomes a
+ conjunction of two independent grants — a DSN injected at **build** time AND a
+ positive permission from the **runtime** — so the single pre-built SPA that both
+ the hosted SaaS console and the on-premises / air-gapped EE images embed can be
+ silenced by the deployment it lands in, with no rebuild and without editing files
+ inside a published bundle. That was the half objectui#5522 could not close before:
+ every other input to the gate is a Vite build-time variable frozen into the bundle
+ as a literal, which is how an air-gapped EE Console came to send 14 Sentry
+ envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for
+ the customer to turn it off (objectstack-ai/cloud#1508).
+
+ The permission fails **closed** in every direction: absent key, `telemetry` block
+ absent, malformed payload, failed fetch, or a runtime predating the key all read as
+ *do not send* — which is precisely the set of runtimes leaking today. It is a
+ permission and never a source: the server supplies no DSN and cannot turn telemetry
+ on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1`
+ and other truthy lookalikes do not.
+
+ Behaviour change for deployments that already inject a DSN: reporting now also
+ requires the runtime to grant permission, via
+ `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s
+ `allowClientErrorReporting`). A build that opted in but whose runtime says nothing
+ will go quiet — deliberately, since that is the same artifact an air-gapped
+ customer runs.
+
+ `@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and
+ the `RuntimeTelemetry` type, so consumers read the permission through the one
+ fail-closed accessor instead of writing their own optional-chain against the
+ payload.
+- 5173a5e: ⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on
+ the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read
+ this before upgrading if any of your form metadata gates on the session user.
+
+ objectui#6010 bound the host predicate scope on the five authored-predicate call
+ sites in the components form renderer, so `current_user` (plus the ADR-0068
+ `user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn`
+ there. Two other authored-predicate evaluators were still passing `undefined`
+ for that argument, so the same authored text meant two different things
+ depending on which surface opened the form (objectui#6110):
+
+ - **`apps/console`'s form renderer**, on the authed internal route
+ `/forms/:name`. The internal route is a runtime record surface by ADR-0089
+ D1's own words (*"runtime record surfaces bind `record` + `current_user`"*),
+ and its `visibleWhen` metadata is the same `*.view.ts` FormView the
+ object-view chain renders — so a role gate authored once behaved differently
+ depending on which route opened the form.
+ - **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`),
+ the gate that re-checks the whole declared field set at final submit because
+ `allowSkip` can jump past a step. Its docstring promises *"the same verdict
+ from all three rather than a second, divergent dialect"*, and since #6010 it
+ was the divergent one.
+
+ **Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails
+ OPEN: a field on screen is what you get when the predicate resolves TRUE, when
+ the scope was never bound so the predicate faulted, *and* when the predicate is
+ broken. Those worlds were indistinguishable, so an app that authored a
+ `current_user` gate saw the field render and had no way to tell the rule was
+ inert. After this change the predicate is evaluated for real, and fields and
+ sections that have always been visible will disappear for the users the rule
+ excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user`
+ requiredWhen that has been silently not applying will now start holding submits.
+
+ In the wizard the change is a fix in the user's favour as well: a required field
+ the wizard HID from this user was still counted as visible by the submit gate,
+ so the submit was refused on a control the submitter could neither see nor fill
+ in.
+
+ **Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in
+ your form-view and object metadata that names `current_user`, and confirm each
+ predicate says what you actually want evaluated against `record` +
+ `current_user`.
+
+ **The public anonymous form `/f/:slug` is deliberately unchanged.** It is
+ mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is
+ no authenticated principal, and no provider is mounted above it — so its scope
+ is empty and a `current_user` predicate authored on a public form still faults
+ and still fails open, exactly as before. Nothing new is declared to say so: the
+ two routes are told apart by which component mounts them.
+
+ `@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider`
+ user normalisation, so every console surface that mounts the provider publishes
+ the same `current_user` shape rather than re-deriving it.
+- 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
+ the direction so the next one cannot land (objectui#8198).
+
+ `check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
+ so a declaration could outlive its last consumer indefinitely. That is what happened to
+ `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
+ was removed by hand on objectui#7625, and nothing would have reported the next one. The
+ new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
+ `optionalDependencies` of every released package.
+
+ **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
+ changes: their Vite `external` predicates are path-based and never read `dependencies`,
+ so no built artifact moves. What changes is the install graph — a project that imports
+ one of the removed packages while depending only on the ObjectUI package that used to
+ drag it in will no longer resolve it. Declare it directly; that is the correct
+ dependency edge in either case. The removals, by package:
+
+ - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
+ - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
+ - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
+ - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
+ - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
+ - `@object-ui/core`: `lodash`, `zod`
+ - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply
+ - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
+ - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
+ - `@object-ui/fields`: `clsx`, `tailwind-merge`
+ - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
+ - `@object-ui/auth`: `@object-ui/types`
+ - `@object-ui/plugin-calendar`: `@object-ui/fields`
+ - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
+ - `@object-ui/react`: `react-hook-form`
+
+ Every one was verified by a whole-package grep before removal — the name appeared nowhere
+ under the package but its own manifest and CHANGELOG — and the whole workspace builds,
+ type-checks and tests green afterwards.
+- c8da8b3: Stop showing an access-denied screen to a real administrator while their
+ adminship is still resolving.
+
+ `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not
+ arrived yet" and "resolved: not an admin" were the same answer. One of its three
+ sources — the active organization member row — is fetched some round trips after
+ the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`),
+ so an administrator whose adminship lives only in that row rendered at least
+ once as a non-admin, and every gate downstream acted on it: the two marketplace
+ surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and
+ re-added its admin nav entry, and `AppContent` fired a `` that the later flip could not undo.
+
+ **Breaking (published API, hence `minor` per this repo's version policy):**
+ `useIsWorkspaceAdmin(): boolean` is replaced by
+ `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old
+ name is removed rather than kept alongside, so a call site that ignores the
+ third state fails to compile instead of silently refusing an administrator.
+
+ -const isAdmin = useIsWorkspaceAdmin();
+ +const { isAdmin, isResolved } = useWorkspaceAdminStatus();
+
+ `AuthProvider` gains `isMembershipResolved` on its context — the organization /
+ member pipeline has reached a terminal state — because `organizations`,
+ `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before
+ the pipeline starts and after it finds nothing.
+
+ No extra wait for administrators: `isResolved` is true the instant `isAdmin` is,
+ so an admin the session already identifies through `positions[]` never waits on
+ the member row.
+- 14ef9f5: Console error reporting is now configured entirely by the **server**: the DSN and every
+ knob that travels with it arrive on `GET /api/v1/runtime/config`, and the build-time
+ `VITE_SENTRY_DSN` path is retired (objectui#5522, consumer half of objectstack#12681).
+
+ ObjectStack's users consume a **prebuilt** Console. Under the previous two-key gate —
+ a build-time DSN **and** a runtime permission — a build-time key was unreachable for
+ them, so a self-hosting operator could not enable client error reporting at all: the
+ permission was reachable and the source was not. The maintainer named it on 2026-08-27:
+
+ > 「我是一个开发平台呀,我的用户并不会去构建我的前端,我理解这种应该在服务端传进去。」
+
+ ```
+ send ⇔ the runtime served a DSN
+ ```
+
+ **The DSN's presence IS the grant.** There is no companion permission flag, and the one
+ that briefly existed is removed rather than paralleled. Two knobs in two places produced
+ two silent dead states — "permission on, no DSN" and "DSN in, permission off" — that look
+ identical from the browser; one knob cannot disagree with itself. Turning reporting off is
+ unsetting the server DSN, and there is deliberately no build-time force-off left, because
+ nobody consuming a prebuilt console could reach one.
+
+ The fail-closed posture is unchanged and structurally stronger. Absence of a *source* is
+ not a value that can be misread, so a runtime predating the key, a third-party host, a
+ 404, a network failure, a malformed body and a config that has not arrived yet all read as
+ off — where the boolean needed a strict `=== true` plus a written argument about why a
+ negative `disabled` flag would have been vacuous on exactly the runtimes that were leaking.
+
+ ### What moved, and the one thing that did not
+
+ `sendDefaultPii`, `environment`, `tracesSampleRate` and the error-session replay rate move
+ into the runtime payload. They were build-time variables, so a prebuilt-console consumer
+ could set none of them — including the one deciding whether IP and User-Agent leave their
+ network. This is not new surface; it is the same surface moved to the side that can
+ operate it.
+
+ `VITE_SENTRY_RELEASE` **stays build-time**, and is now the only `VITE_SENTRY_*` variable
+ that exists. A release identifies which bundle produced a stack trace and must match the
+ source maps that bundle's pipeline uploaded — a property of the build, which no server can
+ know. `VITE_SENTRY_ENABLED`, `VITE_SENTRY_ENVIRONMENT`, `VITE_SENTRY_TRACES_SAMPLE_RATE`
+ and `VITE_SENTRY_REPLAY` are retired along with `VITE_SENTRY_DSN`.
+
+ ### Breaking
+
+ | FROM | TO |
+ |:--|:--|
+ | `VITE_SENTRY_DSN=…` in the Console build environment | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_DSN=…` on the ObjectStack runtime |
+ | `VITE_SENTRY_SEND_DEFAULT_PII=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_SEND_DEFAULT_PII=true` |
+ | `VITE_SENTRY_ENVIRONMENT=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENVIRONMENT=…` |
+ | `VITE_SENTRY_TRACES_SAMPLE_RATE=…` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_TRACES_SAMPLE_RATE=…` |
+ | `VITE_SENTRY_REPLAY=true` | `OS_TELEMETRY_CLIENT_ERROR_REPORTING_REPLAY_SAMPLE_RATE=0.1` |
+ | `VITE_SENTRY_ENABLED=false` | unset the runtime DSN |
+ | `isClientErrorReportingAllowed(): boolean` | `getClientErrorReporting(): RuntimeClientErrorReporting \| null` |
+ | `resolveSentryGate(env, runtimeAllows)` | `resolveSentryGate(runtimeErrorReporting)` |
+ | `RuntimeTelemetry.allowClientErrorReporting: boolean` | `RuntimeTelemetry.errorReporting?: RuntimeClientErrorReporting` |
+
+ One-line fix for a deployment: move your `VITE_SENTRY_*` values onto the ObjectStack
+ server as the `OS_TELEMETRY_CLIENT_ERROR_REPORTING_*` variables above, and drop them from
+ the Console build environment. One-line fix for a consumer of `@object-ui/app-shell`:
+ `const sink = getClientErrorReporting(); if (sink) …` in place of
+ `if (buildTimeDsn && isClientErrorReportingAllowed())` — the build-time conjunct is gone,
+ because the server now supplies the source.
+
+ **Landing order is safe in both directions.** A Console built before this change meets a
+ new server, reads an absent `allowClientErrorReporting` and stays off; a Console built
+ after it meets an old server, reads an absent DSN and stays off. Neither half can turn
+ reporting on by itself, so the two repos' PRs can land in any order.
+
+ The `committed-telemetry-endpoint.test.ts` ratchet is unchanged in rules and unchanged in
+ job: nothing endpoint-shaped may be committed to this repo. Its rules key on the
+ variable's suffix and on the value rather than on the `VITE_` prefix, so they already
+ cover the runtime-side spelling — now pinned, so a later tidy-up cannot narrow them to the
+ retired names and reopen the hole under a new one.
+
+### Patch Changes
+
+- ff8587f: Setup's settings selects now follow the specifier's `valueDomain` declaration instead of
+ treating the curated `options` table as the domain (objectui#3719).
+
+ Since objectstack#5712 / PR objectstack#6581 a settings specifier may declare
+ `valueDomain` (`iana_time_zone` | `iso_4217_currency` | `iso_3166_alpha2`), and when it
+ does the **standard's membership is the enforcement boundary** — the server accepts
+ `timezone: 'Europe/Zurich'` and `currency: 'CHF'`, neither of which is in the manifest's
+ list. The console kept drawing those keys as closed dropdowns, so an admin could author
+ only the 17 curated zones and 9 curated currencies while the contract took the whole
+ domain; every other legal value was reachable by API or `OS_LOCALIZATION_*` env only. The
+ keys' own descriptions had promised "IANA zone" / "ISO 4217 code" all along.
+
+ `case 'select'` in `SettingsField` now keys the control off the declaration. Declared →
+ an editable combobox: the curated options stay on as suggestions (native ``, the
+ same suggest-but-allow-anything affordance `FlowReferenceField` uses — no new dependency),
+ free text is committed verbatim, and an out-of-domain value is refused by the server with
+ `invalid_value` + `constraint: { valueDomain }` into the field-error slot that already
+ exists.
+
+ **Undeclared → the closed dropdown is untouched**, which is half the change rather than a
+ caveat. Those `options` are still exhaustive under objectstack#5131 (the sms/mail provider
+ selects), and `localization.locale` had its domain declaration deliberately **rejected** in
+ objectstack#6515 because its options *are* the shipped catalogs. Widening those to free
+ input would be a regression wearing this fix's clothes, so the two branches are pinned
+ against each other from the specifier data rather than from a list of key names — a key
+ that gains a domain server-side joins the right side of the pin with no edit here.
+
+ Root cause, because it will recur: `Specifier` in `pages/settings/types.ts` is a
+ hand-written **local mirror** of the server's shape, not an import, so nothing tells it when
+ the schema grows — and TypeScript reports nothing, because a narrower mirror is a
+ structurally valid reading of a wider object. `valueDomain` is added there and the file
+ header now says to check the mirror first when a settings feature "doesn't render".
+- baac3f4: Fix four `find()` calls that passed a query option without its `$`, and gate the shape.
+
+ `QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies
+ exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw,
+ no warning, and it type-checks because the type carries `[key: string]: any` for
+ adapter-specific params. For a dropped cap the result is an **unbounded** read rather than
+ a truncated one: the platform's GET list route has no default page size, so the query
+ returns the whole match set and stays invisible until the object is large.
+
+ - `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one
+ **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so
+ the dropped key turned "count only, fetch nothing" into "fetch every row in the object",
+ on every mount and every refresh of every list view. It now sends `$top: 0` and reads
+ the count off `total` only; the row-counting fallbacks are gone rather than repointed,
+ because once zero rows are requested an empty `data` means "you asked for none", not
+ "the object is empty", and counting it would assert a confident `0`. With no total the
+ footer line is omitted instead.
+ - `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`,
+ one line from three correct `$top` calls.
+ - `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so
+ a filter's option list read every row and every column of its source object while its
+ own comment described it as capped at 200. The same call read `records.items`, which is
+ not a `QueryResult` member, so against a real adapter the fallback produced no options
+ at all.
+ - `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the
+ result in its page-source metadata.
+
+ A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time:
+ a known query-option name missing its `$` in the second argument of a `find`/`findOne`
+ call. It is narrow on purpose — a closed list of spellings, anchored to the call — because
+ the index signature exists so adapters can take adapter-specific params, and a rule that
+ flagged any unprefixed key would report the shape the type was written to allow. Its
+ sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged.
+- 7a72422: Publish the create-payload rule from `@object-ui/plugin-form`'s entry, so a
+ second form renderer can call it instead of composing it by hand
+ (objectui#6059).
+
+ Newly importable from `@object-ui/plugin-form` — two functions, nothing else:
+
+ ```typescript
+ import { omitServerResolvedDefaults, isRequiredInForm } from '@object-ui/plugin-form';
+ ```
+
+ - `omitServerResolvedDefaults(values, objectSchema)` — drop the keys a CREATE
+ payload must leave to the producer: a field whose declared `defaultValue` is a
+ runtime instruction (`NOW()` / `current_user`, or a CEL envelope) and whose
+ submitted value is empty. `ObjectQL.applyFieldDefaults` resolves a declaration
+ only for a field that arrives absent or null, so submitting a blank stores
+ `''` and silently defeats it. **Create-only** — the caller keeps the mode gate.
+ - `isRequiredInForm(field, isCreateForm)` — the `required` a form should
+ enforce, given the mode. Published as the pair's other half on purpose:
+ excusing a server-owned field from `required` and then submitting the key
+ anyway is not half a fix, it is no fix.
+
+ Both are pure functions over plain data (no React, no registry). The rest of
+ `schemaDefaults.ts` — `seedCreateValues`, `schemaDefaultValues`,
+ `isSeedableDefault`, `isCreateFormMode`, `SeedContext` — stays module-private,
+ and `isRuntimeDefault` stays `@object-ui/core`'s to publish.
+
+ No behaviour change. The console's `FormPage` now calls the published helper
+ instead of composing `isRuntimeDefault` + `isMissingForRequired` locally; its
+ create payload is decided identically before and after, pinned against the
+ deleted implementation over the full matrix of default shapes, value spellings
+ and both modes.
+- 6630b37: The console boot no longer flashes a fully-white frame after the splash has painted
+ (objectui#6378).
+
+ Cause, established by measurement before any fix was written — the card named
+ `LoadingScreen`'s unmount timing and `RouteFader` as suspects and both are exonerated.
+ A CDP `Page.startScreencast` frame ledger (every frame classified with the card's own
+ rule: white when no colour channel falls below 242) was correlated against a DOM-state
+ ledger on the same clock (`performance.timeOrigin`), against the production
+ `apps/console` bundle with the boot endpoints mocked. `RouteFader` never mounts on the
+ boot path at all, and `LoadingScreen` unmounts exactly when its own gate says to. What
+ is wrong is what REPLACES it: every readiness gate renders the splash while it waits and
+ a bare `` the moment it decides. `` renders `null` and react-router
+ runs the navigation as a transition, so the destination tree renders while the commit
+ that already dropped the splash is what the compositor is showing — 41–147 ms during
+ which `#root` holds no view and the viewport is the bare page background. The flash is
+ intermittent only because it depends on a frame being swapped inside that window; the
+ window itself was present on every measured boot.
+
+ `RedirectWithSplash` (new, `@object-ui/app-shell`) pairs the same `` with the
+ same `LoadingScreen` the gate one line above was already rendering, so the handoff
+ changes no pixels and the transition runs underneath an unchanged screen. The console's
+ three boot redirects use it: the auth gate's `/login` bounce, the `/` landing resolver,
+ and the catch-all route. The nested organization `index` redirect deliberately does not —
+ it fires under an already-painted layout, where covering the screen would be the
+ regression.
+
+ Acceptance campaign — same instrument on both sides, the two arms INTERLEAVED inside one
+ process and one browser so drift in this shared container's load lands on both equally.
+ 102 paired boots per arm across five cells (signed-out `/`, signed-in `/`, an unmatched
+ entry, and the card's two throttled network profiles). The empty-viewport window: 102/102
+ pre-fix, 0/102 post-fix. The white frame itself, pooled over the three cells where the
+ pre-fix build actually flashed: 67/87 (77%) pre-fix, 0/87 post-fix — 95% upper bound on
+ the residual rate 3.4%, against a card-reported defect rate of ~1/3. The two throttled
+ cells are reported but NOT pooled: the pre-fix build flashed 0 times there, so before and
+ after agree and those cells prove nothing about the pixels (they still separate 15/15 vs
+ 0/15 on the DOM window).
+
+ `e2e/console-boot-indicator.spec.ts` gains the deterministic half as a gate — after
+ React's first commit the viewport centre must never stop being covered. That reading is
+ what makes an intermittent defect gateable: the flash needs a frame to be swapped inside
+ the window, but the window itself was present on every measured boot. Verified red-first,
+ 6/6 red on the pre-fix bundle and 6/6 green on this one.
+- 1db5b76: Console: free the marketplace routes `AppContent` declares lazy, and pin the metadata-admin surface that cannot be freed
+
+ `AppContent` declares four surfaces with `lazy()` that the emitted bundle loaded on
+ every console page load anyway. Measured on `b98352a15` from
+ `apps/console/dist/eager-closure.json` and the emitted chunks' own module lists —
+ not from a source-level search, which cannot see the mechanism at all:
+
+ | chunk | gz, eager before | after |
+ |---|---|---|
+ | `metadata-admin` | 172,651 B | unchanged — pinned, with the module list that proves it |
+ | `MarketplacePackagePage` | 7,647 B | lazy |
+ | `MarketplaceInstalledPage` | 1,836 B | lazy |
+ | `MarketplacePage` | already lazy | lazy |
+
+ Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page:
+ rolldown had parked `components/SuggestedBindingsPanel.tsx` (statically imported by the
+ eager `views/studio-design/StudioDesignSurface.tsx`) in the first, and
+ `console/marketplace/InstalledListWidget.tsx` (bare-imported by the package barrel for
+ its SDUI registration) in the second. An `app-shell-eager-leaves` group in
+ `apps/console/vite.config.ts` isolates those co-tenants so the three declared-lazy pages
+ chunk by their own dynamic-only reachability. The console eager closure moves
+ 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three
+ per-chunk ceilings unmoved.
+
+ `metadata-admin` is NOT freed and is now pinned in
+ `DECLARED_LAZY_VIEWS_STILL_EAGER` with the reason: it is statically imported by the
+ package barrel and by `services/builtinComponents.tsx`, which registers
+ `MetadataDirectoryPage` and `MetadataResourceRouter` by value, and it performs five
+ load-bearing top-level registrations. Freeing it would change what
+ `registerAppComponent` accepts and what the barrel re-exports — a published-contract
+ decision, not a bundling one.
+- 4704aa4: A form no longer ends on a screen asserting both a failure and a success (objectui#7252).
+
+ A refused submit raised an error toast that nothing ever retired, so when the user
+ fixed the input and submitted again the confirmation of that second attempt appeared
+ *beside* the refusal of the first — a wizard's last step showing "Invalid project
+ status transition." and "Your new project is ready…" at the same time.
+
+ Every outcome toast a form raises now travels under one stable per-form id, so the
+ later outcome supersedes the earlier one instead of stacking beside it:
+
+ - `@object-ui/components`' form renderer publishes its three outcome toasts (the
+ field-level rejection, an `onAction` error, and a rejected write) under that id, and
+ retires the previous attempt's toast in the same place it already cleared the
+ previous attempt's in-form banner. This is what fixes the reported wizard flow: the
+ refusal comes from this renderer while the success toast is raised by the host
+ (`WizardForm` / `ObjectForm`), so no single raiser could supersede the other before.
+ - the console's own `FormPage` publishes its confirmation and its submit failure under
+ one id, for the same reason.
+
+ Toast durations are unchanged — this is about supersession, not lifetime. The
+ objectui#4190 arm is deliberately excluded: a refused redirect *destination* still gets
+ its own toast, because the write succeeded and that refusal has to stay readable beside
+ the confirmation it qualifies.
+- d4c6a86: The console chrome's Home affordances follow the product's declared landing (objectui#7256).
+
+ `/` has honored `app.isDefault` since the hardcoded `PREFERRED_APPS = ['cloud_control']`
+ redirect was retired; the chrome had not. The top-bar logo, the sidebar's Home row, the
+ mobile sheet's Home row and the app-switcher's Home entry each named `/home` literally —
+ the ENVIRONMENT layer's launcher (ADR-0075). So a deployment that declares a landing
+ offered the customer two homes in two voices, and one click on the logo left the declared
+ one.
+
+ On cloud's control plane that second home is actively wrong: its "Build an app" / "Start
+ from a template" cards are environment-side actions that cannot work from the control
+ plane, and its "Your apps" tiles are the control plane's own internal management apps.
+
+ - `@object-ui/app-shell` adds `resolveDeclaredHomePath()` — the one reader of the
+ declaration — plus `useHomePath()`, which the four chrome sites now consume. The signal
+ is the App metadata the server already sends: no hostname sniff, and no product name
+ baked into the shared bundle.
+ - `@object-ui/console`'s `/` resolver is unchanged, and a behavioural matrix now pins its
+ answer equal to the chrome's for every declared app list, so the post-login landing and
+ the logo cannot drift apart.
+
+ Deployments that declare no landing are unaffected: every Home affordance still resolves
+ to `/home`. The "this app is gone" recovery redirects in `AppContent` / `ConsoleShell` are
+ deliberately untouched — they are error paths, not Home affordances.
+- ef1ffc2: `MetadataHmrReloader` stops flooding a production-posture deployment with
+ `GET /api/v1/dev/metadata-events` 404s (objectui#7257).
+
+ The dev-only HMR component subscribes via `EventSource`, gated on
+ `import.meta.env.DEV`. That gate is not airtight against every rig: a
+ "prod-like" build/serve setup that forces `NODE_ENV=development` for the
+ *build tooling* while running the server itself in production posture can
+ bake `DEV === true` into the shipped bundle even though the server never
+ mounts the dev route there — and the old reconnect loop treated every closed
+ connection as transient, retrying on a fixed `reconnectDelayMs` (2s)
+ forever. On an env host that is ~30 requests/minute of 404s per open
+ record/list page, drowning out the legitimate `sys_inbox_message` /
+ `sys_notification_receipt` polling in the same console.
+
+ The first `connect()` attempt now doubles as the real capability probe: if
+ the stream closes before it ever reaches `open`, the component gives up for
+ good instead of retrying (and specifically not a longer interval either —
+ that would still spam 404s, just slower). A stream that DID open at least
+ once and later drops — a real dev-server restart or network blip — keeps
+ reconnecting exactly as before.
+
+ No production-side replacement is introduced here: this component's only
+ job is turning "a metadata file changed on disk" into a full reload, and
+ production deployments have no such file-system event to watch. The
+ separate Studio-left-nav-doesn't-refresh caching issue does not go through
+ this SSE stream and needs its own fix.
+- 9b9af8d: The Approvals Inbox stops remounting every row on every render.
+
+ `ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions`
+ inside its own component body. React identifies a component by the identity of
+ its function, so each render produced three brand-new component *types* and React
+ unmounted and remounted every row's subtree instead of updating it — and the page
+ holds its clock in state and ticks it every 60s, so this fired on a timer whether
+ or not anyone was touching the page (objectui#5348).
+
+ Two consequences were reproduced against `origin/main` before the fix, in
+ `apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`:
+
+ - **Transient subtree state is discarded.** Focus placed on a row's Approve
+ button moved to `` on the next clock tick.
+ - **Input is silently swallowed.** A pointer sequence that spans a re-render —
+ press, tick, release — left the confirmation dialog unopened: the captured node
+ had been replaced, so React's delegated listener never saw the click. This is
+ the failure objectui#5211 hit and worked around at its call site
+ (`Unable to find role="alertdialog"`).
+
+ The three cells are now at module scope beside `StatusBadge`, which was moved
+ there for the same reason and already carries the explanation. Everything they
+ closed over is passed in: `RequestCell` and `InlineActions` take the page's
+ scoped translator, and `RecordCell` takes `href: string | null` — one prop rather
+ than two, so the objectui#5211 readable/unreadable decision and the URL cannot be
+ handed in disagreeing with each other.
+
+ The verification asserts the consequence, not the placement. A test that checks
+ the three functions now sit at module scope stays green for a refactor that moves
+ them and introduces a fourth inline component beside them; these cases compare
+ DOM-node identity for all three cells across a clock tick, which no remount can
+ pass, and re-drive the swallowed click.
+
+ That guard is load-bearing because lint cannot supply one here.
+ `react-hooks/static-components` exists for exactly this class and is `error` in
+ this repo via the plugin's recommended set, yet it reports nothing on this page:
+ measured on `origin/main`, an arrow-form inner component injected into
+ `ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same
+ shape in a ten-line file produced two. The rule's analysis bails out on this
+ component, which is how three of them shipped.
+- 9180fd1: Approvals inbox: the queue rows and the amount sort now honour each request
+ object's own `hidden: true` field declaration (objectui#6020).
+
+ The `hidden: true` trim added for the drawer summary card reached only the
+ drawer — the desktop queue row, the mobile card and the amount comparator
+ still read the field, so an amount an app author declared hidden rendered
+ inline in the queue and ordered the list, which leaked its relative magnitude
+ even to a viewer who never saw the figure.
+
+ The queue spans many objects, so the trim is a per-object lookup and every row
+ is answered about its own object; a row left with no renderable amount now
+ sorts with the other amount-less rows. `hidden` stays a UI contract
+ (objectstack#10749) and the filter still fails open: an unanswered or failed
+ metadata read renders today's figure.
+- 7c0e417: The Approvals Inbox no longer shows a business approver the submitted record's raw
+ row JSON.
+
+ The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no
+ principal check of any kind — so every approver could expand (and one-click copy) the
+ complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`,
+ `organization_id`, bare lookup ids, and **the fields the object's metadata declares
+ `hidden: true`**. Reported from a live EHR deployment on 17.1.0
+ (objectstack-ai/objectstack#10734), where that declaration is a patient-data control.
+ The app author had no legitimate lever to remove the panel — field `hidden`, view
+ columns, app navigation, permission sets and env vars are all ineffective against it —
+ so the remedies available in the field were patching the shipped bundle or injecting
+ CSS.
+
+ The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's
+ `studioEntry` module: `studio.access` is a declared platform-scope capability that a
+ tenant org owner does not hold by design, and it already reaches the browser in
+ `systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served,
+ computed or made authorable — no new config key, no new i18n copy, and the panel is
+ byte-for-byte unchanged for the platform operator it was written for. A business
+ approver keeps the structured record summary, the approval chain, the activity feed
+ and the decision actions; only the raw snapshot is gone.
+
+ The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from
+ `usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a
+ holder's button while the server still refuses the write is the worse outcome for an
+ action. This panel has the opposite stake, since the measured defect is a non-holder
+ seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend
+ predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200`
+ with no `systemPermissions` at all, and a reported empty array. A deployment whose
+ permission layer just failed must not be the one that leaks the snapshot.
+
+ `ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the
+ acceptance condition is that something does *not* render — which an empty render
+ reproduces perfectly — every denial case also asserts the drawer it denies inside, and
+ the `studio.access` case drives the same fixture through the same helper and finds the
+ panel. `created_by` and `organization_id` are the witnesses: both are in the page's
+ `PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can
+ reach the DOM only through the raw panel. Ablating the gate (restoring the bare
+ `payload != null` condition) turns the three denial cases red on exactly that
+ assertion and leaves the holder case green.
+
+ Out of scope, tracked separately: trimming the summary by object metadata, and the
+ server-side residual that sends the unfiltered snapshot to the client at all.
+- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop
+ clipping their tail steps.
+
+ Both occurrences were a single non-wrapping flex row whose steps were each
+ `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable
+ items, so the bar's intrinsic width grew without bound with step count and
+ label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK
+ step names measured **1070px inside a 527px container** (objectui#5554).
+
+ The two hosts failed differently, and neither failure was recoverable by the
+ reader:
+
+ - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not
+ scrollable, so the nearest scroller was the drawer *panel*. Reaching steps
+ 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the
+ record card, the activity timeline and the action buttons off-screen and left
+ a near-blank panel.
+ - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one
+ carried `overflow-x-auto`, so it scrolled itself rather than its container.
+ Better, but the tail steps still sat behind a scroll gesture with no visible
+ affordance.
+
+ In both, readers took the clipped bar for the end of the data; the reporting
+ customer acceptance tester said so verbatim. Widening the window does not help:
+ the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000.
+
+ Both now render as a column: one row per step, a badge-and-rail gutter, and a
+ label that may wrap. Width is capped by the container at every step count and
+ every label length, which also suits both hosts' tall-and-narrow aspect. The
+ rail segment below each step keeps the tint rule the horizontal connector used
+ — it is coloured by the step it leads *into*.
+
+ **Always vertical, with no step-count or measured-width threshold**, because
+ the overflow is driven by intrinsic content width (labels x count), not by
+ count alone: three 16-character CJK labels already crowd a 527px drawer, so any
+ count threshold picks a cutoff that is wrong for some real flow, and a measured
+ one reintroduces a viewport-dependent branch. The card's requirement is a fix
+ that cannot break at an untested viewport or flow length, and a layout with no
+ breakpoint and no measurement is the form that satisfies it. Horizontal-with-
+ scroll was ruled out for both occurrences: it leaves steps behind a gesture.
+
+ Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and
+ `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is
+ green against the broken code too — every step was always in the DOM, and the
+ clipping was layout — so the suites assert the property the defect names
+ instead: no row is `shrink-0`, every label is `min-w-0` and none is
+ `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no
+ axis, overflow or width-pinning class carries a breakpoint prefix (so there is
+ no viewport with untested behaviour). The reported failing regime is exercised
+ directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins
+ that the layout classes are byte-identical across all four, so no count
+ threshold can put some other flow length back on the old path.
+
+ The two steppers are kept identical by hand rather than extracted to a shared
+ component: they live in different packages, and deduplicating them is a
+ refactor with its own surface. Filed separately.
+- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424)
+
+ Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no
+ deprecation window, and the protocol-17 session face emits no `roles` key at
+ all. Three client sites still read it:
+
+ - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's
+ Approvals tab and Home's To-do card. It read nothing else, so it sent **no
+ `role:` identity at all**: an approval addressed to a position rather than to
+ a person matched nothing and vanished from all three surfaces, silently.
+ - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the
+ Approve/Reject enablement. It also splits the scalar `user.role`, so it
+ degraded rather than dying: it still yielded `role:user` while dropping every
+ business position name (`manager`, `finance_approver`, …).
+ - **`AppContent`'s expression user** — forwarded a `roles` key that was always
+ `undefined` into every CEL predicate context. Removed; `positions` and
+ `isPlatformAdmin` were already forwarded correctly beside it.
+
+ The retired spelling is **not** kept as a fallback — pairing the two is what
+ ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the
+ declaration.
+
+ `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately
+ untouched: it is a semantics decision, not a rename, and is deferred to a
+ maintainer ruling.
+- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or
+ `/auth/me/localization` twice (objectui#5544).
+
+ Two pairs of boot callers were racing each other for the same URL, with no shared
+ provider between them, so no guard inside either component could see the other:
+
+ - `GET /api/v1/runtime/config` — the pre-React branding script inlined in
+ `apps/console/index.html` (it runs during HTML parse so the tab title and
+ favicon are the operator's before the bundle is fetched) and
+ `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the
+ expensive one: the console `await`s `initRuntimeConfig()` before
+ `createRoot().render()`, so the duplicate sat on the critical path to first
+ paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed
+ boot concurrency further past the server's pool knee.
+ - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true
+ first visit and `LocalizationFetchProvider` on every boot. The seed keeps
+ running past its 500 ms race by design and the provider mounts the moment that
+ race resolves, so on a first visit the two overlap. Measured ×2 on staging.
+
+ `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET
+ while one is already in flight join that request instead of starting another. It
+ shares the in-flight promise and nothing else — the entry is deleted the instant
+ the request settles, so there is no cache, no TTL and no stale window, and a
+ caller arriving after settle fetches fresh exactly as before. Rejections fan out
+ to every sharer with the status intact (`LocalizationFetchProvider`'s retry
+ policy still sees its own 503), each caller receives its own copy of the parsed
+ body, and only GETs are eligible — a non-GET is refused rather than quietly
+ rewritten.
+
+ Requests that differ in credentials mode or headers keep separate identities, so
+ the console's two deliberate `auth/get-session` calls — one Bearer-only with the
+ cookie omitted to detect a stale token, then one through the cookie — stay two
+ requests. Collapsing those would have destroyed the signal the first one exists
+ to read.
+
+ No component receives anything different: same payloads, same errors, one fewer
+ round trip.
+- 631d81d: fix(console): paint a boot indicator during the pre-React white frame
+
+ The console's `index.html` shipped an empty ``, so a hard
+ navigation showed a pure-white page until the module graph had downloaded,
+ evaluated and resolved the two round trips `main.tsx` awaits before
+ `createRoot().render()`. Measured on this repository's production build with
+ Playwright + CDP screencast (frames classified pixel-by-pixel, corroborated by
+ the Paint Timing API): 1224-2016 ms of pure white on an unthrottled localhost,
+ 2289-2297 ms at 20 Mbps/80 ms RTT, and 5808-6167 ms on a Fast-4G profile.
+
+ The document now carries the indicator itself — an inline style block and the
+ same gradient tile `LoadingScreen` opens with — so it paints from the HTML
+ parser, before the first chunk is requested. It is removed on React's first
+ commit into `#root`, so it never doubles up with `LoadingScreen`, which
+ continues to own the init screen and its ten-language copy.
+- 71ee495: The two form CONTAINER contracts now have ONE declaration each, derived from
+ `@objectstack/spec`, and the console reads them instead of its own copies.
+
+ objectui#5542 converged the LEAF of this contract — the field spec — and left the
+ two containers above it untouched, because converging them was a bigger call than a
+ mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared
+ twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once
+ in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean
+ subset — these two had **already drifted, in both directions**, so neither copy was a
+ subset of the other and there were two live answers to "what may an author write":
+
+ - `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`;
+ the console declared none of them. The console's `columns` admitted the string arm
+ (`'1' | '2' | '3' | '4'`); app-shell's took numbers only.
+ - `FormViewSpec` — the console declared `label` / `groups` / `sharing` /
+ `submitBehavior`; app-shell stopped at `type` plus `sections`.
+
+ The drift is decided by asking the **contract**, not by picking a side. `columns`
+ does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])`
+ with the four numeric literals, folded to a number by its own transform), so
+ app-shell's numbers-only declaration was rejecting metadata the platform accepts —
+ objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is
+ the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured
+ against the installed `@objectstack/spec` 17.0.0), because a form config is titled,
+ not labelled. The value that read actually finds is the VIEW's identity label, which
+ arrives on the `ExpandedViewItem` envelope or beside the config on a flattened
+ runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to
+ the body it unwraps, rather than smuggled onto the form contract.
+
+ Both types are therefore **derived from the spec's own `FormSection` / `FormView`
+ with named narrowings** — the repo's sanctioned form for a spec-shaped local type
+ (`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the
+ two layers agree on comes from the spec and cannot fall behind it; the four positions
+ where this layer is deliberately narrower are each named in an `Omit` list and
+ restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving
+ it would silently re-open #5542), and `label` / `description` / `visibleWhen` /
+ `visibleOn` keep the shapes this repo's renderers and evaluators actually consume
+ rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s
+ `submitBehavior` union — previously hand-written under the comment "Mirrors the spec
+ FormView.submitBehavior union" — is now read back off the shared type, making the
+ mirror structural. `@object-ui/app-shell` re-exports both names from its package root
+ (type-only, erased at build — nothing is added to the bundle), because a type that
+ cannot be imported is a type that gets retyped.
+
+ The pins are what make future drift loud, and each half is pinned on both sides.
+ `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the
+ non-narrowed half of each type against the spec's own symbol, so re-hand-writing
+ either declaration fails `type-check` the day the spec moves rather than years later
+ when someone reads two files side by side — and the console's pins read both types
+ back out of the **exported** `buildSections` signature rather than naming them, so a
+ re-inlined local copy fails even if it agrees on every key on the day it is written.
+ Their liveness controls are what stop them being phantom checks: the removed copies
+ are pinned NOT equal to the shared types (proving the `Equal` helper still
+ discriminates), the renderer's honoured `RenderableSection` is pinned not equal
+ either (so the authored-document and honoured-row types cannot be collapsed again),
+ and an undeclared key is still rejected (so the derivation smuggled in no index
+ signature or `any`). Every narrowing carries a matching negative pin, so "derived"
+ cannot quietly become "widened to whatever the spec says".
+
+ Behaviour is unchanged — the runtime always accepted these keys. The vitest halves
+ prove it: a section spelling its column count as the string `'3'` lays out identically
+ to the numeric `3` on both sides, and a section carrying the keys only one side used
+ to declare builds the same rows.
+- cebdfe7: The form-field authoring contract now has ONE declaration, and the console reads it
+ instead of its own copy.
+
+ objectui#5040 was not a missing key. It was that **two hand-written descriptions of
+ one contract drifted**, and nothing could notice, because each was only ever checked
+ against itself. PR #5537 converged the two app-shell descriptions into
+ `views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`:
+ `FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same
+ name, in a different package — so the same failure mode stayed fully available.
+
+ Measured key by key before choosing a route, because the two honest outcomes are
+ "same contract, import it" and "genuinely narrower layer, rename it and pin the
+ subset". The console's copy was a strict subset — 9 of the shared type's 26 keys,
+ every one identical in type, none console-only — and it sat in a position that
+ describes an **authored document**: `FormSectionSpec.fields`, read straight off the
+ `/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both
+ files even spell the same six-member `type` union and call the element type
+ `FormFieldSpec`). The narrow, renderer-honoured shape is a different type that
+ already exists in that file, `RenderableField`. So this was one contract described
+ twice, and the console's description was wrong about the document: legal metadata —
+ `visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`,
+ and ten more keys — was undeclared there. That is #5040's own symptom, "the type
+ rejects the configuration the runtime accepts", which no runtime test can see.
+
+ `@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root
+ (type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx`
+ imports it and deletes the local declaration. Reachability is the load-bearing half:
+ a type that cannot be imported is a type that gets retyped, and retyped copies drift.
+ `form-spec.ts` itself is untouched.
+
+ `FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the
+ field-spec type back out of the **exported** `buildSections` signature rather than
+ naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even
+ if the copy agrees on every key on the day it is written — which is exactly what did
+ not happen to the copy this change removes. Its liveness controls are what stop it
+ being a phantom check: the removed nine-key shape is pinned NOT equal to the shared
+ type (so the `Equal` helper is proven to still discriminate), `RenderableField` is
+ pinned not equal to it either (so the honoured-row and authored-document types cannot
+ be collapsed again), and an undeclared key is still rejected (so the import did not
+ smuggle in an index signature). Behaviour is unchanged: the runtime always accepted
+ these keys, and the vitest half proves the same rows are built.
+- 07cd685: The console's form routes no longer seed a RUNTIME `defaultValue` into the
+ control, so the server-side resolution the declaration asks for actually happens
+ (objectui#5727).
+
+ `readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared
+ default unconditionally. A `defaultValue` may be a literal, or an *instruction*
+ the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`,
+ `current_user`) or a CEL Expression envelope. Seeding one of those literally put
+ the text `NOW()` into a datetime input on both `/forms/:name` and the public
+ `/f/:slug` route, and submitting it sent that string as the field's value —
+ which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved
+ the declared default and the column stored the token text instead of a timestamp.
+
+ The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same
+ published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its
+ seeding with, and the one this renderer already reads once removed (through
+ `isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default
+ leaves the key ABSENT rather than empty, because absent is precisely the case the
+ engine resolves.
+
+ Nothing else about the prefill precedence moves: a literal default still seeds, a
+ stored record value still wins over a default, and an explicit `prefill_
=`
+ param still wins over both — including for a field whose default is a runtime
+ token, since a value a producer supplies is not a declaration awaiting
+ resolution.
+- b63a9a3: The console's standalone form renderer now evaluates conditional field visibility.
+
+ `apps/console/src/components/FormPage.tsx` is a **second, independent form renderer**
+ — its own `buildSections`, its own JSX — and it serves both the public
+ `/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of
+ the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` /
+ `visibleOn` *read* inside that file returned zero. So a field an author conditioned on
+ `record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec`
+ normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both
+ authors and honours — rendered unconditionally on both routes. Fail-open and silent:
+ the author saw the field always, with no diagnostic.
+
+ objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different
+ chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form`
+ `sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx`
+ is on that chain at no point, and #2212's regression pin lives with the chain it fixed,
+ so nothing in the suite could see this copy. One contract, two implementations, each
+ only ever checked against itself.
+
+ The wiring is **#2212's ruling applied verbatim** rather than a second predicate
+ semantics invented for this renderer, because two form renderers disagreeing about what
+ `visibleWhen` *means* would be a worse defect than one renderer ignoring it. The
+ predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`,
+ `evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and
+ `{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*`
+ = the stored record an edit form started from), and the fail-open-but-loud behaviour on
+ an unevaluable predicate are the shared ones by construction. Resolution is
+ canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers:
+ `sectionFields.ts` and app-shell's `readVisibility`.
+
+ Two things deliberately did **not** change. A field hidden by its predicate still
+ submits its value — conditional visibility is a rendering rule in both renderers, and
+ making it a submit-payload rule would be a new contract decided once for both, not
+ invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain:
+ the second-renderer question is real, but it belongs with the #5596 convergence track,
+ not with a predicate that is dead today.
+
+ `FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this*
+ renderer on purpose — a pin that cannot see the second copy is how the first gap
+ survived. With the fix reverted and the pin in place the suite reports
+ `11 failed | 1 passed (12)`; the one green is the control that has to be green (a field
+ with no predicate still renders), without which every "the field is absent" assertion
+ would be equally satisfied by a renderer that draws nothing at all.
+- fb934fb: The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to.
+
+ `AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the
+ app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same
+ three statically for the platform portal at `/docs` (ADR-0046 section 6), so all
+ of them sat in the eager graph regardless and the `import()` moved nothing —
+ three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build`
+ (objectui#5467). A static import on either side silently defeats the split for
+ both, and the only signal is a build warning that fails nothing.
+
+ `App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`,
+ matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even
+ though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot,
+ so nothing imported `DocsIndex` dynamically, but left static it alone would keep
+ `DocShell`, `use-book-data` and `book-nav` eager and the portal would only
+ half-leave the closure.
+
+ Measured on this branch with the `dist/eager-closure.json` gauge added by
+ objectui#5324, both builds exiting 0:
+
+ | | before | after |
+ |---|---|---|
+ | `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 |
+ | eager closure, gzipped | 3,881,609 B | 3,870,058 B |
+ | eager chunks | 58 | 52 |
+
+ Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz),
+ `CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B),
+ `componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown
+ folds into the entry chunk rather than dropping — which is why the entry chunk
+ grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by
+ 11,551 B. The entry stays far under that budget's 350 KB line, and the eager
+ closure is the number a page load actually pays.
+
+ What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this
+ looked like a bigger win than it is. Three eager chunks import it statically,
+ and only one of them was this portal: `plugin-chatbot` reaches it directly, and
+ `packages/fields`' `MarkdownContent` — lazy in source — is folded into the
+ eagerly imported `ui-components` chunk by the `advancedChunks` group that claims
+ every `packages/fields` module. That is objectui#5325's mechanism, not this
+ card's, and it is why the saving here is 0.30% rather than 4%.
+- 8549453: The console's pre-boot branding script now resolves its server origin from
+ `VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of
+ `window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set
+ (objectui#5660).
+
+ The two callers of `GET /api/v1/runtime/config` share one in-flight request, and
+ that sharing keys on the FULL URL. So the origin was half of the contract: the
+ inline script in `index.html` read one spelling and `src/main.tsx` read another, and
+ whenever they disagreed the two never found each other. A same-origin production
+ build hid it completely — both spellings collapse to `''` — so the split surfaced
+ only in a dev pointing the console at a separate server, where it cost two requests
+ to two different servers and let pre-boot branding paint from the wrong one.
+
+ The pre-boot fetch is kept, not deleted: it is the request the module side JOINS.
+ `sharedGetJson()` can only hand an earlier request to a later caller, and this
+ script is the earlier one by construction — it runs during HTML parse, before the
+ bundle is fetched, while `initRuntimeConfig()` is awaited before
+ `createRoot().render()`. Deleting it would not remove a request; it would move the
+ single remaining one later, onto the critical path to first paint, and leave the
+ page's empty `` and favicon unbranded until React mounts.
+
+ Vite substitutes its HTML env token only when the variable is set and leaves it
+ verbatim otherwise, so the unset case is read as same-origin `''` rather than
+ allowed to reach the URL as a path segment.
+- 7493bff: `registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing.
+
+ The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a
+ `Suspense` fallback — naming the same barrel the line above it imports
+ statically for `registerAppComponent`, and the same barrel `App.tsx` pulls
+ `BuilderLanding` from to render the standalone `/studio` landing full-screen.
+ Either reason alone makes the `import()` unable to move a module into another
+ chunk (objectui#5486).
+
+ **This moves no modules and is not a bundle improvement.** `BuilderLanding` was
+ already in the eager graph via `App.tsx` and still is. Measured on
+ `dist/eager-closure.json`, both builds exiting 0: the eager closure holds the
+ same 52 chunks with the same names, and the only difference is 130 B gzipped
+ (413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback
+ text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure,
+ because nothing could.
+
+ What it does fix is honesty. The old code told every reader the builder was
+ deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every
+ console build — the console's count of those drops from 44 to 43, with the 43
+ remaining ones all belonging to the `packages/fields` barrel (objectui#5325).
+ A permanent warning that fails nothing is how a team learns to skim past build
+ warnings, and a decorative `lazy()` is how the next reader learns something
+ false about the chunk graph.
+
+ The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx`
+ lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in
+ statically, and is genuinely deferred; it is untouched. Making the *builder*
+ genuinely lazy would mean taking `App.tsx` off the static import too, changing
+ how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind
+ the barrel — a separate measured card, not folded in here.
+- d8afbe5: `FormPage` — the console's own form renderer, serving both the public `/f/:slug`
+ route and the internal `/forms/:name` one — now honours the three conditional-rule
+ surfaces it still dropped after objectui#5594: section-level `visibleWhen` /
+ `visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` /
+ `requiredWhen` (objectui#5627).
+
+ This is the second form renderer in the repo, and it honoured exactly one of the
+ four surfaces the sibling chain does. A section an author conditioned away rendered
+ in full — heading and every control — on both routes including the anonymous one.
+ The object-level half was worse than fail-open hiding: `readonly` was whatever the
+ static flag said, so a field a `readonlyWhen` should have locked stayed editable and
+ paired with the server's fail-closed unbound-scope behaviour into "the user edits,
+ the save reports success, and the value never lands".
+
+ Both halves evaluate through the SHARED machinery rather than a fourth consumer-side
+ copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three
+ field rules — which brings the settled rulings with it, including the `serverOwnedValue`
+ carve-out that keeps a create form from requiring a producer-owned control (#4069 /
+ #4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first
+ `visibleWhen ?? visibleOn` read every sibling reader spells.
+
+ Visibility stays a RENDERING rule at both granularities: a hidden section's fields
+ still submit their values, exactly as a hidden field's have since #5594.
+- cdda37a: `buildSections` now honours a FormView field's `maxLength` override instead of always
+ taking the object's ceiling (objectui#5595).
+
+ The function merges a form's field overrides with the target object's field definitions,
+ and its own docstring states the rule: *"Field-level FormField overrides take precedence
+ over object defaults."* Every key in the loop is built that way — `override.label ??
+ def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder`
+ — except one, which read `def.maxLength` unconditionally. So an author who set a tighter
+ per-form limit (a short public intake form over a column whose object-level ceiling is
+ generous) got the generous one.
+
+ The failure was silent in the worst direction: no diagnostic, no warning, and the form
+ still submits, so the symptom is a value the author believed the input refused being
+ accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two
+ `maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"`
+ arm.
+
+ `override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys,
+ so an explicitly declared `0` stays a value the author wrote rather than falling through
+ to the column's ceiling. This narrows only what the input allows; the object's storage
+ ceiling still decides at submit time, so nothing that was accepted before is now rejected
+ anywhere but at the keyboard.
+
+ Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all
+ until objectui#5542, so no one typing a spec in this app could write the override in the
+ first place, and the inert merge branch was never exercised. #5542 converged that type
+ onto the shared app-shell declaration, which does declare the key — making the gap
+ expressible, and therefore findable.
+
+ The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in
+ `FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than
+ assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered
+ evidence for this fix, and it is what made the gap findable in the first place, so it
+ keeps its place and names the honoured answer.
+- 60b2211: Console form pages no longer submit a cleared server-owned field as a blank.
+
+ A field whose declared `defaultValue` is an instruction the server resolves per
+ insert (a `NOW()` / `current_user` token, or a CEL expression envelope) opens
+ with an empty control on a create form, and its key stays out of the payload
+ while nothing touches it. But a submitter who typed into that control and then
+ cleared it put the key back holding `''` — and `ObjectQL.applyFieldDefaults`
+ resolves a declared default only for a field arriving absent or null, so the
+ blank was stored and the declaration silently defeated.
+
+ Such a key is now dropped from a CREATE submit on both the internal
+ `/forms/:name` and the anonymous `/f/:slug` route. A blank cleared from a field
+ with no runtime default — or with a static one — is still submitted, because
+ that is the user removing a value; and an edit submit is untouched, where a
+ cleared column is a deliberate removal.
+- 343c598: The active organization id is now stored per user, and a change of session user drops the
+ previous user's client state wholesale (objectui#5664).
+
+ `auth-active-organization-id` was a single un-namespaced `localStorage` key while its
+ siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`,
+ `flow-palette-recents:u:`). On a browser handed from one account to another — a shared
+ machine, a kiosk, a handover, a support session — the arriving user's console read the
+ PREVIOUS user's organization id. The header workspace chip rendered the previous user's
+ workspace for a user whose `organization/list` was empty, and the consequence past the
+ cosmetics is the one worth stating: the polluted org context suppressed
+ `RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so
+ a brand-new user on that browser silently never got the new-user flow at all.
+
+ Nothing about row visibility rode on this. With the stale id the server answers
+ `403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero
+ environments; the damage was entirely in what the client believed about itself.
+
+ Three changes, and the third is the one that closes the class rather than the instance:
+
+ - The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention
+ its siblings already use.
+ - It can no longer be written un-namespaced at all. Where no session user is known yet the
+ value lives in memory for that page-load only — a namespacing that kept a bare-key
+ fallback would re-open the defect the first time a write happened before the user id
+ resolved.
+ - **A change of session user drops the previous user's client state wholesale.** This is an
+ allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so
+ the NEXT storage key someone adds without a `:u:` scope is covered before it is written.
+ Only device-scoped entries survive: the arriving session's own bearer token, the pointer
+ recording whose state the browser holds, and the UI theme.
+
+ Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a
+ non-null `localStorage` read and falls back to the in-memory value, and the memory value is
+ nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge
+ too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then-
+ sign-in path that never reloads the page.
+
+ Existing browsers are not migrated. A value sitting under the retired bare key is
+ unattributable — nothing recorded whose org id it is — so migrating it is precisely the
+ defect it would be migrating away from, and it is deleted instead. A signed-in user loses
+ nothing durable: the active organization is a server-owned fact that
+ `AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no
+ active org is held, including the ADR-0081 single-membership repair. One boot re-supplies
+ it; users with no organization land on the guided first-run flow, which is the outcome this
+ card is about.
+
+ `apps/console`'s pre-render auth preflight purges every spelling of the active-org key —
+ the retired bare one and each `:u:` scope — when it finds a dead bearer token, and
+ deliberately leaves the session-user pointer in place so the next sign-in can still tell
+ that the browser changed hands.
+- 9093de8: Approvals drawer: the business summary card no longer renders a field the object
+ declares `hidden: true`.
+
+ `payloadSummary` built the card from the request's `payload_json` snapshot behind
+ five filters (system keys, the lead amount key, null/object/empty values,
+ unresolved opaque ids, a six-field cut) and no field-visibility filter, so a
+ hidden field that survived to the first six survivors rendered in the card,
+ labelled. The drawer now reads the open request's object metadata and drops the
+ declared-hidden keys before the six-field cut, so the next business field is
+ promoted into the freed slot rather than the card silently shrinking. The lead
+ amount figure at the top of the same card takes the same trim.
+
+ Per the platform ruling, `hidden: true` is a UI-only contract and `internal: true`
+ is the serialization primitive, so this is the UI enforcing the only contract
+ `hidden` has — not a client-side compensation. Field-level security is unchanged
+ and remains the server's answer. The metadata read is the same cached
+ `GET /meta/object/:name` the record form already performs, once per object per
+ page visit, and an unanswered read leaves the card exactly as it renders today.
+- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in
+ (objectui#5522).
+
+ `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from
+ `apps/console/.env.production` — is what the hosted SaaS console and the on-premises /
+ air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the
+ bundle as a frozen object literal, so the DSN committed there was a live third-party
+ telemetry endpoint compiled into artifacts that land inside customer networks. It could
+ not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off
+ that same frozen literal, so on a shipped bundle it is `undefined` forever and editing
+ env vars on the deployed host does nothing. An air-gapped deployment was measured
+ sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by
+ the customer.
+
+ - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`,
+ `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never
+ imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched.
+ - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so
+ IP address and User-Agent are never the inherited default of a build that did not ask
+ for them.
+ - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not
+ send. The direction is deliberately inverted from the usual — an unreported error is
+ recoverable, PII leaving an air-gapped deployment is not.
+
+ **Action required for deployments that want error reporting** (the hosted SaaS/demo
+ console): inject `VITE_SENTRY_DSN` from your build environment, the same way
+ `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you
+ still want IP/User-Agent on events. Nothing else changes for builds that opt in.
+- e067173: Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains.
+
+ **The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`.
+
+ The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists.
+- 7d0143c: The Console now gates the `/studio/*` routes on the `studio.access` ENTRY
+ capability, not just on the backend's refusal of the writes behind them
+ (objectui#5519).
+
+ `/_console/studio/` rendered the full Studio pillar builder — Data /
+ Automations / Interfaces / Access, with Publish and Save draft — to any
+ authenticated principal who typed the URL, on deployments where the Studio nav
+ tile is deliberately absent and every metadata write is refused. A plain tenant
+ user was walked through the entire "new package" form and only refused at
+ submit (403). The lockdown criterion for that deployment shape is two-part — UI
+ entry hidden AND API refused — and only the API half was met; what stood on
+ this side was a write-level gate where an entry-level one belongs.
+
+ The whole `/studio` subtree now hangs off one route element that reads
+ `systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this
+ app already consumes) and admits only a principal whose LOADED set carries
+ `studio.access` — the capability declared as "Enter the Studio metadata-design
+ surfaces", which a tenant org owner does not hold by design. Everyone else is
+ sent to `/home` without the builder ever mounting.
+
+ The fail direction is deliberately inverted from this app's other capability
+ gates: those fail OPEN on an unknown answer because their bad outcome is a
+ holder losing a button, whereas a route gate's bad outcome is a non-holder
+ seeing the builder. So the loading window renders the console splash (never the
+ builder), an outright fetch failure renders the retryable error splash, and a
+ `200` that carries no `systemPermissions` at all is refused rather than waved
+ through. The server-side refusals are untouched.
+- Updated dependencies [0db4fb3]
+- Updated dependencies [4703651]
+- Updated dependencies [9e37d9b]
+- Updated dependencies [cf1d29e]
+- Updated dependencies [8d58f46]
+- Updated dependencies [0b1326d]
+- Updated dependencies [305205a]
+ - @object-ui/sdui-parser@17.7.0
+
## 17.6.0
### Minor Changes
diff --git a/apps/console/package.json b/apps/console/package.json
index eeb86d7b08..544cc1dc08 100644
--- a/apps/console/package.json
+++ b/apps/console/package.json
@@ -1,6 +1,6 @@
{
"name": "@object-ui/console",
- "version": "17.6.0",
+ "version": "17.7.0",
"description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.",
"license": "MIT",
"type": "module",
diff --git a/packages/app-shell/CHANGELOG.md b/packages/app-shell/CHANGELOG.md
index c3876b79ac..395a710288 100644
--- a/packages/app-shell/CHANGELOG.md
+++ b/packages/app-shell/CHANGELOG.md
@@ -1,5 +1,6113 @@
# @object-ui/app-shell — Changelog
+## 17.7.0
+
+### Minor Changes
+
+- 2a23000: One composer for makers — the built-in `ask` folds into `build` for authoring
+ principals (cloud#1674 maker convergence, Phase B).
+
+ Ruling: `build` is the higher-privilege admin agent and, since cloud#1673, a
+ strict data superset of `ask` — it answers records/aggregation/chart questions
+ with the same tools. So Ask/Build stop being peer modes for a maker:
+
+ - `surfaceAgent.ts` (the ONE ADR-0063 resolver) gains rule (3b): an `ask` want
+ upgrades to `build` for a principal with `manage_metadata` when the catalog
+ serves it — including over an app's explicit `defaultAgent: 'ask'` pin (the
+ pin serves the app's business users; the maker always gets the superset).
+ New `makerConvergedOnBuild` / `makerVisibleAgents` carry the same predicate
+ to the picker sites.
+ - ChatPane's agent launcher lists `makerVisibleAgents`: the built-in ask leaves
+ the list for makers (custom agents stay), computed inside ChatPane so every
+ host — full `/ai` page, Studio copilot, console dock — converges identically.
+ - A maker's bare `/ai/ask` redirects to `/ai/build`; `/ai/ask/:conversationId`
+ keeps rendering, so ask history stays readable.
+ - `ConversationsSidebar` gains `includeAskConversations`: the converged surface
+ lists BOTH built-in groups' threads whichever one is open (merging only ask
+ made the build history vanish the moment an ask thread was opened — measured
+ in-browser on the first pass).
+ - Console home shows the single Build entry for makers; non-authoring sessions
+ (the objectstack#8270 hosted posture) keep Ask AI, and every layer is a no-op
+ for them and for deployments without a build agent.
+ - `useCanAuthorMetadata` extracted from HomePage to `hooks/` so the CTAs and
+ the chat surfaces answer the same per-principal question.
+- 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the
+ dataset captions.
+
+ **The maker's start chips now promise only what ADR-0112 v1 builds
+ (cloud#1984).** Two of the five asked for automation the first version has no
+ flows or actions for — the ticket chip said 「状态流转」, the inventory chip said
+ 「低库存预警」 — and the measured behaviour was not a refusal but a silent
+ degrade: a status kanban and a low-stock view. The chip promised an alert and
+ delivered a page. All five are reworded in all ten packs (and in the call-site
+ `defaultValue` fallbacks, which are a second copy of the same strings) to ask
+ for objects, fields, views, pages, dashboards and sample data, keeping each a
+ real business scenario — the ticket chip now asks for a status field and a board
+ grouped by it, the inventory chip for a view that filters below the reorder
+ point. A note beside the keys says to revert when v2 re-adds flows.
+
+ **Five newer AI tools get their step labels (objectui#7481).** A zh conversation
+ read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」:
+ `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`,
+ `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are
+ newer than the pinned spec's tool registry, so they had no `chatbot.tool.*`
+ entry in any pack and fell through to the English title-caser.
+
+ **The assistant rail follows the thread when you send (objectui#7480).** The
+ rail and the full-page maker are the same component; what differs is width. A
+ reply that still ends on screen in the wide column runs two or three times
+ taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the
+ user types and the new bubble, the tool steps and the streaming answer all land
+ below the fold. Every send path now re-arms the lock — including the plan-card
+ "Build it" and 确认修改 approvals, whose own code comments already named this
+ miss. Message APPENDS deliberately do not, so a user reading back through the
+ thread mid-answer is never yanked to the bottom.
+
+ **Console toasts move off the assistant composer (objectui#7482).** 「客户更新
+ 成功」 sat on the ChatDock composer's send button and stayed there. One defect,
+ two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override
+ that predates ADR-0057 P3a — so a toast both covered the button and, because
+ sonner pauses a toast's dismiss timer while the pointer is inside the toaster
+ region, never got to run its 4s timer with a pointer resting on the composer
+ underneath. The override is gone; the console takes `ConsoleToaster`'s own
+ documented top-right anchor, and the 4s success duration is now pinned.
+
+ **Built-in aggregate captions follow the locale everywhere (objectui#7534).**
+ objectui#7258 taught `buildChartSeries()` to resolve a server-minted default
+ measure through the locale map, so a chart legend read `计数` while the table
+ beneath it, the KPI caption, the pivot header and the dataset preview still
+ printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()`
+ takes the same optional `builtinAggregateLabels`, resolving through the one
+ `resolveMeasureLabel` order, and the five call sites pass it. Omitting the
+ argument reproduces the previous output byte for byte, and an author-declared
+ measure still keeps its own label verbatim (objectui#4106).
+
+ **The activity feed stops asking for an object the environment does not have
+ (objectui#7476).** A tenant environment has no `sys_activity`, so every page
+ load issued a request that 404'd. Everything downstream was already correct —
+ the adapter memoizes the missing collection, its logger demotes the failure, the
+ feed retires as an ANSWER and the panel renders its earned empty state — so what
+ is left is the request itself, and `data-objectstack` states the rule for it:
+ the cure for a doomed request is not issuing it. New `useObjectPresence` reads
+ the object registry the shell loads for the nav anyway; only a registry that has
+ ANSWERED and lists other objects without this one skips the read. Every
+ uncertainty — no provider, empty registry, still loading, errored — reads as
+ before, because a wrong skip would cost a real deployment its feed.
+- d796c8d: Auto-derived related lists consume the field-level `relatedListFilter`
+ declaration — the list query AND-composed, the tab badge counting the same set
+ (objectui#4664).
+
+ A `lookup` / `master_detail` field may now declare `relatedListFilter`, a
+ canonical Query-DSL `FilterCondition` such as `{ status: { $ne: 'deleted' } }`
+ (`@objectstack/spec` 17.1.0 — objectstack#8704 / PR #8955). Until now this repo
+ accepted that key at every gate and consumed it nowhere: a record page's derived
+ related lists answered wider than the metadata asked, and the driving scenario —
+ soft-deleted child rows on auto-derived record pages — had no way to be
+ expressed at all.
+
+ What ships:
+
+ - `deriveRelatedLists` reads the key off the FK and carries it on the derived
+ descriptor; `RecordDetailView` forwards it into the page synthesizer, which
+ emits it onto the `record:related_list` node's **existing** `filter` key. That
+ key already had a read site (objectstack#7118): `RelatedList` ANDs it with
+ `{ [referenceField]: parentId }`. The declared filter is therefore an authored
+ constraint that may only NARROW this parent's children — never a replacement,
+ which would leak other parents' rows — and no second filter dialect appears
+ for derived pages.
+ - The **tab badge honours the same composed filter**. `page:tabs` reads the
+ `filter` off the `record:related_list` node it is badging and the count store
+ composes it with the parent scope through the same `mergeFilterNodes` sink the
+ row query uses, so the badge and the rows send one `$filter`. Badge-count
+ parity is normative in the spec key's own contract text; without this half the
+ feature would ship the defect it exists to prevent — a badge saying 7 above a
+ list showing 3.
+ - Counts cache per (object, relationship, parent, **scope**), so a filtered and
+ an unfiltered probe over the same relationship are separate entries rather
+ than one wrong number.
+
+ With no `relatedListFilter` declared, the synthesized node, the row query and
+ the badge probe are byte-identical to before. Consumption only — this change
+ adds no authoring UI for the filter.
+- 053fdc8: Console half of `ActionSchema.onSuccess` post-success navigation.
+
+ `@objectstack/spec` declares `onSuccess` as a closed strict object
+ `{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to `type: 'api'` and
+ `type: 'script'` — the two action types whose success event carries a server response.
+ Nothing in this renderer read it, so an action declaring the hop navigated nowhere: the
+ block fell into `ActionRunner`'s older `ActionDef.onSuccess` chained-callback channel,
+ was dispatched as an action, and failed inside `executeNavigation` with "No URL provided
+ for navigation action" — a red toast and no jump. The motivating report is a clone action
+ that leaves the user sitting on the record they cloned from.
+
+ `ActionRunner.handlePostExecution` now performs the declared hop through
+ `navigationHandler` — the same SPA seam every other navigator in that file uses, which
+ the console wires to react-router's `navigate`, so `openIn: 'self'` is a real in-place
+ route hop rather than a full-page load. `interpolateTarget` gains a `${result.*}` scope
+ alongside `${param.*}` and `${ctx.*}`, resolved against the handler's own return value
+ (the level `readActionPayload` reads, one below the action envelope) and supplied only by
+ this call site, so a target interpolated before its request still has no `result` to
+ name. `openIn` is read as the one member that changes the branch and no default is
+ written here — the spec materialises `.default('self')`, so parse output always carries a
+ resolved member — and the two `openIn` spellings stay apart: this reads
+ `onSuccess.openIn` (`'self' | 'newTab'`), never the top-level `type: 'url'` switch
+ (`'self' | 'new-tab'`), each of which spec refuses in the other's position.
+
+ The console's server-action wrapper gains the matching handler-return half: a handler may
+ now return `openIn: 'self'` next to its `redirectUrl` to ask for the same-tab jump, while
+ a `redirectUrl` **without** `openIn` keeps its shipped new-tab behaviour unchanged. When
+ an action declares an `onSuccess` block, the wrapper defers to the runner and only tidies
+ its pre-opened tab, so one navigation happens rather than two.
+
+ The pre-existing `ActionDef.onSuccess` chained-callback channel is unchanged. It is told
+ apart by the spec's own declaration — a non-array object whose `navigate` is a string —
+ and keeps running for every other shape.
+- 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's
+ ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a
+ list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user
+ dragging a column or cycling density was re-styling that view for the entire organization. Nothing
+ in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611,
+ v18) and is deliberately not built here — which is precisely why the write has to be gated rather
+ than narrowed: there is no second, private store for it to fall back to.
+
+ `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability
+ set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError`
+ (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the
+ `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method —
+ before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire.
+ It is on the write rather than on the toolbar button on purpose: withholding the affordance would
+ leave the method still accepting the call from anything else holding the adapter, whereas a gate on
+ the write is inherited by every caller, present and future.
+
+ `manage_metadata` is not a newly minted name. It is the capability this repo already treats as
+ metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself
+ refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very
+ same ADR-0005 metadata door, so this applies the authority the server is already applying instead of
+ inventing a parallel one.
+
+ **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating
+ ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a
+ client-side refusal on missing data cannot protect anything and can only break a permitted user. A
+ *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new
+ `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`.
+
+ The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure
+ into `console.error`, which for a debounced toggle whose UI has already moved would have left the
+ operator looking at a density they did not get; a denied write now raises a toast. And the "View
+ settings" popover — where density and field visibility are actually changed — now states the scope
+ before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who
+ uses this view."*, translated in all ten packs.
+- 26fbe09: Console telemetry can now be hard-disabled on an already-built artifact
+
+ `/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting`
+ (objectstack#11382), and the Console now reads it. The Sentry decision becomes a
+ conjunction of two independent grants — a DSN injected at **build** time AND a
+ positive permission from the **runtime** — so the single pre-built SPA that both
+ the hosted SaaS console and the on-premises / air-gapped EE images embed can be
+ silenced by the deployment it lands in, with no rebuild and without editing files
+ inside a published bundle. That was the half objectui#5522 could not close before:
+ every other input to the gate is a Vite build-time variable frozen into the bundle
+ as a literal, which is how an air-gapped EE Console came to send 14 Sentry
+ envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for
+ the customer to turn it off (objectstack-ai/cloud#1508).
+
+ The permission fails **closed** in every direction: absent key, `telemetry` block
+ absent, malformed payload, failed fetch, or a runtime predating the key all read as
+ *do not send* — which is precisely the set of runtimes leaking today. It is a
+ permission and never a source: the server supplies no DSN and cannot turn telemetry
+ on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1`
+ and other truthy lookalikes do not.
+
+ Behaviour change for deployments that already inject a DSN: reporting now also
+ requires the runtime to grant permission, via
+ `OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s
+ `allowClientErrorReporting`). A build that opted in but whose runtime says nothing
+ will go quiet — deliberately, since that is the same artifact an air-gapped
+ customer runs.
+
+ `@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and
+ the `RuntimeTelemetry` type, so consumers read the permission through the one
+ fail-closed accessor instead of writing their own optional-chain against the
+ payload.
+- 6d63cd0: An auto-derived related list now orders its rows by the CHILD object's default list view
+ `sort`, instead of falling to the server's primary-key order (objectui#5795). A task
+ version's "check items" tab whose child object declares `sort: [{ field: 'seq_no' }]`
+ renders 10/20/30/40; before this it rendered whatever order the ids happened to give —
+ 20/30/10/40 in the reported case — while the child object's own list page obeyed the
+ declaration.
+
+ **Declared as user-visible, deliberately, even though no key was added.** The contract
+ question ("where does a derived related list's sort declaration live?") was ruled on
+ objectstack#11345 (maintainer, 2026-08-23) as **direction 1**: inherit the child's list
+ view sort, and add **no** new spec key — the field-level `relatedListSort` the issue also
+ proposed was explicitly not approved. So there is nothing new to author, and
+ `record:related_list.sort` was already declared, parsed and consumed; this fills it. What a
+ host observes is nonetheless new: a derived related-list descriptor gains a populated
+ `sort` where it had none, and the query it issues gains an `$orderby`. An app whose child
+ objects declare a default list order will see those tabs re-order on upgrade — which is the
+ point of the change, and is why this is not a patch.
+
+ Nothing is inherited where nothing was declared: a child object with no default list-view
+ sort produces the same descriptor, the same node and the same `$orderby`-free query as
+ before.
+
+ The two `sort` surfaces declare the same union and mean different things by its string arm
+ — a `ListView` string is the legacy space-separated `'seq_no desc'`, while the related
+ list's own reader takes `'field'` / `'-field'` — so the inherited value is normalized to
+ the array arm once, at the derivation, through `@object-ui/core`'s
+ `convertSortToQueryParams` (the repo's single definition of both authored dialects). An
+ un-normalized inherit would have ordered by a field literally named `seq_no desc`.
+
+ Known and unchanged: `$orderby` is only assembled while the related list is in windowed
+ (server-paged) mode, so a declared *or* inherited sort still disappears while the built-in
+ client text filter is active. That hole pre-dates this change and affects the authored prop
+ identically; it is now pinned as a recorded fact in
+ `plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx` rather than
+ fixed here.
+- 4da5109: One `sys_activity` row → `FeedItem` constructor, and the console record page
+ stops dropping author-extended activity types in silence (objectui#5896).
+
+ **The defect.** `RecordDetailView`'s `sys_activity` merge read the shared type
+ table (objectui#5878) and then built the `FeedItem` itself, ending in
+ `if (!feedType) continue;`. That one `continue` collapsed two different
+ situations: a type the table maps to `undefined` **on purpose** (`commented` /
+ `mentioned` / `login` / `logout`), and a type the table has never heard of. The
+ second is an **author-extended** value — `sys_activity.type` is
+ author-extensible (objectstack#11507 direction 4, ruled 2026-08-24), every
+ column on that table is `readonly` so objectql never validates a write, and
+ ADR-0052 §5b.2 forwards an author's `activityMilestones[].type` into it
+ verbatim. So an activity that happened, was written and is queryable had no row
+ on the console record page: no placeholder, no empty state, no console message.
+ Stored, queryable, invisible — objectui#5840's failure mode reached by another
+ route, and one objectui#5969 (PR #6112) had already removed from the block
+ side, leaving the two surfaces disagreeing about the same row of the same table.
+
+ **The fix is convergence, not a second decision.** `@object-ui/plugin-detail`
+ now exports the whole reading — `activityRowToFeedItem`,
+ `UNMAPPED_ACTIVITY_FEED_TYPE` and the `resetUnknownActivityTypeWarnings` test
+ seam alongside `ACTIVITY_TYPE_TO_FEED_TYPE` — and `RecordDetailView` calls the
+ constructor instead of paraphrasing it. Publishing the table alone had left the
+ mirror one level up, and it had already drifted three ways: the silent drop, a
+ timestamp fallback that could leave `createdAt` `undefined` where the helper
+ yields `''`, and a second hand-written system-actor lookup.
+
+ **Behaviour change on the console record page** (breaking in the objectui sense,
+ shipped `minor` — objectui's `major` tracks `@objectstack`):
+
+ - an unmapped `sys_activity.type` now **renders** through the generic
+ `UNMAPPED_ACTIVITY_FEED_TYPE` (`'system'`) presentation instead of vanishing,
+ and is announced once per distinct value on `console.warn` — a **missing
+ decision**, not lost data: the row is visible, what it lacks is its own icon
+ and colour. `FeedItemType` is a closed spec enum, so minting a kind for "we
+ don't know" would be a platform change, not this surface's.
+ - `createdAt` is always a string for a row with neither a usable `timestamp`
+ nor a `created_at`.
+
+ **Unchanged, deliberately:** the four exclusions still produce no row and no
+ warning. They are decisions — comment content lives in `sys_comment`, and
+ login/logout are account events rather than record activity — and a warning
+ about a decision teaches authors to ignore the channel.
+- e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935).
+
+ Seven modules resolved authored icon names into lucide's runtime `icons` record, each
+ with its own copy of the logic: **three different tokenisers** (`split('-')` on five of
+ them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
+ rename on only **four** of the seven. The same authored name therefore rendered on one
+ surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened
+ with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`,
+ and the other six call it.
+
+ **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it
+ was measured rather than chosen.** Its regression set is empty three independent ways:
+ against the authored population, against a maximally-pessimistic every-authored-name x
+ every-surface cross-product, and against a bound-free differential over 8,298 spellings
+ derived from all 1,767 live record keys — each with a discrimination control that fired
+ in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface
+ pairs in that last reading, stripping two surfaces of every snake_case and
+ space-separated spelling they resolve today.
+
+ **What changes for you — all of it widening, none of it removal.** No name that resolved
+ before stops resolving: no key of lucide's record contains `_`, whitespace or `-`
+ (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the
+ wider one produces the same key. Sixteen name-surface pairs start resolving where they
+ rendered a fallback or nothing before:
+
+ - `layout_dashboard` and `building_2` (and every other snake_case or space-separated
+ spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state,
+ `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and
+ the related list.
+ - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried
+ no rename map. `Home` is not a live record key, so this could only ever be a widening.
+
+ **What does NOT change: what each surface draws when a name does not resolve.** The seam
+ answers `name -> component`, returning `null`, and decides nothing else (maintainer
+ ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at
+ the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning
+ (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph,
+ `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar`
+ and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and
+ then dropped once the tree was measured to have four such behaviours rather than two: a
+ lookup function is the wrong place to publish a presentation decision.
+
+ `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface
+ this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
+ its census goes from seven sites to one, and its normalisation stops being a
+ widest-common approximation of three disagreeing resolvers — so the under-reporting that
+ gate disclosed at objectui#5932 is closed rather than merely bounded.
+- d3d0ad3: `asArray` in the two app-shell `find()` readers now reads exactly what
+ `QueryResult` declares — the `records` and `items` arms are removed
+ (objectui#5945).
+
+ `useApproverDirectory.ts` and `views/metadata-admin/AssignedUsersSection.tsx`
+ both normalised a `find()` answer with
+
+ ```ts
+ Array.isArray(res) ? res : res?.records ?? res?.items ?? res?.data ?? [];
+ ```
+
+ `QueryResult` (`@object-ui/types`) declares exactly one rows member — `data` —
+ alongside `total`, `page`, `pageSize`, `hasMore`, `cursor` and `metadata`.
+ `records` and `items` are not members of it, and both were tried *before* the
+ one that is. That is AGENTS.md #0.1 in miniature: a tolerant reader that lets a
+ non-conforming producer keep working, so the wrong shape is never rejected
+ anywhere and hardens into a second de-facto contract. The same
+ `records`/`items` confusion was live in three other places that objectui#5458
+ had to fix, each reading a key no adapter returns — a helper that quietly
+ accepted all three spellings is why nobody found out two of them were wrong.
+
+ **What stops being accepted.** A `find()` answer shaped `{ records: [...] }` or
+ `{ items: [...] }` now reads as **no rows** at these two seams instead of
+ silently resolving. Both call sites degrade rather than throw: the approver
+ directory reports the reference unresolved (falling back to the prettified
+ machine name, and staffing as probed-empty), and the assigned-users section
+ renders its empty state.
+
+ **Nothing produces those shapes today**, which is why this is a removal rather
+ than a migration. Measured across the repo, per arm:
+
+ - **`records`** — no producer at the `DataSource.find()` seam.
+ `ObjectStackAdapter.normalizeQueryResult` maps the server's `records`/`value`
+ envelope to `data` before returning, so the spelling exists only *below* the
+ adapter, on the wire and in the client SDK. The two remaining `records`
+ producers in the repo are on different seams: `ViewDataProvider` returns its
+ own `ResolvedData` interface, which declares `records` legitimately, and is
+ not a `QueryResult`.
+ - **`items`** — no producer at any seam. Every `items` in the repo is the
+ unrelated UI-schema key (dropdown menus, timeline, accordion).
+
+ **The bare-array arm is kept**, because it is live: fakes at these seams answer
+ with a plain array (`AssignedUsersSection.test.tsx` is one). It is pinned in the
+ same tests as the deletions, so the live and the dead shapes cannot drift into
+ each other.
+- 864154e: The Field Designer no longer offers a formula-expression textarea, and no designer write
+ path emits a `formula` key (objectui#6043).
+
+ **This is a behaviour change on an authoring surface: a control is removed.** A field's
+ `type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the
+ palette — but the expression itself is no longer authored here. Authors write formula
+ expressions in metadata-admin's field inspector, where they are checked.
+
+ The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against
+ the installed `@objectstack/spec` 17.2.0:
+
+ ```
+ FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' })
+ => success = false
+ => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?"
+ ```
+
+ so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because
+ the key was then stored, it blocked **every later save of that object**, not just the one
+ that introduced it.
+
+ **The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges
+ the key name and never the expression LANGUAGE — measured, it accepts
+ `expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the
+ empty string is refused. Spec `expression` is CEL rooted at `record`
+ (`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity`
+ — bare field refs, which under the scope formulas bind evaluate to null silently. A rename
+ would therefore have converted a loud, immediate 422 into a formula that saves clean and
+ then quietly computes nothing, which is strictly worse than the bug it appears to fix.
+
+ Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType`
+ inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and
+ app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a
+ dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a
+ feature, not this fix. `returnType` is likewise not authored here: it is only derivable by
+ inferring the CEL result type, and with no expression control there is nothing to infer
+ from.
+
+ `formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already
+ carrying the key is stripped clean on its next save instead of staying blocked forever —
+ which matters more than usual here, because with the control gone an author would
+ otherwise have no way left to clear it. It is dropped rather than migrated to `expression`,
+ for the same reason the rename was refused. A `expression` authored in metadata-admin is
+ **not** touched: it is a real `FieldSchema` key and rides through the designer's
+ round-trip untouched.
+
+ Also removes the now-unreachable `formula` read/write from
+ `views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for
+ the key that neither the card nor the parity gate named.
+
+ The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s
+ `KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that
+ left a stale entry behind would be as red as a new offender.
+- b023625: The field metadata payload no longer emits `sortOrder`, the key `FieldSchema` refuses by
+ name (objectui#6045). Field-level sibling of objectui#6223, same objectui#5761 family.
+
+ Measured against the installed `@objectstack/spec` 17.2.0, whose `FieldSchema` accept set
+ is 71 keys:
+
+ ```
+ FieldSchema.safeParse({ type:'text', label:'L' }) => success = true (control)
+ FieldSchema.safeParse({ type:'text', label:'L', sortOrder: 3 }) => unrecognized_keys ["sortOrder"]
+
+ FieldSchema.safeParse({ type:'text', label:'L', sortable: true }) => success = true (control)
+ FieldSchema.safeParse({ type:'text', label:'L', sortable: 3 }) => success = false
+ ```
+
+ The control is what makes that a key-by-key result rather than a schema refusing
+ everything, and the `sortable` pair is what shows the near-spelling is a *different
+ concept* — a boolean ("whether field is sortable in list views"), not this key's spec
+ name.
+
+ **The resolution was deletion, not a rename**, which is objectui#4687's shape rather than
+ objectui#6041's. The spec has no field-level ordering key at all: it models field order by
+ **declaration order** in the object's `fields` record, so a designer that wants explicit
+ ordering reorders that record rather than carrying an index. There was nothing to map onto,
+ and nothing was invented to map onto.
+
+ **It was latent, and that is confirmed on today's tree.** Neither of the two sites that
+ construct a `DesignerFieldDefinition` — `FieldDesigner`'s create/update handlers and
+ `MetadataFieldsPage.toDesignerField` — ever named the key, so `toFieldPayload` emitted
+ `sortOrder: undefined` and `JSON.stringify` dropped it. The key never reached the wire. It
+ was one reorder feature away from doing so, which is the objectui#4644 shape: a hard 422
+ `INVALID_METADATA` that blocks every subsequent save of the object, with nothing in the UI
+ to say which key caused it.
+
+ Removed in one go from the wire shape (`FieldMetadataPayload`), its writer
+ (`toFieldPayload`) and the UI model (`DesignerFieldDefinition`), so no declaration is left
+ behind that no writer fills and no schema accepts.
+
+ **Breaking for TypeScript consumers**: `sortOrder` is gone from `DesignerFieldDefinition`
+ (`@object-ui/types`) and from `FieldMetadataPayload` (app-shell), so code that set either
+ stops compiling.
+
+ Two keys share this spelling and are untouched, which is why the census was on the *shape*
+ — a field-metadata payload key `FieldSchema` refuses — rather than on the identifier: the
+ **object-level** `sortOrder` (`ObjectSchema`'s, removed from the object wire shape by
+ objectui#6223 and deliberately kept on the `ObjectDefinition` UI model) and the
+ **saved-view** `sortOrder` in `ObjectView`, which is per-view display order on a different
+ document entirely.
+
+ The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes
+ with the fix — that ledger ratchets in both directions, so an entry left behind for a
+ resolved key is as red as a missing one.
+- 5173a5e: ⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on
+ the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read
+ this before upgrading if any of your form metadata gates on the session user.
+
+ objectui#6010 bound the host predicate scope on the five authored-predicate call
+ sites in the components form renderer, so `current_user` (plus the ADR-0068
+ `user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn`
+ there. Two other authored-predicate evaluators were still passing `undefined`
+ for that argument, so the same authored text meant two different things
+ depending on which surface opened the form (objectui#6110):
+
+ - **`apps/console`'s form renderer**, on the authed internal route
+ `/forms/:name`. The internal route is a runtime record surface by ADR-0089
+ D1's own words (*"runtime record surfaces bind `record` + `current_user`"*),
+ and its `visibleWhen` metadata is the same `*.view.ts` FormView the
+ object-view chain renders — so a role gate authored once behaved differently
+ depending on which route opened the form.
+ - **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`),
+ the gate that re-checks the whole declared field set at final submit because
+ `allowSkip` can jump past a step. Its docstring promises *"the same verdict
+ from all three rather than a second, divergent dialect"*, and since #6010 it
+ was the divergent one.
+
+ **Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails
+ OPEN: a field on screen is what you get when the predicate resolves TRUE, when
+ the scope was never bound so the predicate faulted, *and* when the predicate is
+ broken. Those worlds were indistinguishable, so an app that authored a
+ `current_user` gate saw the field render and had no way to tell the rule was
+ inert. After this change the predicate is evaluated for real, and fields and
+ sections that have always been visible will disappear for the users the rule
+ excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user`
+ requiredWhen that has been silently not applying will now start holding submits.
+
+ In the wizard the change is a fix in the user's favour as well: a required field
+ the wizard HID from this user was still counted as visible by the submit gate,
+ so the submit was refused on a control the submitter could neither see nor fill
+ in.
+
+ **Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in
+ your form-view and object metadata that names `current_user`, and confirm each
+ predicate says what you actually want evaluated against `record` +
+ `current_user`.
+
+ **The public anonymous form `/f/:slug` is deliberately unchanged.** It is
+ mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is
+ no authenticated principal, and no provider is mounted above it — so its scope
+ is empty and a `current_user` predicate authored on a public form still faults
+ and still fails open, exactly as before. Nothing new is declared to say so: the
+ two routes are told apart by which component mounts them.
+
+ `@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider`
+ user normalisation, so every console surface that mounts the provider publishes
+ the same `current_user` shape rather than re-deriving it.
+- 1e7fe0a: `record:details` section headings converge on the declared `label` slot; the
+ `title` alias limb is gone (objectui#6190, maintainer ruling 2026-08-31 —
+ option A, three producers plus the consumer in one change).
+
+ **Breaking for anyone reading `deriveFieldGroupDetailSections`' output — the
+ emitted key moves from `title` to `label`.** That function is public API
+ (exported from `@object-ui/plugin-detail`), so this is an output-shape change,
+ not an internal refactor. A caller that reads `section.title` off its return
+ value reads `undefined` after this release and must read `section.label`.
+ `BuildPageOptions.sections` and `ObjectDefLike.sections` declare the same move,
+ so a caller passing `sections` into `buildDefaultPageSchema` supplies the
+ heading as `label` too.
+
+ **The retired authoring spelling is `title` on a `record:details` section.**
+ `RecordDetailsRenderer` read `s.title ?? s.label` — a strict-priority second
+ spelling of one slot, with byte-identical localization on both limbs, so a
+ producer emitting both silently disagreed with itself and `title` won. It now
+ reads `label` only. Nothing an author could publish is affected: `@objectstack/spec`
+ REFUSES `title` inside a `sections[]` entry (`unrecognized_keys`, pinned by
+ objectstack#11902), and `@object-ui/types` plus the authoring inspector have only
+ ever declared `name` / `label` / `columns` / `fields`. The declared and authoring
+ faces were already converged; only three runtime producers lagged, and all three
+ move here:
+
+ - `buildDefaultPageSchema`'s `deriveFieldGroupDetailSections` section literal;
+ - `RecordDetailView`'s re-map of that output through the per-object i18n
+ convention;
+ - `RecordDetailView`'s auto-grouped "More details" section, authored in
+ app-shell and unreachable from the synthesizer.
+
+ **No rendered heading changes.** Every existing producer yields byte-identical
+ headings, asserted by rendering rather than by inspection — including the
+ "More details" bucket, which the earlier two-step scope would have degraded to
+ the literal `details` in every shipped locale while every existing guard stayed
+ green. A new pin (`RecordDetailView.sectionHeadingsRenderPath-6190.test.tsx`)
+ walks the tree app-shell actually renders and closes that blind spot.
+- d18a0d3: Object-level metadata payloads no longer emit the three keys `ObjectSchema` refuses by
+ name — **group**, **sortOrder** and **relationships** (objectui#6223).
+
+ Measured against the installed `@objectstack/spec` 17.2.0, whose `ObjectSchema` accept set
+ is 42 keys:
+
+ ```
+ const base = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } };
+
+ ObjectSchema.safeParse(base) => success = true (control)
+ ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control)
+ ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control)
+
+ ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"]
+ ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"]
+ ObjectSchema.safeParse({ ...base, relationships: [ … ] }) => unrecognized_keys ["relationships"]
+ ```
+
+ The two controls are what make that a key-by-key result rather than a schema refusing
+ everything. Each key was resolved on its own, as the objectui#5761 family ruling requires:
+
+ - **group** — the Object Manager's grouping is a UI-only display category. The spec has no
+ object-level grouping key (`fieldGroups` groups the fields *inside* one object), so the
+ grouping control and its column stay, and the value is now DERIVED from the spec key that
+ is accepted (`isSystem`) instead of round-tripped. `MetadataObjectsPage` also strips a
+ `group` already stored by an earlier build, because its save-back spreads the server
+ document verbatim and would otherwise keep re-sending it forever.
+ - **sortOrder** — what populated it was the array index the converter happened to be at,
+ i.e. the order the list was already in. The declaration is removed from the object
+ payload. The field-level `sortOrder` is a different key with a different card
+ (objectui#6045) and is untouched.
+ - **relationships** — the spec models relationships on the FIELD (`reference` /
+ `master_detail`, plus object-level `indexes`). The object payload stops declaring and
+ sending an object-level relationship array; what the designer should author for a
+ relationship is a data-model question this change does not settle.
+
+ **Breaking for TypeScript consumers of `ObjectMetadataPayload`** (exported from app-shell):
+ the three properties are gone from the published type, so code that set them stops
+ compiling. That is the point — setting any of them produced a payload the metadata route
+ refuses. `ObjectDefinition` (the designer's UI model) is unchanged and still carries all
+ three.
+
+ The parity gate built for objectui#5761 now has a **second oracle**: every shape in
+ `PAYLOAD_SHAPES` names the schema that judges it, `ObjectSchema` alongside `FieldSchema`,
+ and reach is resolved within an oracle rather than across one — `group` is a legal
+ `FieldSchema` key and a refused `ObjectSchema` key at the same time. That extension found a
+ fourth object-level key (`enabled`, objectui#6238) and a value-level rejection the key-name
+ check cannot see (`fields` sent as an array where the spec wants a map, objectui#6240);
+ both are filed and ledgered rather than fixed here.
+- 622f33c: `MetadataService`'s two delete methods no longer PUT a hand-written tombstone. They call
+ the metadata API's own delete door instead, and the latent `enabled?: boolean` on
+ `ObjectMetadataPayload` is gone with it (objectui#6238). Object-level member of the
+ objectui#5761 family, surfaced by the `ObjectSchema` oracle objectui#6223 added to
+ `scripts/check-designer-field-key-parity.mjs`.
+
+ **What the tombstone actually did.** `deleteObject` and `deleteMetadataItem` wrote
+ `{ name, enabled: false, _deleted: true }` through `client.meta.saveItem`, i.e.
+ `PUT /api/v1/meta/:type/:name`. Measured against the installed `@objectstack/spec` 17.2.0
+ using `getMetadataTypeSchema` — the registry the framework's own `saveMetaItem` resolves a
+ PUT's validator from — across all 26 registered overlay schemas:
+
+ ```
+ ObjectSchema.safeParse({ name, label, fields }) => success = true (control)
+ ObjectSchema.safeParse({ name, label, fields, isSystem: true }) => success = true (control)
+ ObjectSchema.safeParse({ name, enabled: false, _deleted: true }) => unrecognized_keys ["enabled","_deleted"]
+
+ 25 of 26 registered overlay schemas refuse `enabled` and/or `_deleted` BY NAME
+ 1 of 26 (`view`) tolerates them; 4 kinds have no registered schema at all
+ 0 of 26 strip them
+ ```
+
+ So there were two failure modes, not one. Where the type has a strict schema — `object`
+ among them — the delete was a hard `422 INVALID_METADATA`, so nothing was ever recorded.
+ Where the schema is tolerant or absent, the framework stores the request item **verbatim**
+ (it deliberately persists the body rather than `parsed.data`), and `_deleted` has no reader
+ anywhere on the platform — so the "soft delete" was a silent no-op that left the item live
+ carrying two junk keys. Neither outcome deleted or disabled anything.
+
+ **The resolution is a mechanism change, not a rename**, and there was nothing to rename to.
+ `ObjectSchema`'s 42-key accept set has no on/off flag; the near-spelling `enable` is
+ `ObjectCapabilities`, a system-features *module object*, so `enabled: false` → `enable:
+ false` fails on the value where it passes on the name. No wire key was invented: a metadata
+ soft-delete convention would be a `@objectstack/spec` contract addition, and the platform
+ does not have one.
+
+ **Both sites now call `client.meta.deleteItem(type, name)`** — `DELETE
+ /api/v1/meta/:type/:name`, the same request `MetadataClient.reset` issues, which is the
+ mechanism `MetadataObjectsPage.handleObjectsChange` and `ResourceEditPage` already used for
+ deletes. Two mechanisms for one operation had disagreed; now there is one. The delete route
+ is generic over `:type` on the same route family and capability gate as the PUT, so this
+ holds for every category the generic `deleteMetadataItem` serves, not just `object`. The
+ doc comment claiming the API "exposes `saveItem` but no dedicated `deleteItem`" was stale:
+ `@objectstack/client` 17.2.0 declares `meta.deleteItem` on the very client this service
+ already holds.
+
+ `reset` semantics are the overlay's, and that is the governed answer rather than a
+ shortfall: it removes the customization row — which *is* deletion for an object the
+ designer authored — and restores the artifact for one a package declares, an object you are
+ not allowed to delete. Which of the two an item is, is what the API's own `deletable` /
+ `resettable` verdicts report, not something a client-side flag should decide.
+
+ **No published type changed.** `ObjectMetadataPayload` is exported from its module but that
+ module is not re-exported by `packages/app-shell/src/index.ts`, the package's only entry, so
+ the removed `enabled?: boolean` was never on the published surface and no `**/src/index.ts`
+ is touched. What consumers *can* observe is behaviour: `MetadataService` is reachable
+ through the published `useMetadataService()` hook, both method signatures are unchanged
+ (`Promise`), and the HTTP request they issue changes from a `PUT` with a body to a
+ `DELETE`.
+
+ The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes
+ with the fix — that ledger ratchets in both directions, so an entry left behind for a
+ resolved key is as red as a missing one. It is now empty for the first time, which is the
+ ratchet arriving where it was pointed; the self-test's non-vacuity guard moved onto a
+ fixture accordingly, so an empty ledger reads as success rather than as a demand that some
+ key stay unresolved.
+- 88b15fd: `ConditionBuilder` now takes a caller-supplied **subject vocabulary** instead of hardcoding a
+ record-scoped one (objectui#6296).
+
+ The builder built every row subject as `record.` + field name, plus a fixed `record.id` /
+ `user.*` / `org.*` context list. That is correct for all five files that mount it today — six
+ mount sites, since `ActionDefaultInspector` mounts it twice — because every one of them is a
+ record-scoped site. It is wrong for a **flattened**-scoped site such as the flow designer's
+ entry condition, where the trigger record's fields *are* the top-level evaluation context
+ (bare `status`) and the prior values arrive as `previous.FIELD`. This repo's own
+ `flow-scope.ts` already computes that distinction (`fieldPrefix: onStart ? '' : 'record.'`,
+ `includePrevious`), and objectstack's `packages/formula/src/validate.ts` defines the two
+ scopes.
+
+ A new optional `subjects` prop declares what a site actually binds:
+
+ - `fieldPrefix` — defaults to `'record.'`; `''` declares a flattened scope.
+ - `includePrevious` — also offer `previous.FIELD` per field, plus the whole-record `previous`
+ token, which is what makes the create-path idiom `previous == null` **selectable** rather
+ than something the author has to recall from help text.
+ - `context` — replace the context subjects, so a flattened site does not inherit `record.id`,
+ a root it does not bind. Offering it there would make this editor emit the one spelling its
+ own sibling ref-check flags as out of scope.
+
+ Declared, never inferred: the component does not guess a site's scope from the value it is
+ handed. **A caller that declares nothing gets exactly the previous behaviour** — the option
+ list, the `record.` prefix on a compiled row, and the single-quoted value spelling are all
+ pinned positively against that default, so changing it fails rather than re-baselines.
+
+ **Double-quoted string literals now round-trip into row mode.** `unfmtValue` stripped only
+ single quotes while `fmtValue` re-emitted only single quotes, so `status == "done"` could
+ never survive the builder's byte-for-byte adoption check and was handed to the raw CEL editor
+ — even though double quotes are what the entry-condition placeholder teaches and what every
+ shipped example flow uses. Each row now remembers the quote character it was parsed with and
+ re-emits that one, so the author's own spelling is preserved rather than normalised, and the
+ byte-for-byte safety rule is kept exactly as it was rather than loosened. Rows built in the
+ builder still emit single quotes, unchanged.
+
+ Measured against the shipped corpus — every start-node entry condition in objectstack's
+ example apps plus the HotCRM example from objectui#6226 — row-mode adoption goes from 3/17 to
+ 15/17. The two that remain on raw mode are `&&` mixed with a parenthesised `||` group: a
+ grammar limit of the row model, unrelated to subjects, and out of this card's scope.
+
+ The component is not re-exported from the package index, so no external caller can pass the
+ new prop yet; the wiring that will (objectui#6226) is a separate card. Scored `minor` for the
+ added capability rather than `patch`, since the widening is real even while its only future
+ caller is in-repo.
+- b362c1b: Setup gains a **Packaged automation** page — the operational surface for the flows an
+ installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other
+ framework-contributed Setup surface is: the page registers the component-registry ref
+ `automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it
+ at `/apps//component/automation/packaged`. No bespoke route is added — a second way
+ in would be a URL the app metadata does not know about.
+
+ Per packaged flow the page does exactly two things:
+
+ - **on/off for this scope** — reads the activation state the engine reports
+ (`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation`
+ ledger) and flips it through `POST /api/v1/automation//toggle`;
+ - **clone** — `POST /api/v1/automation//clone` with a mandatory new machine name and
+ label (§7.1). The carried-over definition is never offered as editable form fields; the
+ copy is edited in Studio like any other flow.
+
+ Authoring stays in Studio. The list is scoped to packaged flows by the server's own
+ three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the
+ `_packageId`-only shortcut, which classifies a tenant overlay bound to a package as
+ packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an
+ install-wide switch.
+
+ **Server refusals reach the operator verbatim** — no client-side softening or rewording.
+ Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose
+ message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard
+ (409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a
+ list nothing on the client could reconstruct), and the §7.1 clone name conflict (409).
+
+ ⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no
+ base-moved notice, no link from a clone back to its source. Cloned-without-disabled and
+ disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including
+ the case where a response carries a `clonedFrom` key anyway — the platform does not track
+ that lineage, so a page that displayed it would be displaying something it invented.
+
+ `@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real
+ translations in all nine other packs, matching each pack's existing vocabulary for "flow"
+ and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket,
+ fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and
+ each one's quotation convention. The group deliberately carries no drift or ancestry
+ wording, and no server refusal text: those arrive as server-authored prose and are
+ rendered verbatim.
+- 9602dc8: Setup › **Packaged automation** gains a packaged **actions** section beside its flows one —
+ the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch
+ doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated:
+ 「动作 可能是需要开关的,因为有的 action 我不想启用。」
+
+ Per packaged action the section does exactly **one** thing: **on/off for this scope**. That
+ is all the `sys_metadata_activation` ledger knows about an action, and the section claims
+ nothing more.
+
+ - **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic
+ data API list the object itself sanctions for exactly this
+ (`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer
+ 'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows
+ everything armed.
+ - **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded
+ `actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with
+ the object-embedded declaration winning a `:` key clash. Listing only the
+ first source would leave an administrator with no off-switch for a packaged standalone
+ action.
+ - **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly
+ the one key its body declares (`{ enabled }`); `global` is the object segment for an
+ object-less action, the same spelling both dispatch doors take.
+
+ **⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3
+ charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered
+ until real pull appears. A clone control here would advertise machinery that does not exist —
+ which is also why the server's §5 refusal for actions recommends the platform operator and an
+ ordinary sibling action where the flow refusal recommends a clone.
+
+ **⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no
+ base-moved notice. The platform tracks no such lineage, so a surface showing it would be
+ showing something it had to invent. The absence is pinned against a response that smuggles
+ `clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire.
+
+ Server refusals reach the operator **verbatim** — no client-side softening, no retry loop.
+ Three shapes are reachable in tests, each transcribed character-for-character from the
+ runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the
+ posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`,
+ naming the objects a machine name collides across — a list nothing on the client could
+ reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`).
+
+ One further refusal is the section's own and it points the same way: a `hasMore` on the
+ ledger read is treated as a **load failure** rather than rendered. A dropped row reads as
+ "active", so a partial ledger would show a switched-off action as armed — the one direction
+ this section must not fail in.
+
+ The flows section is unchanged in behaviour. It gains a heading beside the new one, and the
+ page subtitle now says "Flows and actions" — that string moved in all ten packs together.
+ Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real
+ translations; the two toggle-failure keys are artifact-neutral by wording and are reused
+ rather than duplicated.
+- c71e14d: **The unresolvable-visibility-predicate report now names the roots of the tier the
+ predicate was actually evaluated against** (objectui#6487). An app-shell author
+ whose nav, area or field `visible` faulted was told to check `record` and
+ `page.` — two roots that tier does not bind at all.
+
+ `formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate`
+ (both exported from `@object-ui/react`) take a new **optional sixth argument**, a
+ `PredicateScopeTier` — also exported — selecting the closing advice paragraph.
+ Everything above that paragraph is unchanged on every surface, and so is every
+ verdict: this is diagnostics copy only.
+
+ **The published signature grew; nothing existing breaks.** The argument defaults
+ to `'page-component'`, so a five-argument call keeps printing the bytes it
+ printed before. All three in-repo call sites pass their tier explicitly rather
+ than lean on that default.
+
+ Each tier's root set was derived from the code that builds the bag, not from the
+ prose that described it:
+
+ - **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item
+ predicates. Both bind `record`, `current_user` and `page.` (the roots
+ `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is
+ byte-for-byte what it was.
+ - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility`
+ runs, wired onto this reporter by objectui#6443. Its evaluator is built from
+ `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so
+ the line now names `current_user` with its three ADR-0068 alias spellings,
+ `app`, and `features` — the deployment-flag root that provider documents for
+ exactly this kind of predicate — and states outright that `record` and
+ `page.` do not exist there.
+
+ **Why not generalise the copy instead.** Dropping the concrete root names would
+ have made one paragraph true everywhere at the cost of making it useful nowhere:
+ an author who mistyped a root needs to know which roots exist *at their tier*,
+ which is the whole reason the paragraph is read.
+
+ `data` is bound at the app-shell tier but is deliberately not advertised there —
+ every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it,
+ so naming it would point an author at a root that answers nothing.
+- 2c26335: **BREAKING (in name only):** `MetadataService.saveObject(obj, existingFields)` now requires
+ its second argument (objectui#6490). Calls that omitted it no longer compile.
+
+ **Runtime behaviour is unchanged, and that is the whole justification.** A call that omitted
+ the field list was already a guaranteed `422` — every time it ran, against every backend.
+ `ObjectSchema.fields` is not merely typed, it is REQUIRED: measured against the installed
+ `@objectstack/spec` 17.2.0, `ObjectSchema.safeParse({ name: 'account', label: 'Account' })`
+ fails with `invalid_type @ fields`, and `metadata-protocol`'s `saveMetaItem` parses the whole
+ item against that same schema and throws `422 INVALID_METADATA` **before** it persists. The
+ method cannot build a valid document without the argument, so the only calls this break
+ breaks are calls that already failed. The signature is now honest about it, and the diagnosis
+ moves from a round trip at runtime to the compiler.
+
+ Nothing new is exported and nothing new is accepted — this narrows the published surface
+ rather than widening it. In-repo production call sites were measured at **zero** (only tests
+ called it), so the migration for an external consumer is to pass the field list it was
+ already required to send: `saveObject(obj, fields)`.
+
+ ⛔ Two readings were considered and declined, recorded so neither is taken later as a
+ shortcut. **Not a `{}` default** — `{}` parses GREEN and `PUT /api/v1/meta/object/:name` is
+ an upsert, so defaulting would delete every field of the object on a save that only meant to
+ rename it, trading a loud, harmless 422 for silent data loss; the anti-wipe control from
+ objectui#6240 (`omits fields entirely when the caller supplied none — it does NOT write {}`)
+ moves with the signature and still guards the path a JavaScript consumer can reach. **Not
+ fetch-and-merge** — an object save could GET the current document and preserve its stored
+ `fields` the way `saveFields` does, but that builds capability for a path with zero measured
+ pull and makes the parameter redundant.
+
+ An EMPTY list stays a different statement from a missing one: `[]` means "this object has no
+ fields", writes `{}`, and under the upsert performs the wipe the caller asked for — the same
+ authoritative reading `saveFields` gives its own empty list. Unchanged, and now pinned,
+ because the required parameter is what routes a caller with nothing to hand toward it.
+
+ Scored `minor` and not `major` per AGENTS.md §版本号策略 — objectui's major is pinned to the
+ `@objectstack` major so that "same major ⇒ compatible" holds across the two repos, and every
+ publishable package sits in one `fixed` group, so objectui's own breaking changes ship as
+ `minor` with the break spelled out in the body. That is the convention, which is why the
+ break is stated in words above.
+- 08c3da9: ⚠️ **Behaviour change: `current_user` and `features` gates on an object field's
+ `visible` that have been doing nothing on the record form page and in the
+ console's record modal now TAKE EFFECT.** Read this before upgrading if any of
+ your object metadata gates a field on the session user or on a deployment flag.
+
+ objectui#6010 and objectui#6110 bound the host predicate scope on the form
+ renderer and on the console form routes. `evaluateVisibility` was still being
+ reached with a THIRD and FOURTH evaluator that neither of those touched:
+ `RecordFormPage` and `AppContent` each built a private
+ `new ExpressionEvaluator({ user, app, data })` for the field-visibility filter,
+ beside — not from — the `ExpressionProvider` each of them mounts. Those bags
+ bound `user`, but not the canonical `current_user` nor the ADR-0068 `ctx.user` /
+ `os.user` spellings of that same object, and not `features` at all. So one
+ authored predicate meant two different things depending on which evaluator
+ reached it: `current_user` resolved on a nav item and was unbound on a field.
+ Both sites now build their scope with the same `buildExpressionScope` the
+ provider uses, which is the only declaration of what an app-shell predicate may
+ name.
+
+ **Why nobody noticed, and why the fix is felt as a change.** A field `visible`
+ predicate fails OPEN: a field on screen is what you get when the predicate says
+ TRUE, when the root was never bound so the predicate faulted, *and* when the
+ predicate has a typo. Those worlds are indistinguishable, so an app that
+ authored a `current_user` gate saw the field render and had no way to tell the
+ rule was inert. After this change the predicate is evaluated for real, and
+ **fields that have always been visible will disappear for the users the rule
+ excludes** — and a `features` gate whose flag is off will hide its field once
+ `/api/v1/auth/config` resolves.
+
+ `AppContent`'s bag also hand-rolled its user as `{ name, email, role }`, without
+ `positions`. It now uses the same `buildExpressionUser` normaliser every other
+ console surface publishes, so `'sales' in current_user.positions` — the gate the
+ server enforces on write — reaches the same verdict client-side instead of
+ faulting open.
+
+ **Before upgrading**, audit any `visible` predicate in your object metadata that
+ names `current_user` (or `user` / `ctx.user` / `os.user`) or `features`, and
+ confirm each says what you actually want evaluated. Measured on the metadata
+ shipped in this repo and in the framework at the time of the change: **nothing
+ in it authors such a gate**, so no shipped surface changes behaviour today —
+ the audit is for your own object metadata, which this cannot see.
+
+ **The error path is deliberately unchanged.** A predicate that throws still
+ fails open, exactly as objectui#6443 / objectui#6487 left it. This change is
+ about which roots are BOUND, not about what happens when evaluation fails.
+- 6d68491: metadata-admin inspectors: name the "inactive values retained" state instead of
+ rendering it as live configuration (objectui#6499).
+
+ `showWhen` gates rendering only, and `isFieldVisible` additionally re-shows any
+ field that already holds a stored value — deliberately, "so existing config is
+ never hidden". The consequence on screen: an author who enabled a controller,
+ filled its dependent fields, then switched the controller back off keeps seeing
+ those fields as ordinary, live-looking controls. The stored config and the
+ switch beside it disagree, and nothing said which one was in effect.
+
+ Per the maintainer ruling of 2026-08-27 (Option C), the values are KEPT and the
+ state is made explicit. Pruning on save was rejected: it silently discards
+ config an author entered, and inverts the very rule that stops config from
+ vanishing unseen.
+
+ - New `inactiveRetainedKind(field, node, fields)` in `flow-node-config.ts` — a
+ pure read that reports a field rendered ONLY because the stored-value re-show
+ rule fired. It distinguishes `'controller-off'` (a real toggle the author can
+ switch back on) from `'no-controller'` (the `__legacy__` render-only keys,
+ where no such toggle exists and saying otherwise would be a fresh lie).
+ - `FlowNodeConfigField` renders the notice beside the affected control, with a
+ "Clear value" action so the author can discard the residue **deliberately**.
+ Read-only inspectors show the notice without the action.
+ - Coverage is every `showWhen` group in the inspectors, pinned mechanically:
+ all 33 gated fields across the descriptor tables, plus the two runtime
+ producers (an engine-published `configSchema` and a connector input schema)
+ that mint groups no source file contains.
+
+ Render-layer only: no save-path change, no data deletion, and `isFieldVisible`'s
+ stored-value re-show rule is unchanged — clearing is an ordinary author-initiated
+ field commit, the same write as emptying the control by hand.
+- 5af67c2: Object-field visibility on the record form page and the console's global
+ record-form modal now reads the keys `@objectstack/spec` DECLARES — the static
+ `hidden` and the `visibleWhen` predicate — instead of `visible`, a key the
+ contract refuses (objectui#6514; maintainer ruling 2026-08-27, Option A).
+
+ `FieldSchema` is a `strictObject` and `visible` is not one of its keys: it
+ appears in `FIELD_KEY_GUIDANCE` as prose that REFUSES the spelling, deliberately
+ not as an alias, because — quoting the guidance — "this surface declares BOTH
+ forms and the two answers have opposite polarity". So both call sites were
+ gating on something no author could legally write: metadata carrying a
+ field-level `visible` never survives validation, and a census re-run for this
+ card over the framework's 113 `*.object.*` files (`aef1b7e64`) found zero of
+ them, against 113 `label` / 107 `required` / 76 `readonly` field-level hits from
+ the same query shape.
+
+ What changes for an author: `hidden: true` on a field now actually withholds it
+ from these two forms, and a `visibleWhen` predicate now decides there — both
+ previously inert. The dead `visible` read is deleted rather than kept beside the
+ declared keys, so the refused spelling cannot survive as a second, renderer-side
+ contract.
+
+ `hidden` is INVERTED relative to the key it replaces (`visible: false` is
+ `hidden: true`), and an inversion read backwards raises no error — a field
+ either vanishes with no diagnostic or leaks to a principal it was hidden from.
+ Both directions are therefore pinned per call site (hidden/shown, predicate
+ false/true), and the suites were measured red against the pre-fix code AND
+ against a deliberately inverted implementation.
+
+ The two keys compose as AND: `visibleWhen` is documented as "shown only when
+ TRUE (else hidden)", a necessary condition and never a licence to un-hide a
+ statically hidden field — the same shape `resolveFieldRuleState` already uses
+ for `readonly`/`readonlyWhen` and `required`/`requiredWhen`.
+
+ Field-level visibility keeps the contract's current scope: no `current_user`
+ binding is added, and the tier's documented fault-open is unchanged. Per-user
+ field hiding still goes through the option/form layers that bind the user.
+- a4d39a8: **Breaking (compile-time only):** `buildExpressionUser`'s PARAMETER is now the session
+ contract — `ExpressionUserSession | null | undefined` instead of `unknown` — so every call
+ site is checked against it (objectui#6559).
+
+ objectui#6551 narrowed the CAST the normaliser read its input through, so the module began
+ DECLARING what a signed-in session is: `id`, `name` and `email` required, mirroring
+ `@object-ui/auth`'s `AuthUser`. The parameter behind that cast stayed `unknown`, so the
+ declaration bound nothing — every call site satisfied it vacuously and
+ `buildExpressionUser({ name: 'B', email: 'b@c.d' })` still compiled. A declaration nothing
+ checks is indistinguishable from no declaration at all (AGENTS.md #0.1). The cast is gone
+ and the shape is stated once, on the parameter, so the declaration and the check are the
+ same statement rather than two that merely agree.
+
+ WHAT BREAKS, AND FOR WHOM. This tightens a signature published from the package entry
+ (`packages/app-shell/src/index.ts`), so an external caller that passes an unchecked or
+ under-declared value stops compiling on upgrade. That is accepted (maintainer ruling
+ 2026-08-27, option A): the only calls it refuses are calls that were never conformant with
+ the contract the module already declared. It ships as `minor`, not `major` — objectui's
+ major tracks `@objectstack`'s, so its own breaking changes ship as a minor with the break
+ written down (`scripts/check-changeset-no-major.mjs`). ⛔ Keeping `unknown` and ⛔ adding a
+ second, wider entry point were both declined.
+
+ NO RUNTIME BEHAVIOUR MOVES. All four in-repo production call sites pass `useAuth().user`,
+ typed `AuthUser | null`, and type cleanly unchanged — two in `console/AppContent.tsx`, one
+ in `views/RecordFormPage.tsx`, one in `apps/console`'s `InternalFormRoute.tsx`. The body is
+ byte-equivalent: the same keys, the same `??` defaults, the same anonymous branch. ⛔ No
+ consumer-side fallback was added; `id: u.id ?? null` remains the rejected shape (triage
+ ruling 2026-08-26), because a lenient default in the consumer is what AGENTS.md #0.1
+ forbids and it silently equates "signed in, no id" with "signed out".
+
+ Note for callers holding the SPEC's `AuthUser` rather than `@object-ui/auth`'s: the spec
+ type is an `interface` with no index signature, and TypeScript infers an implicit index
+ signature for type aliases only, so it is not assignable to a contract declaring
+ `[key: string]: unknown`. `@object-ui/auth`'s `AuthUser` extends it and adds that index
+ signature, which is what every call site here passes.
+
+ Pinned by the new `expressionUser.parameterContract.types.test.ts`, compiled by the
+ package's `tsconfig.test.json` (chained off `type-check`, which CI runs). Its refusals are
+ `@ts-expect-error` directives, so a re-widened parameter makes them UNUSED and TS2578 turns
+ the type-check red; a type equation on `Parameters[0]` reds
+ alongside them. Every refusal is routed through a non-fresh value, so what is measured is
+ the parameter and not excess-property freshness.
+- dc4365c: Retire the discovery-wire preview mode — the console no longer turns
+ authentication off because a server said `mode: 'preview'` (objectui#6654).
+
+ `@objectstack/spec` retired the `RuntimeMode` value `'preview'` and the whole
+ `PreviewModeConfig` block (objectstack#11846). This console still read that
+ surface back off the runtime discovery payload, which is a different layer from
+ the retired compile-time type — so the consumption could not simply be assumed
+ dead, and its removal was ruled deliberately (2026-08-29).
+
+ - `ConditionalAuthWrapper` (`@object-ui/app-shell`) drops the branch gated on
+ `discovery.mode === 'preview'`. That branch called `setAuthEnabled(false)` and
+ simulated an identity out of `discovery.previewMode`, every field behind a
+ default. Auth availability is now decided **only** by the ADR-0076 D12 service
+ reading (`isServiceUsable(discovery.services.auth)`), exactly as for any other
+ mode.
+ - `DiscoveryInfo` (`@object-ui/react`) drops the `previewMode` block and stops
+ documenting `'preview'` as a runtime mode; the package README's discovery
+ section is updated to match.
+
+ **Accepted failure direction:** a deployment that still emits `mode: 'preview'`
+ or a `previewMode` block now falls back to the ordinary auth reading — it
+ requires login. That is loud, diagnosable and more secure than keeping a dormant
+ auth-off path keyed on a spelling the platform no longer produces.
+
+ **Not affected:** `AuthProvider`'s `previewMode` prop, `useAuth().previewMode`
+ and `PreviewBanner` in `@object-ui/auth` are a separate published capability
+ with a different producer (a host passing the prop). Only the discovery-wire
+ producer of that prop is retired; hosts that pass it explicitly are unchanged.
+- 969ba84: Renderers for the `app:launcher` and `nav:menu` page blocks (objectui#6661).
+ Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two
+ `PageComponentType` members that are purely metadata-driven, so nothing had to
+ ship before their renderers could. Phase 2 (`global:search` /
+ `global:notifications`) landed in objectui#6757 and set the pattern this
+ follows.
+
+ A page that declared either member drew a dashed box. The two symptoms were not
+ the same, which is worth recording because it decides what "fixed" looks like
+ for each:
+
+ - `nav:menu` is in `PALETTE_PLACEHOLDER_BLOCKS`, registered eagerly, so it drew
+ the literal "Component Placeholder" scaffold in every host.
+ - `app:launcher` is only in `PROTOCOL_COMPONENTS`, registered when a host opts
+ in via `registerPlaceholders()` — which just `apps/console` does. So it drew
+ the scaffold in the console and `SchemaRenderer`'s red OBJUI-001 "Unknown
+ component type" panel everywhere else.
+
+ Neither block adds a data layer — each mounts plumbing that was already live,
+ and neither issues a request or touches an adapter:
+
+ - `app:launcher` reads the metadata app registry (`useMetadata().apps`, which
+ `MetadataProvider` fetches eagerly) through the shared `filterActiveApps`
+ predicate, and draws it with `HomeAppsStrip` — the console's own launcher
+ grid — so an authored launcher and the Home launcher cannot drift into two
+ looks for one thing.
+ - `nav:menu` reads the active app's navigation tree from that same registry and
+ renders it as page content, taking every derived fact from `@object-ui/layout`:
+ hrefs from `resolveHref`, labels from `resolveNavItemLabel`, the active row
+ from `resolveActiveNavItem`, and the item-level guards (`visible`,
+ `requiredPermissions`, `requiresObject` / `requiresService`) in the order
+ `NavigationItemRenderer` applies them, wired to the same console providers
+ `AppSidebar` wires them to. `action` items dispatch through
+ `useNavActionDispatch`, so framework#4509's "renders but dead-clicks" shape is
+ not reintroduced.
+
+ `nav:menu` does not mount `NavigationRenderer` itself: that renders through
+ `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's
+ `SidebarProvider`, and a page block has to render standalone. `@object-ui/layout`
+ therefore exports `resolveNavItemLabel`, which was module-private — an additive
+ export with no behaviour change, so the sidebar and an authored menu cannot show
+ one nav entry under two names.
+
+ Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an empty
+ shape for each, and both use `skipFallback: true` so neither claims the bare
+ `launcher` / `menu` keys. This does not change the Studio page palette —
+ `app:launcher` remains recorded there as a shell singleton, which is a palette
+ decision independent of whether a declared type renders.
+
+ Three new strings — the launcher's and the menu's accessible names, and the
+ menu's empty state — are declared under `console.nav` in `en.ts` and its nine
+ sibling packs. An inline `defaultValue` alone is not a fix: it renders English
+ at one call site and leaves the string untranslatable everywhere
+ (objectui#3517).
+- faac0d9: `@object-ui/app-shell` now publishes a precise `sideEffects` ARRAY.
+
+ **What this means for a consumer.** Until now the package declared no
+ `sideEffects` field at all, which every bundler reads as "assume every module in
+ this package does something when it is imported" — so nothing in the package
+ could be tree-shaken, and importing one named export from the barrel pulled in
+ the barrel's whole reachable graph. The package now names exactly the modules
+ that DO something on import: its entry forms (including `./styles.css`, which a
+ bundler must never drop) and the ten modules that register SDUI widgets, admin
+ components and metadata resources at load time. Everything else is now
+ shakeable, so a consumer's bundler may drop the parts of `@object-ui/app-shell`
+ their app does not use.
+
+ ⚠️ **If your build depends on a module of this package being evaluated for its
+ side effects without importing anything from it, and that module is not one of
+ the ten named**, it may now be dropped from your bundle. Import the value you
+ need by name, or call the registration explicitly. Measured on this repo's own
+ console: 56,668 gzipped bytes left the eager closure and every SDUI registration
+ stayed present.
+
+ `"sideEffects": false` was NOT adopted and remains disproven by measurement: it
+ drops three live SDUI widget registrations (`mcp:connect-agent`,
+ `cloud:onboarding-next`, `cloud:ai-model-status`) to zero chunks on a green
+ build with no warning anywhere.
+
+ Two gates ship with the array, because an INCOMPLETE array fails silently inside
+ a consumer's bundle and would otherwise have no witness:
+ `scripts/check-side-effects-array.mjs` re-derives the enumeration from the module
+ bodies and fails when the array and the derivation disagree in either direction,
+ and `scripts/check-sdui-registration-pins.mjs` weighs the built console for every
+ registration the array promises to keep.
+- dbd5194: The shell's `sys_activity.type` reading stops calling every unrecognised type an
+ update (objectui#6730).
+
+ `mapActivityRows` in `hooks/sharedUserFeeds.ts` — the feed behind the AppHeader
+ bell's Activity tab, Home's activity card and the exported `ActivityFeed` panel
+ — carried the third hand-written reading of that column in this repo, and it
+ bucketed every value outside `created` / `deleted` / `commented` / `mentioned`
+ as `update`. That is not a missing decision; it is a wrong one stated out loud:
+ a `scheduled` meeting, a `login`, a nightly `system` rollup and an author's
+ `contract_countersigned` all rendered as "somebody updated this record".
+
+ - New `layout/activityItemType.ts` holds the whole reading — the table, the
+ generic bucket, the `"NOW()"` timestamp fallback and the row constructor that
+ applies all three — DOM-free, so what a row becomes is assertable directly.
+ - `ActivityItem['type']` gains a fifth kind, `system`: the generic bucket, with
+ its own icon, label and notification toggle. Following
+ `UNMAPPED_ACTIVITY_FEED_TYPE`'s precedent, an unrecognised value renders
+ through it and is named once on `console.warn` rather than being dropped —
+ `sys_activity.type` is author-extensible (objectstack#11507 direction 4), so
+ an unmapped value is real activity nobody has ruled on, not a mistake.
+ - The built-ins that had no honest presentation among the four existing kinds —
+ `system`, `completed`, `scheduled`, `login`, `logout` — now land in that
+ bucket instead of claiming `update`. `assigned` and `shared` stay `update`:
+ both write to the record.
+
+ ⛔ The two readings of this column are deliberately NOT converged.
+ `activityRowToFeedItem` builds a `FeedItem`, and the vocabularies cross:
+ `FeedItem` collapses create/update/delete into one `field_change` and drops
+ `commented` / `mentioned` outright, so routing this surface through it would
+ cost the bell every comment row and every create/delete distinction. What is
+ shared is a pin, not an import — the new suite reads plugin-detail's real table
+ (a devDependency; no runtime edge) and fails when the declared vocabulary grows
+ an entry this side has not read, or when the two readings stop disagreeing in
+ the three measured ways.
+- f99932a: Renderers for the `global:search` and `global:notifications` page blocks
+ (objectui#6757). A page that declared either member drew the literal "Component
+ Placeholder" scaffold: both are first-class `PageComponentType` members that the
+ 2026-08-26 maintainer ruling on objectstack#12183 kept declared once the
+ readiness read in objectstack#13117 evidenced both data sources shipped, and
+ the renderer was the remaining half.
+
+ Neither block adds a data layer — each is a new mount point on plumbing that was
+ already live:
+
+ - `global:search` mounts `useRecordSearch` (`@object-ui/react`), the same hook
+ the ⌘K command palette and the full-page search results already use. It
+ prefers the adapter's `searchAll` (`GET /api/v1/search` — cross-object hits
+ with title/snippet/record) and inherits that hook's fanout fallback for
+ adapters without it. Scope is the metadata provider's searchable object set,
+ which is the hook's documented default when no `objectNames` whitelist is
+ given.
+ - `global:notifications` mounts `InboxPopover` — the bell ADR-0012/ADR-0030
+ defines ("the bell reads `sys_inbox_message`") — over the shared inbox feed.
+
+ To keep that second one honest, the header bell's inbox wiring (rows, badge
+ addends, and the three mark-read paths) moves out of `AppHeader` into a new
+ `useInboxBell` hook that both surfaces mount. Copying it would have re-opened
+ the two defects `sharedUserFeeds` closed — #4225 (two owners of one read issuing
+ it twice per page) and #4316 (two derivations of read-state disagreeing) — so a
+ bell in the header and a bell an author declared on a page now have no
+ representable state in which they disagree about a row.
+
+ Both registrations publish **no** `inputs`: `ComponentPropsMap` declares an
+ empty shape for each ("declares no props at all" is the recorded intent), and
+ both use `skipFallback: true` so neither claims the bare `search` /
+ `notifications` keys. This does not change the Studio page palette —
+ `global:notifications` remains recorded there as a shell singleton, which is a
+ palette decision independent of whether a declared type renders.
+- 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone,
+ dropping the `reference_to` fallback arm (objectui#6837, half 2).
+
+ Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要
+ 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front
+ end just executes the protocol. objectstack#13847 landed the server half — a
+ `field-reference-to-alias` conversion rewrites stored `reference_to` to
+ `reference` on the serve path and in `os migrate meta`.
+
+ `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema`
+ declares. Measured on the installed 17.2.0: it refuses `reference_to`,
+ `referenceTo` and `target` with `unrecognized_keys`, each carrying its own
+ "Did you mean -> `reference`?" rename, while a nonsense key gets the same
+ refusal with NO rename hint and `reference` parses clean.
+
+ ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this
+
+ **This is a behaviour change for BYO consumers, and it is being stated rather
+ than shipped silently.** ObjectUI is usable without an ObjectStack backend
+ (`examples/byo-backend-console`), and a hand-written TypeScript schema passes
+ through no zod door, so nothing rejects the legacy spelling at authoring time.
+
+ **The break surface is narrower than "all BYO consumers", and this is the
+ measurement rather than a blanket claim.** Two ingestion choke points stamp both
+ snake_case keys from whichever spelling arrived — `MetadataProvider`'s type
+ cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any
+ def that passed either one already carries `reference` and is **completely
+ unaffected**. What is affected is exactly:
+
+ - **A `DataSource` implementation other than `ObjectStackAdapter`.**
+ `getObjectSchema` is a required member of the published `DataSource`
+ interface, and the readers call it on the generic `dataSource` (through
+ `useSettledSchema` and directly), so a host adapter's object schema reaches
+ them raw. Every in-repo example of one is on this path:
+ `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`,
+ `examples/byo-backend-console/src/mockDataSource.ts`,
+ `packages/runner/src/lib/mockDataSource.ts`,
+ `apps/site/app/components/galleryDataSource.ts`,
+ `apps/console/src/sdui-workbench-preview.tsx`,
+ `packages/plugin-grid/demo/bulk-actions.tsx`.
+
+ **Measured on this tree, none of those eight emits a relationship target at all** —
+ `reference_to` and `reference` are both zero in each, and
+ `examples/byo-backend-console` carries no lookup or master_detail field
+ anywhere (its only `reference` hits are a vite triple-slash directive and a
+ tsconfig `references` array). The single in-repo producer that WAS on this
+ surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer.
+
+ ⇒ **If you author object metadata by hand and spell a lookup's target
+ `reference_to`, rename that key to `reference`.** Symptom if you do not: the
+ target silently fails to resolve, and the affected surface degrades rather than
+ erroring — a related list is not derived, a gantt quick filter falls back to the
+ distinct values in the loaded rows instead of the referenced object's full
+ domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a
+ raw id, a chart's group-by labels stay unresolved.
+
+ The ingestion choke point now emits a **dev-mode warning** when a def arrives
+ carrying only `reference_to` or `referenceTo` and no `reference`. It names the
+ object, the field and the offending key, and points at this ruling. Stamping is
+ deliberately unchanged, so nothing that worked stops working. It is memoised
+ once per **(object name, field name, spelling, target value)** — every segment
+ of that key is pinned, in both directions, in
+ `reference-keys.legacyWarning-6837.test.ts`.
+
+ ⛔ **This warning does NOT cover the break described above, and it is worth being
+ exact about that rather than letting it read as mitigation.** It lives in
+ `normalizeFieldReferenceKeys`, reachable only through
+ `normalizeSchemaReferenceKeys`, which has exactly two production call sites —
+ `MetadataProvider` (metadata type `object`) and
+ `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the
+ warning fires precisely where the def still resolves and nothing is broken. A
+ hand-written schema served through any OTHER `DataSource` — the break surface —
+ reaches a reader raw: it never passes through this code and produces **no
+ warning at all**. On that path the failure is exactly as silent as before.
+ A reader-side or shared-resolver diagnostic, which would cover it, remains open
+ on objectui#6837.
+
+ ## What did NOT change
+
+ **Every key these readers EMIT is byte-identical**, and that was verified
+ mechanically over the whole diff rather than asserted. Eleven of the sixteen
+ sites write a target onto a bag whose own contract spells it `reference_to` (or
+ camelCase `referenceTo`): the six whose read and write share a line —
+ `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`,
+ `FilterConditionField`, `resolveActionParams` — plus five more that read on one
+ line and emit on another, and so are just as much emitters: `RecordDetailView`,
+ `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and
+ `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the
+ emitted key is what its target contract declares, and renaming it would be a
+ separate change.
+
+ **Three readers were deliberately left alone.** `LookupCellRenderer`
+ (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` —
+ ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and
+ never declares `reference`. They are fed by the emitters above and by published
+ example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so
+ narrowing them would break in-repo producers, and `plugin-grid`'s
+ `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly
+ those three sources — where `reference_to` is recorded with verdict
+ `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched.
+- 2a7ac32: Calendar views no longer render on invented field names (objectui#7029; ruled on
+ objectstack#13748, director batch #19, option A).
+
+ A view that carried no `calendar:` block used to have a complete-looking calendar
+ configuration synthesized for it. `ObjectCalendar` has always decided whether it
+ has a usable configuration by asking whether a start-date binding is PRESENT, so
+ the fabrication short-circuited its own refusal screen — "Calendar configuration
+ required. Please specify startDateField and titleField." — which existed all
+ along and was simply unreachable. Measured on a leave-request object whose real
+ fields are `start_date` / `end_date`: every record piled onto today's cell under
+ titles resolved through the display-name chain. A plausible, fully wrong screen,
+ with zero signal to the author.
+
+ Three faces were fabricating, on two independent routes to the same renderer:
+
+ - `app-shell/ObjectView` emitted `startDateField: 'due_date'` and
+ `titleField: 'name'` into `options.calendar` for every object view;
+ - `plugin-list/ListView`'s calendar branch floored the same two bindings at
+ `'start_date'` / `'end_date'` one layer down;
+ - `plugin-view/ObjectView.generateViewSchema` — the authored `object-view`
+ element route, which bypasses `ListView` entirely — carried its own copy.
+
+ All three now forward only what the author declared. This converges the calendar
+ on the shape its siblings already had: `timelineViewOptions` (objectui#3129
+ retired this very literal from the timeline axis), the kanban lane detector
+ (ADR-0085, "never invents a field the object doesn't have"), and
+ `defaultCalendarFromObject` (a binding, or nothing).
+
+ **Behaviour change, loud over silent.** With no binding to forward, ADR-0047's
+ capability gate stops offering the Calendar toggle to views that configured
+ none, and a view forced onto the calendar renderer reaches the refusal screen
+ instead of a wrong one. A view that happened to sit on an object carrying a real
+ `due_date` field was rendering by luck; it now refuses until its `calendar:`
+ block is written. Correctly configured calendars are unaffected — same fields,
+ same render. The same deletion also stops the fabricated name from answering for
+ the Timeline switcher, which accepts a calendar binding as a legitimate axis.
+
+ The spec half — cross-field validation rejecting a half-written declaration at
+ authoring time — is objectstack#13817. This half makes the runtime honest
+ independent of which spec version the host pins.
+- 5f4514f: Gantt views no longer render on invented date field names (objectui#7070).
+
+ The half PR #7062 fenced out and reported separately. A view that carried no
+ `gantt:` block used to have a complete-looking date axis synthesized for it:
+ all three faces floored `startDateField` at `'start_date'` and `endDateField`
+ at `'end_date'` — field names no view had written and most objects do not
+ carry.
+
+ `ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props
+ are present, so the fabricated pair short-circuited the renderer's own refusal
+ screen — "Gantt configuration required. Please specify startDateField,
+ endDateField, and titleField." — which existed all along and was simply
+ unreachable from every route. The same fabrication answered ADR-0047's
+ capability gate in `ListView.availableViews`, so the Gantt toggle was live on
+ every object view in the product.
+
+ ⚠️ The premise was MEASURED before anything was deleted, because #7029's
+ mechanic is only correct where a refusal path exists and that had never been
+ established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an
+ absent binding — it does not render empty, and it does not throw.
+
+ Three faces were fabricating, on two independent routes to the same renderer:
+
+ - `app-shell/src/views/ObjectView.tsx` — the console object page. The inline
+ branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and
+ `timelineViewOptions`: the declared block spread whole, title floored at
+ `'name'`, no date field invented.
+ - `plugin-list/src/ListView.tsx` — the render branch AND the capability gate.
+ - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored
+ `object-view` element route, which bypasses `ListView` entirely.
+
+ **What changes for an author.** A view that declared no gantt configuration is
+ no longer offered the Gantt toggle, and one forced onto the renderer reaches
+ the refusal screen instead of a plausible, fully wrong chart. A view that
+ declared a binding is unaffected — the declared block is forwarded exactly as
+ before, every spec key included.
+
+ Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx`
+ certified the gantt branch below it as already using the safe two-rung shape.
+ It did not. The note now states each sibling branch as measured, and says
+ explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the
+ two plugin faces, which objectui#7070 routes to a ruling rather than settling
+ per-face.
+
+ Deliberately out of scope, and left in place: `progressField` / `dependenciesField`
+ (not date axes, different absent-value semantics) and the timeline `'created_at'`
+ posture conflict.
+- 351eb31: Converge the lookup/user widget metadata on the spec's camelCase — one concept, one
+ spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19).
+
+ **BREAKING, deliberately, with no deprecation window.**
+
+ Two published contracts declared OPPOSITE dialects for the same four lookup keys, and
+ `@object-ui/fields`' read chains served both — snake FIRST, so the dialect the object
+ contract *refuses* outranked the one it *declares*:
+
+ | | `@objectstack/spec` `FieldSchema` (object metadata) | `@object-ui/types` `LookupFieldMetadata` (widget metadata) |
+ |---|---|---|
+ | camelCase | **declared** | compile error (`TS2561`) |
+ | snake_case | refused (`unrecognized_keys`) | **declared** |
+
+ `LookupFieldMetadata` and `UserFieldMetadata` now declare the spec spellings, and the
+ snake members are **removed**:
+
+ | before (removed) | after |
+ |---|---|
+ | `display_field` | `displayField` |
+ | `description_field` | `descriptionField` |
+ | `lookup_filters` | `lookupFilters` |
+ | `id_field` | `idField` |
+
+ **Migration.** Rename those four keys wherever you author lookup or user field metadata
+ — `LookupFieldMetadata` / `UserFieldMetadata` objects, and any `DataSource.getObjectSchema`
+ that returns them. The old spellings are no longer read: a def still carrying
+ `display_field` falls back to the referenced record's generic name heuristic rather than
+ the field you named.
+
+ `idField` is kept as a **widget-contract** key. It carries objectstack#3508's machine-name
+ hydration — committing a record field other than the id as the lookup's stored value —
+ which is picker behaviour with no `FieldSchema` twin, and none owed.
+
+ **Not renamed** (outside this ruling's four keys, still snake on the widget bag):
+ `reference_to`, `title_format`, `lookup_columns`, `lookup_page_size`, `depends_on`,
+ `allow_create`, `avatar_field`. `reference_to` in particular **stays** — the adapter's
+ `normalizeSchemaReferenceKeys` choke point genuinely stamps it onto every def.
+
+ Also moved with the rename: `content/docs/fields/lookup.mdx` and `user.mdx` (whose
+ snippets CI compiles against the built `d.ts`), all seven in-repo producers, and the
+ inline-edit enrichment allow-list in `@object-ui/plugin-detail`. `plugin-grid`'s
+ `relationalMetaKeys.ts` drops the four `legacy-alias` verdicts and retires that verdict
+ class; its gate is restated to assert the class no longer exists rather than passing
+ vacuously.
+- 1cca678: Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509,
+ maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049).
+
+ **What changes for an operator.** A stored dashboard whose header came from a legacy
+ root `title` now shows its `label`. `label` is the only header source, then the raw
+ `name`.
+
+ Per surface:
+
+ - Console dashboard page (`DashboardView`) — header falls to `label`, then `name`.
+ - Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document
+ with no `label` now shows no header title at all.
+ - The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to
+ `label`, then the generic `Dashboard`.
+ - Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage`
+ heading) — both fall to `label`, then `name` / the generic heading.
+
+ **Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name**
+ (`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no
+ authored dashboard can acquire the key, and what retires is compatibility with documents
+ stored before that refusal existed. Until now five surfaces read the legacy spelling
+ independently, which meant a legacy document could show one header in the console and a
+ different one in the designer. One spelling now answers everywhere.
+
+ **Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard
+ already carries it and needs no change — it simply starts showing that `label` instead of
+ the legacy `title`. A document carrying `title` and no `label` was already invalid; give
+ it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found
+ 9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'`
+ component examples that declare no `header`, so none of them rendered a header title
+ either before or after.
+
+ **Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared**
+ key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings,
+ the designer's widget-title input and its per-locale write path all behave exactly as
+ before. Root and widget arms were separated by receiver, and the retirement's pins carry
+ widget-level controls on every surface for that reason.
+- d80447d: Stop inventing a gallery cover binding, and offer the view switcher only when it
+ can switch (objectui#7547).
+
+ **The object page no longer floors `gallery.imageField` at `'image'`.** It used
+ to supply that key for every object view, declared or not. The screen damage was
+ small — `ObjectGallery` collapses the cover area when no record yields a cover —
+ but the key fed `ListView`'s ADR-0047 capability gate, which reads
+ `options.gallery.imageField`, so **Gallery was offered on every object view that
+ whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now
+ forwards the view's own declared block (both legacy cover spellings still
+ cross-fill each other, and `titleField` keeps its `'name'` display floor) and
+ emits no cover key when the view declared none. Same class and same route as
+ objectui#7029 (calendar) and objectui#7070 (gantt dates).
+
+ ⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery
+ toggle. That is the ADR-0047 rule working: it was only ever offered because this
+ relay answered the gate on the author's behalf.
+
+ **The visualization switcher is drawn for the resolved list, not the whitelist.**
+ `showViewSwitcher` was computed from the LENGTH of
+ `appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects
+ it with the capability gate — so a view whitelisting `['grid', 'timeline']` with
+ no timeline block drew switcher chrome around a single Grid entry. The predicate
+ now lives at the one site that holds both halves. Views whose whitelisted types
+ all resolve are unaffected.
+- 91f9276: **The maker's edit-mode starter offers sample data, not an automation v1 cannot
+ build (objectui#7709).** Bound to an existing app (`?package=`), the maker's
+ empty state offered four starters: add a field, add an object, add a dashboard,
+ and 「加一个自动化 —— 审批、状态流转或通知」. Approval, status flow and
+ notification are all refused by ADR-0112 v1 (cloud#1956 / PR #1970), and the
+ measured behaviour on the sibling chips was not a refusal but a silent degrade
+ into a view — so the product recommended an automation and would have handed
+ back a page.
+
+ Rewording it was not available: asking for a field, a view or a dashboard
+ duplicates one of the three chips beside it. The fourth chip is now
+ `addSampleData` —「给现有对象补一批贴近真实的示例数据,好拿去演示。」 — in all
+ ten packs and in the call-site `defaultValue` fallback, which is a second copy
+ of the same string. The three surviving chips all add STRUCTURE; what an app
+ that already has objects most often lacks is DATA, and `seed` is on v1's
+ authoring whitelist. A note beside the keys in every pack and at the call site
+ says this chip's automation wording comes back when ADR-0112 v2 re-adds flows
+ and actions, and the retired sentence for each pack is kept in the guard suite
+ so v2 has it verbatim.
+- bdb4dbd: A half-filled relationship field stays in the client and is never PUT (objectui#7714,
+ maintainer ruling on objectui#7122 item 4, 2026-09-05).
+
+ **Breaking, deliberately, and stated rather than implied.** Both metadata writers —
+ `MetadataService.saveFields` / `saveObject` in `@object-ui/app-shell`, and
+ `MetadataFieldsPage` in `@object-ui/plugin-designer` — now REFUSE a `lookup` or
+ `master_detail` field whose `reference` is missing, empty, blank or not a string. A
+ caller that previously got a PUT now gets a thrown error and **no request at all**. The
+ refusal is raised while the wire `fields` map is being built, so nothing is sent and
+ nothing is partially applied.
+
+ **Why the draft may not leave the client.** `@objectstack/spec` 17.3.0 turned
+ `reference` from prose into a hard requirement on relationship types (a `custom`
+ refinement at path `reference`). Driven against a real 17.3.0 backend in a running
+ designer: creating a `lookup` and leaving its target empty PUT the whole object,
+ came back `422 INVALID_METADATA` at `fields..reference` — and then the NEXT
+ edit, to a different and already-saved field, was refused identically, because the
+ half-filled draft rides along inside the same document. The author sees that later
+ edit rendered as applied while the server has none of it, and the only escape that
+ does not require noticing the lookup is a reload, which discards the work. An editing
+ session's half-finished state belongs to the client, not to the metadata store.
+
+ ⛔ **Not** "strip the incomplete field from the body and report a successful save".
+ That shows the author a field the server never received — the silent-drop shape
+ objectstack#4001 closed.
+
+ ⚠️ **A declared divergence: objectui is stricter than the contract here.** The
+ predicate is `typeof reference === 'string' && reference.trim() !== ''`. Measured on
+ the 17.3.0 artifact, at field level and again through `ObjectSchema`, the spec
+ ACCEPTS a whitespace-only `reference` while refusing an absent or empty one — so
+ `reference: ' '` is refused by these writers on their own authority, not the
+ platform's. Kept deliberately: whitespace names no object (the spec's own
+ `ObjectSchema.fields` key grammar `/^[a-z_][a-z0-9_]*$/` admits no whitespace-bearing
+ name), so admitting it only moves the identical failure past the PUT and into a stored
+ document, where it surfaces with no field named. Filed upstream as objectstack#16126;
+ if the spec trims, behaviour here is unchanged and only the declaration retires.
+
+ The refusal message diagnoses which of the four states it found — absent, empty,
+ non-string (`invalid_type`, a value of the wrong kind rather than a missing target),
+ or blank — because the repair and the consequence differ per state.
+
+ `minor` rather than `major` per `AGENTS.md`: objectui's major tracks `@objectstack`'s,
+ so objectui's own breaking changes ship as `minor` with the breaking semantics stated.
+- a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered"
+ (objectui#7741).
+
+ `ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the
+ import wizard hides its saved-mapping selector on an empty list. So "the server served
+ zero mappings" and "the server refused, or broke" produced the identical UI on every
+ deployment — the feature simply absent — with a `console.warn` as the only
+ discriminator, in the browser console, with nothing pointing at it. That silence did not
+ merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter
+ (objectstack#14026 was filed, routed and worked by two seats against a wizard that had
+ been correct since `@object-ui/data-objectstack@17.1.0`).
+
+ **The empty-list return is unchanged.** `listImportMappings` still answers
+ `Promise` and still never throws, on every arm including the loud ones — this is
+ a channel added ALONGSIDE that contract, not a change to it.
+
+ - **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe
+ channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a
+ metadata read failed in a way that is NOT the supported "this deployment does not
+ serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the
+ object, whether the server `refused` this caller or the answer was `unreadable`, and
+ the server's own ADR-0112 code, HTTP status and message.
+ - **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported
+ so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code`
+ first, the status only where no code was declared — and never "is the result an empty
+ array", which is what both conditions produce and so can never tell them apart.
+ - **The older-server case stays quiet.** A deployment that does not serve the `mapping`
+ kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list
+ door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no
+ event. That is a real, supported deployment shape and it must not become a visible
+ fault.
+ - **The console now says so.** `AdapterProvider` subscribes to the new channel and
+ renders a warning toast naming the object, the remedy and the server's own words, so a
+ user without devtools open can tell "there are none" from "we could not find out".
+ Three new `console.importMappings*` keys ship in all ten locale packs.
+
+ This applies framework #13906 decision 1 option A — *a thing that could not be READ is
+ not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not
+ a new principle.
+- 151facf: **Breaking for the page designer's output:** the page-block inspector's `object-kanban`
+ panel now authors `groupBy` (the key the board reads) instead of `groupField` (the key it
+ never read), and offers the `limit` control the schema has declared since objectui#7322.
+
+ **What was measured, on this branch's base (`0c8dbc49`).** `BLOCK_CONFIG['object-kanban']`
+ offered exactly four controls — `objectName` / `groupField` / `titleField` / `cardFields`.
+ `ObjectKanbanSchema.groupBy` is REQUIRED on both published faces (`objectql.ts:2765
+ groupBy: string`, no `?`; `objectql.zod.ts:1001 groupBy: z.string()`, no `.optional()`) and
+ had **no control at all**, while `groupField` — the only control able to set grouping — has
+ been a `retirementTombstone()` on the Zod face and `?: never` on the TS face since
+ objectui#7322. `ObjectKanban.tsx` reads `schema.groupBy` at thirteen sites and `groupField`
+ at zero (control: those same thirteen hits in the one query, so the zero is a reading).
+
+ Run against the node those four controls produced, `ObjectKanbanSchema.safeParse` reported
+ **two** issues, not one: `groupBy` "expected string, received undefined", and `groupField`
+ carrying the retirement guidance verbatim. So this panel could not author a valid
+ `object-kanban` node however it was filled in, and the board it produced grouped nothing
+ with no diagnostic on any face. Both halves are now pinned against the schema itself rather
+ than against a spelling, in `previews/__tests__/block-config.test.ts` —
+ `scripts/check-designer-field-key-parity.mjs` judges `PAYLOAD_SHAPES` and does not read
+ `BLOCK_CONFIG`, so nothing mechanical was watching this table.
+
+ `limit` is not polish: `ObjectKanban.tsx` sends it as a real `$top`
+ (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) and renders every fetched record into a lane
+ with no pagination, so a board over 100 records was silently truncated with no way to widen
+ it. The new number box states `100` — `DEFAULT_KANBAN_LIMIT`, read from the renderer's own
+ source by its pin — as the value that applies while it is empty.
+
+ **Stored pages.** Documents saved by every released build since
+ `@object-ui/app-shell@17.5.0` can carry `properties.groupField` on an `object-kanban` block.
+ The inspector's read door now strips it (`stripRetiredBlockProps`, keyed to
+ `RETIRED_BLOCK_PROP_KEYS`), so an edit-and-save round-trip comes out without the retired
+ key — no migration pass, and no blanket unknown-key purge: every other key the designer
+ does not render still survives. It is a strip and **not** a migration into `groupBy` on
+ purpose: reading the old spelling into the new box would be a second de-facto alias
+ (AGENTS.md #0.1) for a value no board ever acted on.
+
+ The **view-level** `kanban.groupField` alias (`core/src/utils/normalize-list-view.ts`) is
+ live and untouched — node-level retirement and the view-level alias are different things.
+
+ The two locale entries move with the control (`engine.inspector.pageBlock.field.object-kanban.groupBy`,
+ plus a new `.limit`); the English wording is unchanged, so the panel reads the same as it
+ always did.
+- 31a0d05: Emit the spec-canonical kanban lane key from the object page, and teach the
+ capability gate to recognize it (objectui#8193).
+
+ `ObjectView` built the view-level kanban config it hands to `list-view` and wrote
+ the deprecated alias `groupField` — never `groupByField`, the key
+ `@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it
+ already READ the canonical key first. Its sibling producer in the same package,
+ `defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and
+ left the reasoning next to itself ("that read-site now prefers the spec key, so
+ one key is enough"); the twin was not carried along, so one producer surface
+ spoke two vocabularies for one concept depending on which entry point you
+ arrived through. `ObjectView` writes into `options.kanban`, which
+ `normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing
+ corrected it downstream and the legacy spelling was what drove the lanes.
+
+ The expression is now the exported `kanbanViewOptions`, the fifth member of the
+ `timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` /
+ `galleryViewOptions` family it had been the odd one out of.
+
+ **`ListView`'s kanban capability gate changed with it, and had to.** The gate
+ consulted the `options.kanban` bag for the LEGACY spelling only, so a producer
+ writing the spec key into the bag was invisible to it while still rendering
+ correctly — the render branch merges the bag and resolves
+ `groupByField || groupField`, so the gate recognized strictly less than what
+ renders. Measured before and after: a bag of `{groupBy, groupField}` offered
+ Kanban and `{groupBy, groupByField}` did not, so shipping the producer change
+ alone would have removed the Kanban toggle from every object view. The gate now
+ reads both spellings out of the bag — the same one-question-two-sites repair
+ objectui#5042 made for `map` and objectui#7544 for `chart`.
+
+ **No alias READ was removed, and the alias is not retired.** Stored metadata
+ still authors `groupField`, and every read site still resolves it; `ListView`
+ already preferring the canonical key is precisely why the sibling producer could
+ drop the alias write. What changed is only what this one face WRITES, plus one
+ added rung on the gate.
+
+ **Migration.** Nothing authored has to change. If you read
+ `options.kanban.groupField` off the schema `ObjectView` produces, read
+ `groupByField` (or both) instead — that bag now carries the spec spelling.
+
+ The view-level `groupBy` in the same bag is untouched. It is not a spec key
+ either — measured against the strict `KanbanConfigSchema`, which refuses it by
+ name — but `ListView`'s projection collectors read it, so retiring it needs its
+ own producer census and is filed as objectui#8213.
+- d06fba8: The console record page and the `record:activity` block read ONE `sys_activity`
+ type table, so a scheduled meeting no longer appears on one and vanishes on the
+ other.
+
+ `RecordDetailView`'s `sys_activity` merge carried a hand-written copy of the
+ table that `record:activity` exports as `ACTIVITY_TYPE_TO_FEED_TYPE`. Neither
+ file imported the other and nothing compared them, so the two could drift
+ silently — and they had. objectui#5840 added `scheduled` -> `event` to the
+ exported table, because a shipped producer (HotCRM's `schedule_meeting`) writes
+ that value and the row was being dropped before any filter ran. The copy here
+ was left untouched, so the same row rendered on a hand-authored record page and
+ was dropped on the console record page: same record, same row, two answers.
+
+ `RecordDetailView` now imports the exported table, and the copy is gone. The
+ table is re-exported from `@object-ui/plugin-detail`'s entry point, which is
+ what makes a single reading possible at all — it was previously reachable only
+ from inside the plugin. No module enters the eager closure: the module holding
+ it was already pulled in by the renderer beside it.
+
+ Adding an activity type is now one edit, in one place, that both surfaces see.
+ A re-fork is caught rather than merely discouraged: the new pins spy on the
+ shared object and inject a member into it at runtime, so a private copy holding
+ today's members exactly — the failure this change removes — fails, where a
+ value comparison would pass on the defect.
+
+ Only the table converges. The row-to-`FeedItem` construction around it is still
+ written twice, so an unmapped activity type is still dropped silently on the
+ console surface where the block warns once; that mirror is filed separately.
+- 12295b6: `provisionProductionEnvironment` now REFUSES a success payload whose `data`
+ carries no `environment` row, instead of resolving best-effort (objectui#6707).
+
+ objectui#6629 fixed this consumer to read the created environment from the
+ nested `environment` key, but deliberately left the envelope check alone: it
+ catches a **missing** `data` and says nothing about `data`'s **shape**. So a
+ producer that regressed to a flat payload would once again resolve successfully
+ with `id` and `hostname` both `undefined` — the same silent outcome #6629 had
+ just fixed, reachable again by a producer change alone.
+
+ A flat payload is a producer contract violation, not a second dialect to be
+ tolerated, and tolerating it is how the original defect stayed invisible. The
+ call refuses it now, which routes a producer regression to this call's already
+ documented failure path rather than a successful-looking no-op: the sole caller
+ (`CreateWorkspaceDialog`) already wraps the call in `try`/`catch`, logs a
+ warning, and lets the onboarding gate re-provision lazily on first navigation.
+ Workspace creation itself is unaffected — the caller does not re-throw, and it
+ never read this call's return value.
+
+ The refusal carries its own diagnostic, distinct from the missing-envelope one,
+ so a logged warning still distinguishes "the control plane did not wrap the
+ payload" from "it did not put the row where it says it does".
+
+ The wire shape this is written against is confirmed producer-side rather than
+ inferred from this consumer — the distinction is load-bearing, because before
+ #6629 the only in-repo artifact pinning this payload was a hand-written mock
+ pinning the bug shape. Both sources are recorded on the function's docblock.
+- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id.
+
+ A record waiting on a position rendered its approver as `positi…ager` — the
+ engine reference `position:sales_manager`, 22 characters, past the identity
+ formatter's 14-character truncation arm and middle-truncated to fit its chip. The
+ step names beside it were human prose; the one line answering *who is holding
+ this record* was an internal identifier, and not even a complete one. The same
+ reference reached the admin-override confirm dialog un-truncated, so a paragraph
+ of plain governance prose ended `— position:sales_manager` (objectui#5414).
+
+ Both surfaces now resolve the reference before rendering, in three tiers, most
+ authoritative first. The server's own `pending_approver_names` wins whenever it
+ answers, and a backend that resolves its own slate costs the record page no extra
+ request. Otherwise the console reads the directory row the spec's approver
+ binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and,
+ for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no
+ adapter and no row, the machine name still prettifies into prose rather than
+ truncating. The raw reference stays on hover, which is where an internal
+ identifier belongs.
+
+ An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)`
+ is actionable where `positi…ager` is not, and it is the motivating rescue case
+ for the admin-override path. Staffing is deliberately tri-state — a
+ `sys_user_position` read the viewer is not permitted to make leaves the seat's
+ staffing UNKNOWN and says nothing, because "I could not look" is a different
+ claim from "nobody holds it" and only one of them is safe to print on a
+ governance surface.
+
+ Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed`
+ and `approvalsInbox.approverNameSeparator`. The separator is a translated
+ punctuation key rather than `Intl.ListFormat`, which was measured on this tree
+ joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with
+ no separator, reading as one person's name.
+
+ The directory-backed kinds and their value columns are read from
+ `@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new
+ approver type is covered the day the spec publishes it. Id-valued kinds
+ (`user` / `team` / `department`) keep the existing middle-truncation: a row id
+ has no prose to recover, and that arm is objectui#3461's answer, not this card's
+ defect.
+- 6bd343d: The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio//interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app::build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio.
+- 1939c96: 设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one.
+- b470e91: The metadata designer states its package on the publish step, not only on the save (#5420)
+
+ Studio's designer save→publish loop bound the draft to a software package on the
+ save (`PUT ?mode=draft&package=`) and then sealed it with a publish that named
+ no package at all. `objectstack#10354` (shipped in `@objectstack/rest` 17.2.0) taught
+ `POST /meta/:type/:name/publish` to accept `?package=`, so the second call can now
+ state the same binding the first one already states.
+
+ - `MetadataClient.publish()` accepts `packageId` and sends `?package=`, the same
+ wire spelling and the same `encodeURIComponent` treatment `save()` gives it.
+ - `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation
+ (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The
+ `?package=all` "show everything" scope keeps folding to "no package".
+
+ The parameter is **omitted**, never sent empty, when the designer holds no binding.
+ Empty and absent are the same to the framework's normaliser today, but absent is the
+ shape the save door already followed, and the framework's promotion path branches on
+ the key being present downstream.
+
+ What this buys is **reachability**, not speed: it lets `#9612`'s package-closure
+ narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That
+ narrowing has a second, independent gate this does not touch — objects carrying no
+ `_packageId` provenance are kept unconditionally — so on a tenant-authored overlay
+ corpus stating the package still narrows nothing.
+- f352a69: flow designer: the Start node's **Entry condition** now opens in the row-based
+ condition builder, with raw CEL one click away as the escape hatch (objectui#6226).
+
+ The builder is given the trigger record's own vocabulary — bare field names and
+ `previous.