diff --git a/.changeset/3719-settings-valuedomain-combobox.md b/.changeset/3719-settings-valuedomain-combobox.md deleted file mode 100644 index f39d01bbcc..0000000000 --- a/.changeset/3719-settings-valuedomain-combobox.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Setup's settings selects now follow the specifier's `valueDomain` declaration instead of -treating the curated `options` table as the domain (objectui#3719). - -Since objectstack#5712 / PR objectstack#6581 a settings specifier may declare -`valueDomain` (`iana_time_zone` | `iso_4217_currency` | `iso_3166_alpha2`), and when it -does the **standard's membership is the enforcement boundary** — the server accepts -`timezone: 'Europe/Zurich'` and `currency: 'CHF'`, neither of which is in the manifest's -list. The console kept drawing those keys as closed dropdowns, so an admin could author -only the 17 curated zones and 9 curated currencies while the contract took the whole -domain; every other legal value was reachable by API or `OS_LOCALIZATION_*` env only. The -keys' own descriptions had promised "IANA zone" / "ISO 4217 code" all along. - -`case 'select'` in `SettingsField` now keys the control off the declaration. Declared → -an editable combobox: the curated options stay on as suggestions (native ``, the -same suggest-but-allow-anything affordance `FlowReferenceField` uses — no new dependency), -free text is committed verbatim, and an out-of-domain value is refused by the server with -`invalid_value` + `constraint: { valueDomain }` into the field-error slot that already -exists. - -**Undeclared → the closed dropdown is untouched**, which is half the change rather than a -caveat. Those `options` are still exhaustive under objectstack#5131 (the sms/mail provider -selects), and `localization.locale` had its domain declaration deliberately **rejected** in -objectstack#6515 because its options *are* the shipped catalogs. Widening those to free -input would be a regression wearing this fix's clothes, so the two branches are pinned -against each other from the specifier data rather than from a list of key names — a key -that gains a domain server-side joins the right side of the pin with no edit here. - -Root cause, because it will recur: `Specifier` in `pages/settings/types.ts` is a -hand-written **local mirror** of the server's shape, not an import, so nothing tells it when -the schema grows — and TypeScript reports nothing, because a narrower mirror is a -structurally valid reading of a wider object. `valueDomain` is added there and the file -header now says to check the mirror first when a settings feature "doesn't render". diff --git a/.changeset/3880-es-done-listo.md b/.changeset/3880-es-done-listo.md deleted file mode 100644 index 032c5075e8..0000000000 --- a/.changeset/3880-es-done-listo.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The Spanish pack renders `Done` as `Listo` at every one of the four sites that say it -(objectui#3880, triage adjudication 2026-08-09). `grid.bulk.done` — the footer button that -dismisses the bulk-action result dialog — read `Hecho` while `common.done`, `view.done` and -`form.fullscreen.done` all read `Listo`, so the same English word rendered two ways in -Spanish across dialogs a user meets in one session. - -Adjudicated a typo rather than a deliberate contextual split, on three checks. All four -keys hold the byte-identical `en` value `Done`, and all four call sites are the same -control: a dialog-footer button whose click finishes or dismisses the surface -(`BulkActionDialog` `onClose(result)`, `ManageViewsDialog` `onOpenChange(false)`, -`fullscreen-editor` `commitFullscreen`, `InviteMemberDialog`'s invitation-created footer). -The nine other packs each render all four identically (de `Fertig`, fr `Terminé`, pt -`Concluído`, ru `Готово`, ja `完了`, ko `완료`, zh `完成`, ar `تم`), so no other translation -pass had found a context worth splitting on. And the neighbouring `Hecho`/`Deshecho` -pairing that could have justified it does not hold: `grid.bulk.undo` is the verb `Deshacer`, -and `Deshecho: ` is `undonePrefix`, a result-line status rather than a button. - -`Hecho` moved to the 3:1 majority `Listo`, which is the value objectui#3546 slice seven had -already chosen for `common.done`. `packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -pinned the old outlier as a recorded example of deliberate divergence; that pin now asserts -the four as one value instead, and its note keeps the history plus the `Pending`/zh row, -which remains a genuine deliberate split. - -No `en` value changes, so no other pack is asked to follow. This is the value half of -objectui#3880 only — the card's 281/164 shared-string census stays on the card as -documentation, and is explicitly not a gate: 164 of those groups diverge legitimately. diff --git a/.changeset/3909-query-params-filter-union.md b/.changeset/3909-query-params-filter-union.md deleted file mode 100644 index e17e77ec51..0000000000 --- a/.changeset/3909-query-params-filter-union.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/fields': patch ---- - -`QueryParams.$filter` now declares both shapes the data sources actually accept — the -MongoDB-style field-keyed record, or a `FilterArray`, the ObjectQL AST sugar bound from -`@objectstack/spec/data` (objectui#3909). - -**Nothing is narrowed and no accepted value changes.** `Record` already -accepted arrays structurally — they satisfy its string index — so the union documents -shapes that were always legal rather than admitting new ones. Measured both ways under -`tsc --strict`: all five inputs `translateFilterToAST` enumerates assign to the old and -new declarations alike, and both reject a bare number and a bare string identically. A -downstream `turbo run build` over all 43 dependent packages is green, which is the -evidence a published type change breaks no consumer. - -The harm was entirely on the type face, and it was two-sided. The declaration blocked -nothing while describing one legal shape as though it were the only one — objectui#3831 -is what that cost, a rule array accepted by a `Record` slot, object-spread -flattened to `{"0": {...}}`, types green, and the query filtering on a column literally -named `0`. And someone writing a new consumer would read the type and its record-only -`@example`, conclude the array path was illegal, and add a tolerant conversion for it — -the "widen the consumer to tolerate the producer" shape AGENTS.md #0.1 forbids. Two -producers have fed arrays through this slot all along: `plugin-list`'s -`buildEffectiveFilter` (grid and export) and `plugin-view`'s `ObjectView` (calendar / -kanban / gallery / timeline). The runtime was right; the declaration was narrow. - -The array half is **bound** to the spec's `FilterArray` rather than restated locally, so -it cannot fork from the vocabulary the servers parse — the same failure two hand-written -operator lists had in objectui#3948. The doc comment names `translateFilterToAST` as the -authoritative accepted set instead of carrying a second list to drift from. - -`@object-ui/fields` drops the local cast this defect forced. PR objectui#3908 wrote -`filter as Record` at one assignment in `useRecordQuery`, deliberately, as -debt rather than widening the shared type. `hasFilter` is now a type predicate narrowing -to the `$filter` slot's own type, so the assignment needs no cast and the guard cannot -drift from the declaration it guards. Type-only throughout; no runtime behaviour changes. diff --git a/.changeset/3917-retire-action-condition-branch.md b/.changeset/3917-retire-action-condition-branch.md deleted file mode 100644 index 717b205f5b..0000000000 --- a/.changeset/3917-retire-action-condition-branch.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire the `ActionCondition` `{ expression, then, else }` branch shape from -`ActionSchema.condition` (objectui#3917, maintainer ruling 2026-08-09 route B, -enforce-or-remove). - -`@object-ui/types` declared `condition` as a branch DSL — `expression` plus `then` / -`else` sub-actions — and shipped a zod mirror (`ActionConditionSchema`) that accepted it. -**Nothing ever read `expression`, `then` or `else`**: a repo-wide grep for -`condition.expression|then|else` has zero non-test hits. The only consumer of the key is -`ActionRunner.execute`, which reads it as a **predicate gate** (boolean / bare CEL / -`${...}` template / `{ dialect, source }` envelope). A branch object carries no `source`, -so the runner's normalizer read it as "no gate declared" and executed the action -unconditionally: the predicate was never evaluated, `then` / `else` were never dispatched, -and `os validate` / `os build` stayed green with zero diagnostics. Two docs pages taught -the shape with worked examples, so an author following the documentation -("amounts over 1000 go to manager approval") got unconditional execution. - -What changes: - -- `ActionCondition` is removed from `@object-ui/types` (and from the barrel export). -- `ActionSchema.condition` is retyped to the predicate the runtime actually honours: - `boolean | string | { dialect?: string; source: string }` — the same three arms - `ActionRunner`'s own `ActionDef.condition` carries, and the same vocabulary `visible` - and `disabled` use. -- `ActionConditionSchema` is removed from `@object-ui/types/zod` (and from the zod - barrel); the `condition` key now validates against that predicate union. -- The two teaching sites (`content/docs/core/enhanced-actions.mdx` Conditional Execution, - `content/docs/api/schema-reference.md` ActionSchema table) are rewritten to the live - vocabulary: `condition` is a gate; a branch is expressed as separate actions with - mutually exclusive `condition`s. - -**The zod parse verdict flips in both directions**, measured on `origin/main` @ `2aff580b5` -against this branch: the branch object went from **accepted** to **refused** -(`invalid_union` on `condition`), and every live predicate spelling — `false`, -`'data.amount > 1000'`, `'${data.amount > 1000}'`, `{ dialect: 'cel', source: ... }` — -went from **refused** to **accepted**. The old schema required `expression`, so the shape -the runtime honours was the one the schema rejected. - -**Breaking for TypeScript authors of `ActionCondition` and for metadata authoring -`condition: { expression, then, else }`** — marked `minor` per this repo's -version-alignment rule, which reserves `major` for following `@objectstack` across a major -(AGENTS.md 版本号策略; same classification as the `MobileOverrides` retirement, -objectui#4919). Runtime behaviour is unchanged: an authored branch object did nothing -before and does nothing now. What changes is that the contract no longer claims otherwise -— the mistake now surfaces at authoring time as a type error and a named zod refusal, -instead of a silent no-op that type-checks, validates and runs the action anyway. diff --git a/.changeset/4420-bulk-delete-visiblewhen.md b/.changeset/4420-bulk-delete-visiblewhen.md deleted file mode 100644 index eafb37e6e6..0000000000 --- a/.changeset/4420-bulk-delete-visiblewhen.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-list': patch ---- - -The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen` -per selected record (objectui#4420). It used to read that key as a bare boolean — the -object-level verdict only — so ticking a record the author's predicate excludes still -offered the red Delete, and pressing it deleted the record the predicate was written to -protect. The row kebab on the same screen hid its Delete correctly, so one declared key -meant two different things on two surfaces. - -Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the -operation and report the skipped**. The bar evaluates the predicate once per selected -record, the delete runs over the allowed subset, and the excluded records are reported -rather than silently dropped. The button itself is never hidden or disabled by the -predicate — a mixed selection is not punished for one stray tick — and a selection where -every row is excluded is a legible refusal rather than an unexplained absence. - -- `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of - `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once. - A bulk gate evaluates N records in a loop, which is why it can never be a hook. -- `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through - `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped - count; a selection with nothing excluded keeps the consumer's own delete flow - untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` — - the bulk half of the same predicates, gated on the object verdict rather than on the - row `onDelete` wiring. The dialog declines to run over zero records. -- `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters - the built-in `delete` to the eligible subset and states the skipped count inline. - -Custom bulk action ids are untouched: they route through the action runner carrying -their own gates. This is a UI affordance — server enforcement was never the leak. diff --git a/.changeset/4730-objectview-config-keys-retired.md b/.changeset/4730-objectview-config-keys-retired.md deleted file mode 100644 index 16c14f7c04..0000000000 --- a/.changeset/4730-objectview-config-keys-retired.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The `console.objectView.*` config-panel vocabulary is retired — 116 keys removed from each -of the ten packs, 1160 translated strings that nothing read (objectui#4730, maintainer -ruling 2026-08-19). - -The namespace held 209 keys per pack. 116 of them labelled a view-configuration settings -panel that does not exist: appearance and density toggles, accessibility attributes, -conditional-formatting rules, row-action and inline-edit switches, quick-filter builders, -an advanced-settings tier. `packages/app-shell/src/views/ViewConfigPanel.tsx` — the panel -they were written for — was migrated off the legacy `buildViewConfigSchema` engine onto -`ViewVariantInspector`, a spec-driven inspector whose field labels come from -`@objectstack/spec` metadata rather than from this namespace. The panel was replaced; the -keys were not cleaned up with it. - -Removed under objectui#4658's three-legged evidence standard, re-measured on this branch's -merge base rather than inherited from the card: zero `t()`/`tt()` call sites, zero textual -occurrence of the dotted key anywhere in the repo outside the packs that define it, and a -consumer spot-check confirming no i18n wiring. The 93 live keys stay — the create-view -dialog fields, the view-type catalogue, `new`/`save`/`cancel`, the object-not-found copy, -plus the 38 keys whose spelling still appears somewhere the AST pass cannot see, which are -out of scope here. - -Four of the retired keys name `ListViewSchema` properties that are still active — -`rowActions`, `inlineEdit`, `hiddenFields`, `filterableFields`. They are retired anyway, by -the ruling's own words: a live schema property is not a consumer of a locale string; only a -labelled UI control is. If such a panel is ever specified, its keys are re-authored -alongside it. - -`packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts` pins the removal -by name. Every i18n gate in this repo runs call site → key, so none of them can see a dead -key come back: the parity gate is fully satisfied by 116 dead keys present in all ten packs, -and the reverse sweep that found them (`scripts/check-i18n-dead-keys.mjs`) is report-only by -design. The pin is the only thing that would notice. diff --git a/.changeset/4850-misplaced-eslint-disable.md b/.changeset/4850-misplaced-eslint-disable.md deleted file mode 100644 index 06a3767dab..0000000000 --- a/.changeset/4850-misplaced-eslint-disable.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Comment-only fix: two `eslint-disable-next-line` directives were placed on the -wrong line and had never suppressed anything since they landed. - -- `apps/console/src/pages/developer/PublicFormsPage.tsx` — the - `react-hooks/exhaustive-deps` directive was an inline block comment inside the - `useEffect(() => { load(); }, [])` statement, so its "next line" was a blank - line, not the statement itself. Moved above the statement with the reason - written out (mount-once by design; refresh is explicit via the Refresh - button and post-mutation `await load()` calls). -- `packages/react/src/SchemaRenderer.tsx` — the - `@typescript-eslint/no-explicit-any` directive's `--` reason wrapped onto a - second comment line, so its "next line" was that continuation comment, not - `type ForwardedProps = Record;`. Collapsed to one line so the - directive is immediately above its target. - -No logic changes. Verified with `eslint --report-unused-disable-directives`: -both directives are now effective (the two warnings they were meant to -suppress are gone) and neither directive is reported unused. diff --git a/.changeset/4894-shared-registration-key-reader.md b/.changeset/4894-shared-registration-key-reader.md deleted file mode 100644 index 1f8cca1445..0000000000 --- a/.changeset/4894-shared-registration-key-reader.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-and-tooling change only; no published behaviour changes. The four pins that read -"which component keys does `packages/layout/src/index.ts` register?" now share one reader -(`scripts/component-registrations.mjs`) instead of four copies of a regex that accepted a -single-quoted key and nothing else — a double-quoted registration was legal, lint-clean -and invisible to all four at once (objectui#4894). diff --git a/.changeset/4905-inline-default-value-pin.md b/.changeset/4905-inline-default-value-pin.md deleted file mode 100644 index 266fe1250d..0000000000 --- a/.changeset/4905-inline-default-value-pin.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch ---- - -Inline `t(key, { defaultValue })` strings are now held to the one placeholder spelling a -provider-less host can resolve, and five of them are pinned to the pack value for the first -time (objectui#4905). - -`check:i18n-keys`' `default-value-drift` class pins an inline default byte-identical to its -`en` row, and objectui#3512 holds `en` to the one spelling `createSafeTranslation`'s -`fallbackT` interpolates — so most inline defaults were covered transitively. Re-measured -on this tree, 66 of 1003 were not: three literal defaults on dynamic keys, and 63 written -as a computed expression. A new `unresolvable-default-spelling` class in -`scripts/check-i18n-call-site-keys.mjs` now judges the text every inline default carries — -the folded sentence, or a template literal's static segments — so `{{ name }}`, -`{{count, number}}`, `{{- name}}` and `$t(key)` are refused wherever they are written, -rather than only inside a copy table. - -Four call sites gain a real pin because their default carries no placeholder at all: the -record-form submit button now falls back to `Update`/`Create` (the pack's wording) instead -of `Save`/`Create` via a nested `t('common.save')`, the context selector's package label -and the approvals separator now state their literal. One more (`detail.showEmptyFields`) -is behind a `createSafeTranslation` hook, whose fallback does interpolate, so it can safely -say what the pack says. - -The other 61 are deliberately left computed, and the measurement behind that is the useful -part: react-i18next's not-ready `t` returns `options.defaultValue` **verbatim, without -interpolating it**. At a call site bound to a bare `useObjectTranslation()`, a default -written as `` `Signed in as ${user.email}` `` is therefore the only form that renders -correctly with no provider — rewriting it to `'Signed in as {{email}}'` would put literal -braces in front of the user, which is the exact defect this family of cards exists to -prevent. Those sites keep their template literals and are covered by the spelling class -instead. diff --git a/.changeset/4918-stale-strip-mode-prose.md b/.changeset/4918-stale-strip-mode-prose.md deleted file mode 100644 index 5f16c6e8bb..0000000000 --- a/.changeset/4918-stale-strip-mode-prose.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in `@object-ui/plugin-detail`'s spec-parity tests. The -`relationshipValueField` block in `recordRelatedListInputs.spec-parity.test.ts` and the -`hideFields` block in `recordDetailsInputs.spec-parity.test.ts` each narrated strip mode -as a present-tense fact about the installed `@objectstack/spec` — that an undeclared -top-level key parses green and is silently dropped from `data`. The props schemas were -closed upstream, so an undeclared key now draws a named `unrecognized_keys` refusal, and -both comments read as current while being false against the installed pin. Both now state -the pin-independent verdict (an undeclared top-level key is never authoring surface) and -note that the contract expresses it two ways depending on the installed pin, pointing at -`specRefusesUnknownTopLevelKeys` in the sibling `recordHighlightsInputs.spec-parity.test.ts` -as the behavioural probe. This also settles `recordDetailsInputs` disagreeing with itself, -its section-key block having already taken the pin-aware disposition. No assertion changed -and no published behaviour changes. diff --git a/.changeset/4919-retire-mobile-overrides.md b/.changeset/4919-retire-mobile-overrides.md deleted file mode 100644 index 2e0d9a4457..0000000000 --- a/.changeset/4919-retire-mobile-overrides.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/mobile': minor ---- - -Retire the `MobileOverrides` type and its `mobileOverrides` mount point (objectui#4919, -maintainer ruling 2026-08-19, ADR-0049 enforce-or-remove). - -`MobileOverrides` published a six-key mobile override surface — `layout`, `columns`, -`useBottomSheet`, `fullScreen`, `touchTarget` and a three-value `navigation` vocabulary -(`'bottom-tabs' | 'hamburger' | 'drawer'`) — from `@object-ui/types` and, re-exported, -from `@object-ui/mobile`. Nothing read any of it. Measured on current `main`: the type had -exactly four mentions repo-wide — its own declaration, the single -`MobileComponentConfig.mobileOverrides` mount point, and the two barrel re-exports — and -the lower-case property name (the spelling a renderer would actually read) appeared only -in that declaration. No renderer, hook or adapter resolved it, and a sweep of the example -apps and the `objectstack` sibling checkout found zero authors. The three `navigation` -values were three spellings of the same no-op. - -The declared surface is removed rather than narrowed. The #3985 lineage's rule is "narrow -to the implemented values"; here the implemented set is empty, so that rule terminates in -deletion — a config that type-checks, builds and silently does nothing is the -declare-without-enforce shape the platform doctrine forbids. - -Removal rather than a `?: never` tombstone follows this package's own discriminator. A -tombstone exists to steer authors to a named live replacement — `crud.ts` `confirm` → -`confirmText` (objectui#4314), `data-display.ts` `hoverable` / `striped` → `data-table` -(objectui#5474) — or to keep a key loud that the docs had actively taught as working. -Neither applies: there is no replacement key to steer to, no documentation ever described -the surface, and there is no successor spelling. That is the same zero-pull, no-successor -shape as the retired `AccordionItem.icon` (objectui#4652) and `ToggleGroupItem.icon` -(objectui#4632), both of which were removed outright rather than tombstoned. - -**Breaking for TypeScript authors of `MobileOverrides` / `mobileOverrides` only** (marked -`minor` per this repo's version-alignment rule, which reserves `major` for following -`@objectstack` across a major — see AGENTS.md's 版本号策略, and the identical -classification used for `AccordionItem.icon`). Runtime behaviour is unchanged: an authored -`mobileOverrides` did nothing before and does nothing now. What changes is that the -contract no longer claims otherwise, so the mistake surfaces at authoring time — importing -the type is now a "has no exported member" error, and authoring the key on a -`MobileComponentConfig` object literal is an excess-property error, instead of a silent -no-op that type-checks and builds. - -If real mobile-override renderer work is ever wanted it re-enters deliberately, as designed -product surface on its own card, with the renderer landing in the same change as the -declaration — not by resurrecting this declaration. diff --git a/.changeset/4929-plugin-published-stylesheets.md b/.changeset/4929-plugin-published-stylesheets.md deleted file mode 100644 index b2cb6e0b03..0000000000 --- a/.changeset/4929-plugin-published-stylesheets.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch ---- - -`@object-ui/plugin-grid` and `@object-ui/plugin-kanban` now publish a stylesheet — -`"./style.css"`, mapped to `dist/index.css` and compiled at build time from the package's -own sources (objectui#4929, maintainer ruling 2026-08-17, Direction 1). - -**What was broken.** Only `@object-ui/components` and `@object-ui/fields` shipped CSS, and -each scans its own `src` only, so a class used exclusively by a plugin could not appear in -either sheet BY CONSTRUCTION. A published-state Vite app that installed one of these two -plugins and followed the quick-start rendered the grid or the board with **25 themed -utilities that had no source anywhere in the world** — `bg-muted/10`, `bg-card/60`, -`text-muted-foreground/60`, `ring-primary/40` and friends, ordinary appearance classes — -plus ~103 plain ones. Re-measured on the merged tree: the 21 the card listed all still hold, -and four more (`[&>h3]:text-foreground/80`, `border-l-primary/40`, `border-primary/30`, -`hover:text-primary`) that its literal-grep method could not see. - -The plain utilities a consumer could in principle regenerate by pointing `@source` at the -package's `dist`. The themed ones they cannot, at all: they resolve `@theme` tokens declared -in `packages/components/src/index.css`, which that package does not publish. A build inside -this monorepo is their only possible producer — which is why the fix is a stylesheet we -ship, not documentation teaching consumers to hand-declare the theme and scan -`node_modules` (the advice objectui#4858 had just retired from the guides). - -**The shape**, inherited from `@object-ui/fields` (objectui#4059): each package gains -`src/index.css` that `@reference`s the components entry — theme tokens, the class-based -`dark` variant and the animate plugin become available for resolution while emitting -nothing — plus `scripts/build-css.mjs`, which subtracts every rule components' published -sheet already ships. So these are **supplements, imported after** the components sheet, and -they are 16.30 kB and 11.41 kB rather than another ~170 kB each: - -```css -@import 'tailwindcss'; -@import '@object-ui/components/style.css'; -@import '@object-ui/fields/style.css'; -@import '@object-ui/plugin-grid/style.css'; -@import '@object-ui/plugin-kanban/style.css'; -``` - -Add a line only for the plugins you install; no other `@object-ui/plugin-*` package -publishes a stylesheet yet. The build step is shared -(`scripts/build-plugin-stylesheet.mjs`) so it is the pattern the next one inherits rather -than a file to copy, and it refuses to write a sheet that fails any of four assertions — no -rule may vanish, the subtraction must have removed something, the class count may not pass -a leak ceiling, and named themed utilities only this build can produce must still be -present. - -Nothing is removed and no existing import changes: a consumer who does not import the new -sheets is exactly where they were, and the guides' "do not scan `node_modules`" advice -stays correct — it is now correct for plugins too. diff --git a/.changeset/4935-element-filter-comment-residue.md b/.changeset/4935-element-filter-comment-residue.md deleted file mode 100644 index caebf8b3b5..0000000000 --- a/.changeset/4935-element-filter-comment-residue.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Comment-only cleanup in `@object-ui/components`: the `elements.tsx` header note no -longer lists `element:filter` among the heavier interactive elements said to live in -their owning plugins. No such file ever existed for it, and the element was retired at -element grain upstream (ADR-0049), so the type is gone from `PageComponentType` -entirely. No published behaviour changes. diff --git a/.changeset/4938-timeline-dead-example-link.md b/.changeset/4938-timeline-dead-example-link.md deleted file mode 100644 index f000f50e64..0000000000 --- a/.changeset/4938-timeline-dead-example-link.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Doc-only fix in `@object-ui/components`: `packages/components/src/renderers/complex/TIMELINE.md` -linked `../../examples/prototype/src/App.tsx` for "comprehensive examples of all -three timeline variants in action" — that example app was deleted whole in -`3aa84cee0` with no successor at the same path, and no gate had ever opened this -file, so the link sat dead with nothing to notice (objectui#4938). Repointed at -the interactive demo the docs site now serves for the same three variants -(`https://www.objectui.org/docs/plugins/plugin-timeline`). - -No source or behaviour change; text only. diff --git a/.changeset/4947-tombstone-judge-convergence.md b/.changeset/4947-tombstone-judge-convergence.md deleted file mode 100644 index 7f6937b240..0000000000 --- a/.changeset/4947-tombstone-judge-convergence.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-infrastructure convergence, no shipped code touched: the last two local copies of -the ADR-0087 D2 tombstone criterion — in `packages/plugin-detail`'s -`recordDetailsInputs` spec-parity test and `packages/app-shell`'s `block-config` -preview test — now import `@object-ui/test-support`'s shared judge instead of -spelling out `unwrap()._def.type === 'never'` by hand. Both copies carried the -structural channel alone; the shared judge OR-s it with the `[REMOVED]` description -channel, so neither channel can go quietly permissive on its own, and every -tombstone-aware gate in the repo now moves together when the criterion changes. diff --git a/.changeset/4971-declared-arm-subset-gate.md b/.changeset/4971-declared-arm-subset-gate.md deleted file mode 100644 index 1d3b39bc39..0000000000 --- a/.changeset/4971-declared-arm-subset-gate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Test-only: `registry-inputs-spec-parity.test.ts` gains a THIRD direction — every coarse -arm a block declares on a key must be one `@objectstack/spec` accepts there (objectui#4971). - -objectui#3832 gave `ComponentInput.type` the array form so a union key can declare its real -arms, and in doing so created a second way to disagree with the contract. The two are not -symmetric: declaring FEWER arms than the spec accepts produces NOISE (the manifest gate warns -on a legal write — audible), while declaring an arm the spec REJECTS is SILENT — `checkType` -clears a value the contract refuses, the manifest and the generated `.d.ts` publish it as -legal, and `declared = enforced` inverts with nothing to announce it. Measured on #3832's own -branch: a fake `'object'` arm on `element:text_input.defaultValue` reddened that block's -per-block test, while a fake `'number'` arm on `page:card.title` left all 856 tests green — -the property held by per-block discipline, not by a gate. It is one-directional for that -reason. - -An arm names a value's KIND, never its domain (`ComponentInput.type`, maintainer ruling -2026-08-17), so the gate refutes an arm only when the contract refuses the KIND — read off the -parse ISSUES rather than a boolean, scoped to the key so a required sibling cannot speak for -it, and recursing through a union's branches. A refusal of the VALUE leaves the arm standing, -which is what keeps a correct `'string'` arm on a spec-enum key from reading as invented; the -same rule still refutes a `'number'` arm there, and both halves are pinned by name. An `enum` -arm is judged exactly instead — every declared member must be a value the spec accepts. - -Two red-on-arrival findings, reported rather than declared away: `element:number.filter` -(objectui#6206) and `object-grid.data` (objectui#6207), both carried as reasoned, issue-backed -exemptions that a stale-exemption test deletes the moment either side moves. diff --git a/.changeset/4991-lucide-pin-range-consistency.md b/.changeset/4991-lucide-pin-range-consistency.md deleted file mode 100644 index 9982eb31c2..0000000000 --- a/.changeset/4991-lucide-pin-range-consistency.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change to `packages/cli/src/__tests__/app-generator.test.ts`: the -`lucide-react` manifest pin, and the pre-fix reverse-verification's drift -check, no longer read an arbitrary member off `inRepoRangesOf(...)` without -first asserting the repo's in-repo declarations for that dependency actually -agree. No published behaviour changes. diff --git a/.changeset/5034-navigateonsuccess-url-contract.md b/.changeset/5034-navigateonsuccess-url-contract.md deleted file mode 100644 index 567b1f9cf8..0000000000 --- a/.changeset/5034-navigateonsuccess-url-contract.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@object-ui/plugin-form": minor -"@object-ui/types": minor ---- - -`navigateOnSuccess` is relative-only, escapes the interpolated id, and is deprecated in favour of `submitBehavior` - -The url contract for this key was undeclared: it was same-origin-guarded (so a same-origin -ABSOLUTE value was accepted), it interpolated `{id}` / `{recordId}` without escaping the -substituted value, and nothing said which of those was intended. The maintainer ruled it on -2026-08-17: `navigateOnSuccess` is the pre-ruling ancestor of the `submitBehavior` family -rather than a second dialect, so as a compat alias it runs under the semantics -objectstack#7496 ruled for that family. - -**Relative paths only.** A same-origin absolute such as `https://own-host/record/{id}` is -now refused like any other out-of-contract value, rather than accepted and navigated at -browser level. The destination is authored metadata, which is exactly where an address -somebody else chose gets copied in. Cross-origin and protocol-relative values were already -refused and still are; every relative shape that worked before still works. - -**The interpolated id is URL-escaped.** `/r/{id}` with an id of `a/b c` resolved to -`/r/a/b c`, silently growing a path segment, and a template of `{id}` let the id become the -whole destination. The substituted value now goes through `encodeURIComponent`, so a token -is a value in the path and never a way to add path structure. The template is the author's -and is untouched — only the id, which is data read off the written record, is escaped. - -Both halves are needed and neither implies the other: relative-only is a rule about where a -destination starts, so it cannot see structure injected further along; escaping runs only on -the substituted value, so it cannot see an absolute the author wrote out. - -This can only narrow what is reachable. Every destination the key now accepts is a relative -reference, and a relative reference cannot carry an authority, so it was already accepted by -the same-origin guard this replaces — no value that was refused is now followed. With every -accepted destination relative, the browser-level `window.location.assign` fallback at both -call sites became unreachable and was removed; an accepted destination goes to the injected -navigation seam, and the absent-seam fallback inside the shared hook is unchanged. - -**Deprecation.** `navigateOnSuccess` is marked `@deprecated` in favour of `submitBehavior`, -which already takes precedence over it and carries the richer `{{record.field_name}}` -interpolation. The `{id}` / `{recordId}` dialect keeps working for forms that already -declare it — the ruling converges the documentation and the semantics, not the spelling. diff --git a/.changeset/5106-doc-key-table-scan-surface.md b/.changeset/5106-doc-key-table-scan-surface.md deleted file mode 100644 index 6dc86a21d0..0000000000 --- a/.changeset/5106-doc-key-table-scan-surface.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Tooling only, no published surface. `scripts/check-doc-component-types.mjs` now -reads the plugin KEY TABLES in `content/docs/**` — the canonical -`| Namespaced key | Bare-name fallback | Renderer behind it |` form the -objectui#5002 family standardised on — and judges both halves of every row, -including the namespaced one the gate never judged at all. Also fixes a floor -that named a counter which never existed (`docFiles`), so the check that catches -the docs walk finding nothing had been inert, and adds a guard that makes the -same mis-key fail loudly instead of silently passing. diff --git a/.changeset/5221-onsuccess-navigation.md b/.changeset/5221-onsuccess-navigation.md deleted file mode 100644 index 709dc4aa88..0000000000 --- a/.changeset/5221-onsuccess-navigation.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/app-shell': minor ---- - -Console half of `ActionSchema.onSuccess` post-success navigation. - -`@objectstack/spec` declares `onSuccess` as a closed strict object -`{ navigate: string, openIn: 'self' | 'newTab' }`, refine-scoped to `type: 'api'` and -`type: 'script'` — the two action types whose success event carries a server response. -Nothing in this renderer read it, so an action declaring the hop navigated nowhere: the -block fell into `ActionRunner`'s older `ActionDef.onSuccess` chained-callback channel, -was dispatched as an action, and failed inside `executeNavigation` with "No URL provided -for navigation action" — a red toast and no jump. The motivating report is a clone action -that leaves the user sitting on the record they cloned from. - -`ActionRunner.handlePostExecution` now performs the declared hop through -`navigationHandler` — the same SPA seam every other navigator in that file uses, which -the console wires to react-router's `navigate`, so `openIn: 'self'` is a real in-place -route hop rather than a full-page load. `interpolateTarget` gains a `${result.*}` scope -alongside `${param.*}` and `${ctx.*}`, resolved against the handler's own return value -(the level `readActionPayload` reads, one below the action envelope) and supplied only by -this call site, so a target interpolated before its request still has no `result` to -name. `openIn` is read as the one member that changes the branch and no default is -written here — the spec materialises `.default('self')`, so parse output always carries a -resolved member — and the two `openIn` spellings stay apart: this reads -`onSuccess.openIn` (`'self' | 'newTab'`), never the top-level `type: 'url'` switch -(`'self' | 'new-tab'`), each of which spec refuses in the other's position. - -The console's server-action wrapper gains the matching handler-return half: a handler may -now return `openIn: 'self'` next to its `redirectUrl` to ask for the same-tab jump, while -a `redirectUrl` **without** `openIn` keeps its shipped new-tab behaviour unchanged. When -an action declares an `onSuccess` block, the wrapper defers to the runner and only tidies -its pre-opened tab, so one navigation happens rather than two. - -The pre-existing `ActionDef.onSuccess` chained-callback channel is unchanged. It is told -apart by the spec's own declaration — a non-array object whose `navigate` is a string — -and keeps running for every other shape. diff --git a/.changeset/5232-view-config-org-wide.md b/.changeset/5232-view-config-org-wide.md deleted file mode 100644 index fe7e5b2452..0000000000 --- a/.changeset/5232-view-config-org-wide.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/app-shell': minor -'@object-ui/plugin-list': patch -'@object-ui/i18n': patch ---- - -**View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's -ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a -list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user -dragging a column or cycling density was re-styling that view for the entire organization. Nothing -in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611, -v18) and is deliberately not built here — which is precisely why the write has to be gated rather -than narrowed: there is no second, private store for it to fall back to. - -`ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability -set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError` -(`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the -`VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method — -before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire. -It is on the write rather than on the toolbar button on purpose: withholding the affordance would -leave the method still accepting the call from anything else holding the adapter, whereas a gate on -the write is inherited by every caller, present and future. - -`manage_metadata` is not a newly minted name. It is the capability this repo already treats as -metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself -refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very -same ADR-0005 metadata door, so this applies the authority the server is already applying instead of -inventing a parallel one. - -**Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating -ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a -client-side refusal on missing data cannot protect anything and can only break a permitted user. A -*reported* empty grant gates strictly. Hosts push the session's capabilities in with the new -`setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`. - -The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure -into `console.error`, which for a debounced toggle whose UI has already moved would have left the -operator looking at a density they did not get; a denied write now raises a toast. And the "View -settings" popover — where density and field visibility are actually changed — now states the scope -before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who -uses this view."*, translated in all ten packs. diff --git a/.changeset/5234-onnavigate-documented-exception.md b/.changeset/5234-onnavigate-documented-exception.md deleted file mode 100644 index a0aabb6e02..0000000000 --- a/.changeset/5234-onnavigate-documented-exception.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Comment- and docs-only: `ObjectGridSchema.onNavigate` is now stated as the explicit, -documented exception the maintainer ruled it on 2026-08-19 (objectui#5234, option C). -Nothing is added to or removed from any accept set, and no published behaviour changes. - -`@object-ui/plugin-grid`'s `ObjectGrid` reads `schema.onNavigate` at the -`useNavigationOverlay` call, while `GRID_QUERY_INPUTS` does not publish the key — so the -manifest, the designer panel and the generated `sdui-intrinsics.d.ts` deny a key the -renderer honours. That gap is now deliberate and said out loud rather than left to be -re-discovered by the next census: - -- An exemption comment at the read site, in the shape objectui#5091 / PR #5241 - established for `columnState`, `hideRowHeightToggle`, `maxInlineRowActions` and - `rowActionDefs` — with the one difference those four do not share: this key IS - declared in `@object-ui/types`, so the read is plain rather than a cast. -- A programmatic-only note on the declaration in `@object-ui/types` - (`ObjectGridSchema.onNavigate`): it is a function value and a schema is a serialisable - document, so `(recordId, action) => void` cannot survive a metadata round-trip whatever - declares it, and programmatic callers should prefer `ObjectGridComponentProps` where its - nine sibling callbacks live. -- `packages/plugin-grid/README.md` carried the un-narrowed universal claim that the grid - "never reads a callback off the schema", which `onNavigate` falsifies. Narrowed to match - `content/docs/plugins/plugin-grid.mdx`, which had already been narrowed to "any of these - nine"; both pages now also name the one callback that is read. -- `gridNonAuthorKeys.test.tsx` extended, not rewritten: the four objectui#5091 keys keep - every assertion they had, and ten cases are added for this key — the ledger premises - plus two source-reading cases that pin the exemption prose itself, which is the only - part of a zero-behaviour-change ruling an ordinary assertion cannot see. - -The key is deliberately NOT removed (option A: a breaking public type change plus a -deprecation cycle for zero measured harm) and deliberately NOT added to -`GRID_QUERY_INPUTS` (option B: publishing to the designer a key no author can express). diff --git a/.changeset/5293-view-sort-order-spelling.md b/.changeset/5293-view-sort-order-spelling.md deleted file mode 100644 index 47728f24b6..0000000000 --- a/.changeset/5293-view-sort-order-spelling.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -'@object-ui/plugin-view': minor ---- - -**Breaking (shipped as `minor` per AGENTS.md §版本号策略).** `ObjectViewProps.views[].sort` -now spells its direction key **`order`**. The retired spelling is **`direction`** — named -here so that a host still writing it can find this entry by searching the old key -(objectui#5293). - -```diff - -``` - -**Nothing that worked stops working on this surface, because on the `views` prop -`direction` never worked.** All three consumers of the resolved `activeView.sort` read -`order`: the non-grid fetch lowers it through the shared sink `convertSortToQueryParams`, -whose `entry.order === 'desc'` is false for a missing key; the grid path forwards it to -`ObjectGridSchema.sort`, where `ObjectGrid` builds the wire string `` `${s.field} ${s.order}` `` -— literally `"created_at undefined"` — and `parseSchemaSort` reads a missing `order` as -ascending, so the column header even drew an ascending arrow; `mergedSort` hands the same -value to the delegated list view. - -So a host writing the exact shape the prop declared got an **ascending** list with no -failure signal anywhere: the declaration said the value was well-formed, and the direction -was dropped at three independent readers rather than rejected at one. This rename does not -take away a feature — it converts a silent wrong answer into a loud type error at the one -place that can still be fixed cheaply. - -**Scope — one published export still accepts `direction`, and this release does not retire -it.** `toSortItems` (`packages/plugin-view/src/config/view-config-utils.ts`, re-exported -from the package root and listed in the README) folds `s.order || s.direction || 'asc'`. -It serves a different surface — the studio inspector-draft that feeds `SortBuilder` — and -it is not reachable from the `views` prop, so it neither affects nor is affected by this -rename. If you migrate by searching for the old key, that is the other hit you will find: -it is dormant (nothing in this repo calls it outside a test), and removing it would be a -separate break on a separate public export, tracked as objectui#6011. It is not a partial -retirement of this one. - -`order` is the spelling every other sort surface already uses (`SortConfig`, -`NamedListView.sort`, `ObjectGridSchema.sort` / `.defaultSort`, and the shared -`QuerySortEntry` sink), so the prop now has one spelling repo-wide and declared equals -enforced. - -⛔ Deliberately **not** a tolerant dual-read (`direction ?? order`): that is the tolerance -layer objectui#4869 ruled against, and admitting the old key as an alias would rebuild the -drift this change removes. `SortUI` is untouched — it legitimately owns `direction` on its -own `SortUISchema` and converts at its boundaries. diff --git a/.changeset/5325-ineffective-dynamic-import-ledger.md b/.changeset/5325-ineffective-dynamic-import-ledger.md deleted file mode 100644 index c1652b19e7..0000000000 --- a/.changeset/5325-ineffective-dynamic-import-ledger.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Build tooling only, no published package source changed. - -The console build emitted 43 `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every run -— every `import()` in `@object-ui/fields`' widget-loader map is defeated by a -static edge to the same module. They are true, and they scroll past every build -until nobody reads the log at all. - -They are now pinned to a ledger (`scripts/vite-ineffective-dynamic-imports.ts`) -rather than filtered away: the pinned 43 are replaced by one summary line, an -ineffective dynamic import the ledger does NOT know about keeps rolldown's -original warning and fails the build, and a pinned entry that stops firing fails -it too — so a build that dies before chunk assignment reports 43 named absences -instead of a clean-looking zero. - -The module graph is deliberately unchanged; the ledger's header carries the -measurement of why, and the eager-closure budget already governs the bytes. diff --git a/.changeset/5397-release-pr-post-version-validation.md b/.changeset/5397-release-pr-post-version-validation.md deleted file mode 100644 index 06dfedf041..0000000000 --- a/.changeset/5397-release-pr-post-version-validation.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -CI-only: the changeset release workflow's refresh lane now renders the -post-version tree, validates the surfaces `changeset version` can move -(`QUICK_REFERENCE.md`, the generated `CHANGELOG.md` files, the manifest -versions as the tooling suite reads them) and restores the tree before -`changesets/action` opens or updates the version PR. No package changes. diff --git a/.changeset/5422-coverage-lane-concurrency.md b/.changeset/5422-coverage-lane-concurrency.md deleted file mode 100644 index 9a77ba1754..0000000000 --- a/.changeset/5422-coverage-lane-concurrency.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -CI-only change: `ci.yml`'s workflow `concurrency` group now carries `github.sha` on -the `push` trigger, so a merge to `main` no longer cancels the previous merge's -still-running CI. The `pull_request` group (the PR number, `cancel-in-progress: true`) -and the `merge_group` fallback to `github.ref` are both unchanged. - -The push lane is the only lane that runs the coverage gate, and the merged 4-shard -report is what enforces `coverage.thresholds` for a commit. Measured over the 64 -completed push-lane runs on `main` between 2026-08-23T06:34Z and 2026-08-24T13:56Z, -39 of them — 61% — lost that gate to cancellation rather than to a red suite. - -No published package changes. diff --git a/.changeset/5458-unprefixed-query-params.md b/.changeset/5458-unprefixed-query-params.md deleted file mode 100644 index 040a74bb72..0000000000 --- a/.changeset/5458-unprefixed-query-params.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/console': patch ---- - -Fix four `find()` calls that passed a query option without its `$`, and gate the shape. - -`QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies -exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw, -no warning, and it type-checks because the type carries `[key: string]: any` for -adapter-specific params. For a dropped cap the result is an **unbounded** read rather than -a truncated one: the platform's GET list route has no default page size, so the query -returns the whole match set and stays invisible until the object is large. - -- `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one - **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so - the dropped key turned "count only, fetch nothing" into "fetch every row in the object", - on every mount and every refresh of every list view. It now sends `$top: 0` and reads - the count off `total` only; the row-counting fallbacks are gone rather than repointed, - because once zero rows are requested an empty `data` means "you asked for none", not - "the object is empty", and counting it would assert a confident `0`. With no total the - footer line is omitted instead. -- `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`, - one line from three correct `$top` calls. -- `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so - a filter's option list read every row and every column of its source object while its - own comment described it as capped at 200. The same call read `records.items`, which is - not a `QueryResult` member, so against a real adapter the fallback produced no options - at all. -- `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the - result in its page-source metadata. - -A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time: -a known query-option name missing its `$` in the second argument of a `find`/`findOne` -call. It is narrow on purpose — a closed list of spellings, anchored to the call — because -the index signature exists so adapters can take adapter-specific params, and a rule that -flagged any unprefixed key would report the shape the type was written to allow. Its -sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged. diff --git a/.changeset/5483-5439-dts-config-wiring.md b/.changeset/5483-5439-dts-config-wiring.md deleted file mode 100644 index 02dff4f5b5..0000000000 --- a/.changeset/5483-5439-dts-config-wiring.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/fields': patch -'@object-ui/plugin-ai': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-charts': patch -'@object-ui/plugin-chatbot': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-designer': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-editor': patch -'@object-ui/plugin-form': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-map': patch -'@object-ui/plugin-markdown': patch -'@object-ui/plugin-report': patch -'@object-ui/plugin-timeline': patch -'@object-ui/plugin-tree': patch -'@object-ui/plugin-view': patch ---- - -Published typings from every `vite-plugin-dts` package now carry an explicit extension on -every relative specifier, and a type error in the declaration build now fails the build -instead of being printed and ignored (objectui#5439, objectui#5483). - -**Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that -is the fix working.** These packages re-export mostly through NAMED re-exports — -`export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the -extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent -`any`. Nothing errored; consumers simply got no types. With the extension emitted, the -symbol carries its real type, and any call site that was relying on the `any` now type -checks for the first time. This is the mode that produced the 21 residual `TS7006` on -`@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly, -unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced -immediately as `TS2305: has no exported member`. - -410 extensionless relative specifiers across 19 packages were emitted before this change; -the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`. -`@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and -is wired so it stays that way. - -The second half changes no emitted output today: 22/22 packages built green unmodified, so -making the declaration step's exit code honest turns nothing red. It changes what a FUTURE -regression does — print and exit 0, versus fail the build. diff --git a/.changeset/5490-per-chunk-eager-budgets.md b/.changeset/5490-per-chunk-eager-budgets.md deleted file mode 100644 index f058a3687b..0000000000 --- a/.changeset/5490-per-chunk-eager-budgets.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Build tooling and CI only — `apps/console/vite.config.ts` (not published source: -`@object-ui/console`'s `files` list carries `dist`, `plugin.*` and `README.md`), -`scripts/check-eager-closure-budget.mjs` and its unit test. Nothing ships from -this change. - -The console eager-closure budget weighed one total across 52 chunks. Inside its -headroom a single chunk can absorb the whole allowance while the others shrink, -and the total never moves — the shape of objectui#5266, whose 89 KiB landed -entirely in `vendor-objectstack`. Per-chunk gzipped ceilings now sit on top of -the aggregate for the three largest eager chunks, set at the measured current -state plus ~2%, with each headroom narrower than the regression the gate exists -to catch. - -The ceilings key on the chunk names the report itself carries (`files[].name`, -new in report v2, taken from rolldown's own `chunk.name`) rather than on names -this checker expects to exist. A budgeted chunk that is absent — renamed group, -chunk gone — is therefore an error, not a skip: a ceiling with no subject weighs -nothing and would be green forever. diff --git a/.changeset/5522-runtime-telemetry-consumer.md b/.changeset/5522-runtime-telemetry-consumer.md deleted file mode 100644 index 7e626d0bab..0000000000 --- a/.changeset/5522-runtime-telemetry-consumer.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': minor ---- - -Console telemetry can now be hard-disabled on an already-built artifact - -`/api/v1/runtime/config` gained `telemetry.allowClientErrorReporting` -(objectstack#11382), and the Console now reads it. The Sentry decision becomes a -conjunction of two independent grants — a DSN injected at **build** time AND a -positive permission from the **runtime** — so the single pre-built SPA that both -the hosted SaaS console and the on-premises / air-gapped EE images embed can be -silenced by the deployment it lands in, with no rebuild and without editing files -inside a published bundle. That was the half objectui#5522 could not close before: -every other input to the gate is a Vite build-time variable frozen into the bundle -as a literal, which is how an air-gapped EE Console came to send 14 Sentry -envelopes per session to `sentry.io` carrying IP + User-Agent PII with no way for -the customer to turn it off (objectstack-ai/cloud#1508). - -The permission fails **closed** in every direction: absent key, `telemetry` block -absent, malformed payload, failed fetch, or a runtime predating the key all read as -*do not send* — which is precisely the set of runtimes leaking today. It is a -permission and never a source: the server supplies no DSN and cannot turn telemetry -on for a build that carries none. Only a real boolean `true` grants; `'true'`, `1` -and other truthy lookalikes do not. - -Behaviour change for deployments that already inject a DSN: reporting now also -requires the runtime to grant permission, via -`OS_TELEMETRY_CLIENT_ERROR_REPORTING_ENABLED` (or `RuntimeConfigPlugin`'s -`allowClientErrorReporting`). A build that opted in but whose runtime says nothing -will go quiet — deliberately, since that is the same artifact an air-gapped -customer runs. - -`@object-ui/app-shell` additionally exports `isClientErrorReportingAllowed()` and -the `RuntimeTelemetry` type, so consumers read the permission through the one -fail-closed accessor instead of writing their own optional-chain against the -payload. diff --git a/.changeset/5631-ui-icon-icon-key-migration.md b/.changeset/5631-ui-icon-icon-key-migration.md deleted file mode 100644 index fa31693dee..0000000000 --- a/.changeset/5631-ui-icon-icon-key-migration.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -**BREAKING (authoring): `ui:icon` names its glyph with `icon`, not `name`** - -`{ "type": "icon", "name": "check" }` no longer renders an icon. Write -`{ "type": "icon", "icon": "check" }`. Stored metadata authored before this -release needs converting — see the migration below. - -Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major` -outside an `@objectstack` major sync); the break is real and is stated here. - -**Why** - -`name` is the SDUI identity key every authored node carries, alongside `id` — -it is not `ui:icon`'s private prop. So an ordinary node like -`{ type: 'icon', id: 'save_icon', name: 'save_icon' }` asked lucide for a glyph -called `SaveIcon`, missed, and rendered **nothing at all**: silent to a human, -and clean to a DOM gate, because a renderer that renders nothing spreads no -attributes to find. `action:*` already reads `icon`, so this is the vocabulary's -existing answer, and it leaves no node type on which the identity key is -unusable. - -**What changed** - -- `IconSchema` (types + its zod mirror) declares `icon: string` **required**, - exactly as `name` was required before it — a key rename at constant - strictness. `name` reverts to the optional identity inherited from - `BaseSchema`. The mirror previously *required* `name`, which is why the - renderer could not be migrated on its own: the published contract refused the - correct shape. -- `ui:icon` resolves its glyph from `schema.icon`. There is deliberately **no** - `icon ?? name` fallback: a key meaning "identity" or "glyph" depending on - whether a lucide lookup happened to hit is the ambiguity being removed. -- The registry's `inputs` entry and `content/docs/components/basic/icon.mdx` - moved in the same change as the resolver. -- All 98 authored icon nodes in this repo are converted. - -**The break is loud in three places, never silent** - -1. `IconSchema` **refuses** a legacy node, with a message that names the rename - and points at the converter — not zod's default `expected string, received - undefined`. -2. A legacy node that reaches the renderer unvalidated draws the visible - placeholder shipped in the previous release, and its `console.warn` now - carries the exact rename (`icon: "save_icon"`) plus the converter's name. - Its accessible name says so too, and it gains a - `data-objectui-icon-legacy-name-key` marker so a gate can tell - "unmigrated node" from "glyph that does not resolve". -3. **Migration for stored metadata** — `migrateIconNodeKeys` from - `@object-ui/types`: - - ```ts - import { migrateIconNodeKeys } from '@object-ui/types'; - - const { document, converted, warnings } = migrateIconNodeKeys(storedPage); - if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n')); - if (document !== storedPage) await save(document); - ``` - - It walks the whole document and lifts `name` to `icon` on every icon node. - It is a one-shot conversion a deployer runs over stored documents — **not** a - read-path fallback; nothing calls it during rendering or parsing. It - **reports rather than guesses** for the two cases it will not touch: a node - already declaring both keys (`icon` wins, `name` stays the identity it is), - and a node naming no glyph at all. diff --git a/.changeset/5631-ui-icon-visible-placeholder.md b/.changeset/5631-ui-icon-visible-placeholder.md deleted file mode 100644 index 0e64171ad8..0000000000 --- a/.changeset/5631-ui-icon-visible-placeholder.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`ui:icon`: an unresolvable glyph now renders a visible placeholder instead of nothing - -An icon whose name does not resolve to a lucide glyph used to `return null`. -That failed silently in two independent ways at once: invisible to a human (no -gap, no error boundary — just an absent glyph), and clean-looking to a gate (a -renderer that returns `null` spreads no attributes, so a DOM scan of it reports -no findings). - -It now renders a dashed-square placeholder on the same SVG host, keeping the -authored `className`, `size` and colour so the gap sits exactly where the icon -would have been, with `role="img"`, an accessible name identifying the icon -that failed, and a `data-objectui-icon-unresolved` marker. The `console.warn` -stays and now names the cause. - -Also fixed: a node with no `name` at all reached `toPascalCase(undefined)` and -threw, which the error boundary then swallowed — a third silent failure. It -renders the placeholder too. - -Not included: `ui:icon` still reads the SDUI identity key `name` as its glyph -name. Moving it to `schema.icon` is ruled but blocked on an authored-metadata -migration — see objectui#5631. diff --git a/.changeset/5676-layered-envelope-boundary-parse.md b/.changeset/5676-layered-envelope-boundary-parse.md deleted file mode 100644 index 88c2a25333..0000000000 --- a/.changeset/5676-layered-envelope-boundary-parse.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -`MetadataClient.layered()` validates the ADR-0010 protection envelope against the -producer's own schema at the boundary, instead of casting ten wire fields through -unchecked (objectui#5676, triage adjudication 2026-08-22). - -The envelope arrived by ten `as` assertions over a raw `res.json()` body — no parse, no -allowlist, no default. The consumer that reads it opens the metadata lock banner on -`layered?.lock && layered.lock !== 'none'`, true for **any** non-`none` value, so a server -sending a lock state this console had never heard of opened the amber box, drew the padlock -and the border, and rendered an empty title. No fifth state ever had to be added to this -repo for that to happen: a union types what this repo writes and constrains nothing about -what a server sends. - -The boundary now runs `GetMetaItemLayeredResponseSchema.safeParse`. On the conforming path -every value is the producer's schema output and the ten assertions are gone. `safeParse` -and never `parse`: a metadata console that rejected every dialect it had not been compiled -against would answer a newer server with a blank page, which is strictly worse than the -wrong render being fixed. Values the schema rejects are still **forwarded** — dropping them -would be that same refused rejection wearing different clothes — and are named in a new -optional `MetadataLayered._unrecognized`, absent whenever everything parsed. This extends -to the whole envelope the "pass through and label" treatment objectui#5672 chose for `lock` -alone; the banner's existing unrecognised-token title is unchanged and needed no edit. - -The labelling is per field, which is the part that makes it a degrade rather than a subtler -version of the same bug. Measured on the installed spec (17.2.0): -`GetMetaItemLayeredResponseSchema.safeParse(body)` is all-or-nothing — one unknown `lock` -returns `success: false` with `data` undefined — so the failure branch re-checks each key -against that schema's own `shape[key]`, where only the offending field fails and the other -six still arrive typed. Absence is never "unrecognised": the four resolved verdicts are -required upstream on this path, so a pre-ADR-0010 backend takes the failure branch with -nothing flagged and behaves exactly as before. - -One consequence of the same ruling, fixed alongside because it defeats it: a 200 answer -whose body was a bare JSON string or number **rejected** the promise with a -`TypeError: Cannot use 'in' operator`, from the envelope-detection guard's bare truthiness -check. A malformed body must degrade, never throw. diff --git a/.changeset/5700-shared-ref-namespaces.md b/.changeset/5700-shared-ref-namespaces.md deleted file mode 100644 index ead20e9239..0000000000 --- a/.changeset/5700-shared-ref-namespaces.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Instruction-file only. `AGENTS.md` §9 多 agent 协作纪律 gains one bullet beside the -existing `git stash` bullet, generalising it: a worktree isolates your checkout and -exactly four ref namespaces (`HEAD`, `refs/bisect`, `refs/worktree`, `refs/rewritten`) -and nothing else — not the object store, not the repo config, not any other ref. The -existing stash rule is reframed as one case of that, because a reader who learns only -the stash rule draws the opposite general conclusion. - -Two further instances that have cost work are stated: `refs/remotes/*` (a sibling -agent's fetch advances *your* `origin/main`, so a path-scoped `git checkout origin/main` -restores whatever that ref points at now — possibly newer than your branch base — and -stages it), and `FETCH_HEAD` (last fetch in the checkout wins; the symptom is an empty -diff that exits 0, which reads as "the change is not there" rather than as wrong -content — a confidently wrong review conclusion about someone else's work). - -The `FETCH_HEAD` isolation boundary is stated as measured rather than extrapolated from -`refs/stash`: on git 2.43 `git rev-parse --git-path FETCH_HEAD` resolves per-worktree in -a linked worktree, so a sibling's fetch in *its* worktree does not move yours; the -hazard is the shared primary checkout, where the same command resolves to the common -`.git/FETCH_HEAD` and where every agent's first fetch lands before it creates a worktree. - -No hook is added — the safe forms are ordinary commands and the unsafe form is -legitimate elsewhere, so a mechanical block would fire on correct usage. No published -package changes. diff --git a/.changeset/5729-column-sortability-signal.md b/.changeset/5729-column-sortability-signal.md deleted file mode 100644 index d5f990d8e0..0000000000 --- a/.changeset/5729-column-sortability-signal.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/data-objectstack': patch -'@object-ui/plugin-grid': patch ---- - -Grid headers offer a sort click only on columns the PLATFORM says it will order by -(objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23: -the platform serves an explicit per-column sortability signal and the grid reads it, -rather than re-deriving "virtual ⇒ unsortable" from field type). - -`GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its -ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve -time from the platform's own storage predicates, deliberately beside `item` rather than -inside it so the key stays un-authorable. The signal was reaching the browser and being -discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps -the envelope to `item`, so every UI reader saw a document with no signal on it. It now -survives that unwrap, carried on the schema under a symbol key — invisible to -`JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata -write endpoint can never take it into a body the server parses strictly. - -`@object-ui/core` gains the one spelling of the consumer contract: -`isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and -says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown -name, a dotted path and an unprovisioned audit column, all three of which the runtime -doors reject — so the `!== false` spelling every other optional flag in this repo uses -would get exactly that family backwards. A projection that is absent ALTOGETHER is a -different question with a different answer (`undefined`: no signal was served) and is -typed apart from an empty one, so a deployment older than the upstream change keeps the -behaviour it had rather than being told, falsely, that nothing on the object is sortable. - -Three things follow in the grid. The header click on a refused column ceases to exist, so -neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is -reachable from it. A sort PERSISTED before the signal existed is filtered out of both what -the grid renders and what it emits, so a restored personalization cannot ride back into -the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the -PUT still fires. And the relational carve-out is untouched and deliberately not delegated -to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored -foreign key and both runtime doors accept ordering by it), while the grid withholds that -header for a different reason — a column of names ordered by an invisible id. - -Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts -those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced -drift in mirror image. diff --git a/.changeset/5733-changeset-presence-population.md b/.changeset/5733-changeset-presence-population.md deleted file mode 100644 index a50347b9c0..0000000000 --- a/.changeset/5733-changeset-presence-population.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Tooling only; no published package behaviour changes. - -`scripts/check-changeset-presence.mjs` counted a changed file as a package's source -only when it sat under `/src/`. `apps/console/index.html` does not — it is the -console's HTML entry, compiled into the published `dist/`, and it carries two inline -classic scripts that run in every user's browser during parse. A pull request editing -only that file changed shipped console behaviour while the gate reported -`No source of a released package changed in this range` (objectui#5733). - -The population is now the package's published, executable source: `/src/`, plus -the bundler's HTML entry, plus whatever the package's own `package.json` `files` list -publishes verbatim — minus documentation and licences, which ship but are not -behaviour. On this tree the widening adds exactly three files -(`apps/console/index.html`, `apps/console/plugin.ts`, `packages/runner/index.html`) -and nothing else in any package directory. diff --git a/.changeset/5753-providers-drop-spec-dep.md b/.changeset/5753-providers-drop-spec-dep.md deleted file mode 100644 index eb0756fe59..0000000000 --- a/.changeset/5753-providers-drop-spec-dep.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/providers': patch ---- - -`@object-ui/providers` no longer declares `@objectstack/spec` as a dependency. Nothing -in the package imports it, so consumers stop installing it on this package's account -(objectui#5753). - -The edge was live for exactly one release cycle. It was promoted from `devDependencies` -to `dependencies` when `ThemePreference` was derived from the spec's `ThemeMode` union, -because the package's public `.d.ts` then referenced the spec. objectui#5716 re-pointed -that derivation at `@object-ui/types` (`ThemeMode` / `THEME_MODES`), which removed the -last three import sites — `src/types.ts` and the two retirement-era test files — and -left the declaration behind with no reader. - -Re-measured on `origin/main` at `ad0f5f11f` before removal, with a positive control so -the empty result is a real absence rather than a broken command: the import-shaped grep -(`from` / `require(` / `import(` / `vi.mock(` against `@objectstack/spec`, bare name and -every subpath) returns **0** hits under `packages/providers/` and **434** across -`packages/` + `apps/` — same command, same invocation. The only surviving mentions in -the package are the declaration itself, immutable `CHANGELOG.md` history, and a prose -comment in `tsconfig.test.json` that this change corrects. - -No API change and no behaviour change: `dist/types.d.ts` imports only `react` and -`@object-ui/types`, and no emitted file references a spec symbol. Consumers on an -isolated `node_modules` (pnpm) never had supported access to the spec through this -package, so nothing they could legitimately import goes away — the change is to the -install graph only, which is why it is scored `patch` rather than `minor`. diff --git a/.changeset/5754-ci-runs-hook-selftests.md b/.changeset/5754-ci-runs-hook-selftests.md deleted file mode 100644 index ed261bbe01..0000000000 --- a/.changeset/5754-ci-runs-hook-selftests.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -CI-only change: no published package's `src/` changed. - -Adds `.github/workflows/hook-selftests.yml`, a new PR/push gate over -`.claude/hooks/*.selftest.sh` — the hermetic self-test matrices for the -PreToolUse guards behind the worktree-first and never-`git stash` rules. Runs -on any PR touching `.claude/hooks/**` or the workflow itself; fails the build -the moment either matrix goes red. Does not modify the hooks or their -self-tests. Paired writes: `content/docs/guide/ci-cd-pipeline.md` (workflow -inventory) and `scripts/dependabot-merge-gate.mjs` (classifies the new check -as `OPTIONAL_CONTEXTS`, following `Changeset Bump Policy`'s path-filtered -shape). diff --git a/.changeset/5775-lane-changeset-reader-mirror.md b/.changeset/5775-lane-changeset-reader-mirror.md deleted file mode 100644 index 0a8e5a5070..0000000000 --- a/.changeset/5775-lane-changeset-reader-mirror.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -CI-only change: the `lane` job in `.github/workflows/changeset-release.yml` now scans -`.changeset/pre/*.md` as well as `.changeset/*.md`, and matches the reader's README -exclusion case-insensitively (`/^README\.md$/i`) instead of comparing the one exact -spelling. `scripts/__tests__/changeset-release-lane-mirror.test.ts` pins the step's -script against the installed `@changesets/read`, executed rather than transcribed, so a -dependency bump that changes the ignore list fails there instead of drifting. - -No published package changes, and no release behaviour changes: `pending_changesets` -gates only the release job's clear step, and both edits make it count MORE, never less. diff --git a/.changeset/5793-spec-range-floors.md b/.changeset/5793-spec-range-floors.md deleted file mode 100644 index 74fd71c447..0000000000 --- a/.changeset/5793-spec-range-floors.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`@object-ui/plugin-detail` now declares `"@objectstack/spec": "^17.1.0"` rather than -`^17.0.0`, which is the lowest published spec that carries the symbol its own build -output re-exports (objectui#5793). - -`dist/renderers/record-reference-rail.d.ts` reads -`export type { ReferenceRailEntry } from '@objectstack/spec/ui'`, and -`ReferenceRailEntry` first appears in `@objectstack/spec@17.1.0` — measured against the -published tarballs, not the installed tree: the name is absent from 17.0.0's -`dist/ui/index.d.mts` (425 exported names) and present in 17.1.0's (442). So the old -range was a claim the package could not honour. Any consumer resolution that lands -17.0.0 — a sibling pinning it exactly, an `overrides` entry, a mirror a minor behind — -satisfied `^17.0.0` and got a dangling type re-export. - -Nothing a consumer installs today changes: normal resolution already picks the newest -17.x, and `pnpm-lock.yaml` still resolves 17.2.0 on this edge after the bump. The change -is to the declared floor only, which is why it is scored `patch` rather than `minor` — -the same reasoning objectui#5753 used for the other direction on this dependency. - -The bump is one instance; the durable half is `scripts/check-spec-range-floors.mjs`, a -gate that compares every published package's `dist` imports of `@objectstack/spec/*` -against the export set of that package's own declared minimum. It runs on the publish -path (`pnpm changeset:publish`) and nightly, and it names the symbol behind every range -it asks for. No other package's floor is touched: the gate finds nothing else to justify -one across the 19 packages that declare the spec in a consumer-facing field. diff --git a/.changeset/5795-related-list-inherit-list-view-sort.md b/.changeset/5795-related-list-inherit-list-view-sort.md deleted file mode 100644 index ba4b8789e3..0000000000 --- a/.changeset/5795-related-list-inherit-list-view-sort.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-detail': minor ---- - -An auto-derived related list now orders its rows by the CHILD object's default list view -`sort`, instead of falling to the server's primary-key order (objectui#5795). A task -version's "check items" tab whose child object declares `sort: [{ field: 'seq_no' }]` -renders 10/20/30/40; before this it rendered whatever order the ids happened to give — -20/30/10/40 in the reported case — while the child object's own list page obeyed the -declaration. - -**Declared as user-visible, deliberately, even though no key was added.** The contract -question ("where does a derived related list's sort declaration live?") was ruled on -objectstack#11345 (maintainer, 2026-08-23) as **direction 1**: inherit the child's list -view sort, and add **no** new spec key — the field-level `relatedListSort` the issue also -proposed was explicitly not approved. So there is nothing new to author, and -`record:related_list.sort` was already declared, parsed and consumed; this fills it. What a -host observes is nonetheless new: a derived related-list descriptor gains a populated -`sort` where it had none, and the query it issues gains an `$orderby`. An app whose child -objects declare a default list order will see those tabs re-order on upgrade — which is the -point of the change, and is why this is not a patch. - -Nothing is inherited where nothing was declared: a child object with no default list-view -sort produces the same descriptor, the same node and the same `$orderby`-free query as -before. - -The two `sort` surfaces declare the same union and mean different things by its string arm -— a `ListView` string is the legacy space-separated `'seq_no desc'`, while the related -list's own reader takes `'field'` / `'-field'` — so the inherited value is normalized to -the array arm once, at the derivation, through `@object-ui/core`'s -`convertSortToQueryParams` (the repo's single definition of both authored dialects). An -un-normalized inherit would have ordered by a field literally named `seq_no desc`. - -Known and unchanged: `$orderby` is only assembled while the related list is in windowed -(server-paged) mode, so a declared *or* inherited sort still disappears while the built-in -client text filter is active. That hole pre-dates this change and affects the authored prop -identically; it is now pinned as a recorded fact in -`plugin-detail/src/__tests__/RelatedList.sortDroppedOutsideWindowed.test.tsx` rather than -fixed here. diff --git a/.changeset/5853-tablecolumn-type-canonical-union.md b/.changeset/5853-tablecolumn-type-canonical-union.md deleted file mode 100644 index 47431dd506..0000000000 --- a/.changeset/5853-tablecolumn-type-canonical-union.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-dashboard': patch ---- - -`TableColumn.type` now has ONE canonical value set across all three ends that disagreed -(objectui#5853, maintainer ruling 2026-08-25, Option B: the 8-literal interface union is -canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' | -'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted -anything; the renderer branched on a third set and could only read the key through an -`as any` cast. - -## ⚠️ Accept-set narrowing — these spellings stop validating - -`TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now -`z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse -time with `type` named in the error path. Spellings that validated before and are **refused -now**, grouped by why they were being written: - -- **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`, - `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is - the card's headline case: it validated, matched no renderer branch, and the column fell - through to plain text rendering with nothing reported. That silent fall-through is the - lenient-validation face that lets AI-authored metadata errors through, and it is now a - loud parse failure. -- **Object-schema field types written into a column slot** — `'select'`, `'lookup'`, - `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of - `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD, - not on the column: a column gets its dedicated widget from the field definition behind its - `accessorKey`, never from `type`. - -**Authored metadata in this repo needs no migration.** Measured before tightening, across -`examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files -plus the docs and playground sources): **zero** authored `TableColumn.type` values outside -the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column -position anywhere in the repository. If you author `type` on a table column, check it -against the eight; if the value describes the FIELD rather than the column, remove it. - -## The renderer's undeclared vocabulary disappears instead of being declared - -`int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES` -and `datetime-local` had its own editor branch, none of them declared. They arrived because -column-inference producers forwarded an object schema's field type **verbatim** into -`TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred -value onto the declared vocabulary at their emit seam via the new -`normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`, -`datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the -column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`) -and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote -the raw field type into the same slot. - -Dropping the annotation is behaviour-preserving at the only consumer that reads the key. -`data-table`'s inline editor branches on `date`, `datetime` and the numeric set and -otherwise falls through to a text input — which is exactly the `undefined` path. The -dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`, -which resolves the field through `column.accessorKey` and never reads `type`. - -## New public API - -`@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single -declaration the zod mirror builds its enum from, so the two cannot drift), the -`TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast -in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared -spelling is a tsc error rather than a silent widening. - -A value-level parity pin covers all three ends -(`packages/types/src/__tests__/table-column-type-canonical.test.ts` and -`packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`). -objectui#5684's guard is key-set only and cannot see value drift — `type` was present on -both sides the whole time — which is how this instance survived while its siblings were -caught. A future inference value turning that pin red is by design; the note at the pin says -so, and names the two correct repairs. diff --git a/.changeset/5856-plugin-grid-catalog-entries.md b/.changeset/5856-plugin-grid-catalog-entries.md deleted file mode 100644 index e44709e0e4..0000000000 --- a/.changeset/5856-plugin-grid-catalog-entries.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Docs and catalog fixtures only, no shipped code touched: the two `plugin-grid` catalog -entries are now real `object-grid` nodes instead of hand-built static card layouts. -`content/docs/plugins/plugin-grid.mdx` mounted `product-inventory-grid` and -`team-members-grid` under `PluginLoader plugins={['grid']}` while both authored only -`badge button card flex stack text` — pictures of a grid, not a grid. They are replaced -by `object-grid-columns` (a `ListColumn` set with `sort`, `searchableFields` and -`pagination`) and `object-grid-selection-summaries` (multi-row `selection`, named -`rowActions` / `bulkActions`, per-column footer `summary` roll-ups), both querying the -docs gallery's demo data source the same way the `plugin-view` entries have since -objectui#5113. - -The two mock-ups are legitimate static layouts filed under the wrong plugin, so they are -re-seated into `components-layout-card` as `inventory-table-card` and `team-roster-card` -rather than deleted — deleting a catalog entry moves three corpus-wide counters -(`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx`, and the `className`-carrying layout -node and `stack` node floors in `layout-props-conversion.test.tsx`), and a floor that -moves because a fixture was deleted is indistinguishable later from one that moved -because coverage regressed. All three are unchanged, with no floor edited. - -objectui#5113's pin in `catalog-gallery-render.test.tsx` is extended to cover -`plugin-grid` through an explicit two-entry category/type map, keeping both of its halves -— every entry authors the type its own package registers, and the rendered tile shows a -record that exists only in the gallery's data source. diff --git a/.changeset/5859-retire-orphaned-object-exports.md b/.changeset/5859-retire-orphaned-object-exports.md deleted file mode 100644 index 7e460424ea..0000000000 --- a/.changeset/5859-retire-orphaned-object-exports.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ObjectTrigger` and `ObjectRelationship` are removed from `@object-ui/types` — two -hand-written interfaces orphaned by the `ObjectSchemaMetadata` derivation -(objectui#5859, triage adjudication 2026-08-24; the derivation itself was -objectui#5362). - -**Breaking for anyone importing either name.** The two symbols are, verbatim: - -- `ObjectTrigger` — the `{ name, when, on, condition?, action, config? }` trigger - configuration -- `ObjectRelationship` — the `{ name, object, type, foreign_key?, cascade_delete? }` - relationship configuration - -Both existed solely to type members of the retired hand-written object-document mirror: -`triggers?: ObjectTrigger[]` and `relationships?: ObjectRelationship[]`. objectui#5362 -replaced that mirror by deriving `ObjectSchemaMetadata` from `@objectstack/spec/data`'s -`ServiceObject`, and the spec's object document declares neither member — so the two -interfaces have had nothing to type since. objectui#5362 deliberately left them standing -because cutting published exports is a separate decision from the ruled derivation; this -is that decision. - -Measured before removing, on `main`: zero references in this repo outside the declaration -and the `src/index.ts` re-export (`packages/`, `apps/`, `examples/`, `*.ts`/`*.tsx`, -`node_modules` and `dist` excluded), and zero in the sibling `objectstack` checkout, which -does import `@object-ui/types` in eleven files. Absence of a spec correspondence was -verified at type level against the installed `@objectstack/spec` 17.2.0 rather than -inherited from the card: neither `triggers` nor `relationships` is a key of -`ServiceObject`. `DataModelDesigner`'s `relationships` state is its own local model and -never referenced these types. - -**That measurement cannot see npm.** In-repo zero is not consumer zero — an external -application importing either name from `@object-ui/types` will fail to compile after this -release, and nothing in this repository can detect that. Both names are spelled out above -so a host can search its own sources for them. If you were importing either, the shapes -were client-side vocabulary with no runtime behaviour and no spec backing: copy the -interface into your own code, or model the concept against `@objectstack/spec`, which owns -the object document. - -Type-only change; nothing is emitted and no runtime behaviour moves. Ships `minor`, not -`major`, per the version-alignment convention in AGENTS.md — objectui's major tracks -`@objectstack`'s, and breaking changes of objectui's own carry `minor` with the semantics -stated in the body. diff --git a/.changeset/5867-layout-plaintext-fences-batch5.md b/.changeset/5867-layout-plaintext-fences-batch5.md deleted file mode 100644 index 089856b3da..0000000000 --- a/.changeset/5867-layout-plaintext-fences-batch5.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Documentation only: re-fenced the 12 TypeScript blocks in `content/docs/layout/**` that were -fenced `plaintext`, so `check-doc-snippet-types` compiles them, and fixed the four blocks it -then reddened (a missing `AppShellBranding` import, three examples using components and values -they never imported, and two `AppShell` examples that passed no children). Lowered -`KNOWN_UNHIGHLIGHTED_TS_FENCES` by the three layout files in the same change. No published -behaviour changes. diff --git a/.changeset/5867-plaintext-fenced-ts-batch1.md b/.changeset/5867-plaintext-fenced-ts-batch1.md deleted file mode 100644 index 80ee21ed4b..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch1.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Docs only, publishes nothing: twenty-four TypeScript snippets across thirteen -`content/docs/fields/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 1 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts`; genuinely -prose blocks on the same pages (a bare object literal, a comment-only -illustration) are left alone. The gate's blocks-to-compile count rises from 157 -to 181 — exactly the batch — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the covered/ungated sets unmoved. The blocks also stop -rendering as unstyled plaintext and pick up TypeScript highlighting, which is -the reader-visible half. diff --git a/.changeset/5867-plaintext-fenced-ts-batch2.md b/.changeset/5867-plaintext-fenced-ts-batch2.md deleted file mode 100644 index 6c65b12f7c..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch2.md +++ /dev/null @@ -1,20 +0,0 @@ ---- ---- - -Docs only, publishes nothing: twenty-one TypeScript snippets across three -`content/docs/core/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 2 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts` / `tsx`; -genuinely prose blocks on the same pages are left alone. Compiling them found -three real documentation defects, now fixed: `app-schema` documented an -`AppSchema` type `@object-ui/types` has never exported (the shipped name is -`AppComponentSchema`), and two `enhanced-actions` callbacks authored an -`ActionCallback.dialog` object with no `type` key, which `SchemaNode` requires. -Blocks that referenced ambient names are made self-contained with the import a -reader copying them needs. The gate's blocks-to-compile count rises from 181 to -202 — exactly the batch — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the covered/ungated sets unmoved. The blocks also stop -rendering as unstyled plaintext and pick up TypeScript highlighting, which is -the reader-visible half. diff --git a/.changeset/5867-plaintext-fenced-ts-batch3.md b/.changeset/5867-plaintext-fenced-ts-batch3.md deleted file mode 100644 index 523ebb6a4d..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch3.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Docs only, publishes nothing: seventeen TypeScript snippets across ten -`content/docs/fields/*.mdx` reference pages were fenced ```plaintext, so -`check-doc-snippet-types` — which reads only `ts` / `tsx` fences — never saw -them (objectui#5867, batch 3 of N). Triage's classifier is the one applied: a -plaintext block whose first line starts with `import` / `export` / `interface` / -`type X =` / `const x: T` is code, and those fences are now `ts`; genuinely -prose blocks on the same pages are left alone. Compiling them found two real -documentation defects, now fixed: `image` annotated `ImageFieldSchema.value` -with `FileMetadata`, a name `@object-ui/types` does not export — it was -deliberately renamed to `UploadedFileMetadata` (objectstack#4115) because the -spec's same-named type is the storage layer's file record, a different shape — -and `lookup` referenced `DataSource` without importing it. The gate's -blocks-to-compile count rises from 206 to 223 — exactly the batch — with -diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and the -covered/ungated sets unmoved. The blocks also stop rendering as unstyled -plaintext and pick up TypeScript highlighting, which is reader-visible. Three -further `fields` pages are excluded by measurement with blockers filed: -`auto-number` (an ambient backend `db` handle), `object` (imports `ajv`, not a -workspace dependency) and `location` (one fence welds a JSX element and a bare -metadata object literal, so it parses as neither `ts` nor `tsx`). diff --git a/.changeset/5867-plaintext-fenced-ts-batch4.md b/.changeset/5867-plaintext-fenced-ts-batch4.md deleted file mode 100644 index 88d2a22e54..0000000000 --- a/.changeset/5867-plaintext-fenced-ts-batch4.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Docs only, publishes nothing: nineteen TypeScript snippets across -`content/docs/blocks/block-schema.mdx` and six `content/docs/plugins/*.mdx` -reference pages were fenced ```plaintext, so `check-doc-snippet-types` — which -reads only `ts` / `tsx` fences — never saw them (objectui#5867, batch 4 of N). -Triage's classifier is the one applied: a plaintext block whose first line -starts with `import` / `export` / `interface` / `type X =` / `const x: T` is -code, and those fences are now `ts`; genuinely prose blocks on the same pages -are left alone. Compiling them found real documentation defects on -`block-schema`, now fixed: the *Complete Example*, *Block Slots* and -*Marketplace Example* blocks annotated `BlockSchema` / `SchemaNode` / -`BlockLibrarySchema` without importing them, and three blocks referenced values -defined in other blocks on the page. Resolving `BlockLibrarySchema` then -un-masked a defect the unresolved name had been suppressing: both marketplace -listings carried `schema: { /* block schema */ }`, an empty object that -`BlockLibraryItem.schema` rejects because `BlockSchema` requires `type`. The -gate's blocks-to-compile count rises from 206 to 225 — exactly the batch — with -diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and the -covered/ungated sets unmoved. The blocks also stop rendering as unstyled -plaintext and pick up TypeScript highlighting, which is the reader-visible half. diff --git a/.changeset/5874-expandable-field-family-convergence.md b/.changeset/5874-expandable-field-family-convergence.md deleted file mode 100644 index bcbee7fa1d..0000000000 --- a/.changeset/5874-expandable-field-family-convergence.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -Four more private copies of the reference-bearing field family converge onto -`@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`, and the "fourth and last private copy" claim -that `paramToField` still stated is corrected (objectui#5874, objectui#5875). - -Each copy diverged from the published family in BOTH directions, so this is a behaviour -change on every face and not a refactor: - -- **`user` and `tree` are now treated as relations.** Both carry the same foreign-key - storage as `lookup` and resolve through the same expand path (objectui#2032), and each - face's own stated reason for special-casing `lookup` applied to them verbatim — so - gaining them restores the rule each face already meant. A `user` / `tree` field is now - read-only in the quick-look drawer (`RecordDetailDrawer`), where the drawer wires no - relation picker and a plain text input let a user overwrite the relation with a - free-form string; it gets the wide layout basis in the record header's highlights strip - (`HeaderHighlight`), whose inline editor is a record picker; and a field-backed action - param over one now inherits the picker config it needs (`resolveActionParams`). -- **`master_detail` is now treated as a relation by `resolveActionParams` too** — it was - the only face missing that member as well, so a field-backed `master_detail` action param - inherited no `referenceTo` at all and degraded to the unexplained "paste a record id" - text input that objectui#3405 exists to prevent. -- **The undeclarable `reference` spelling is gone from the three field-type faces.** - Measured against `@objectstack/spec`'s closed `FieldType` vocabulary with live controls - (`lookup` / `master_detail` / `user` / `tree`) and dead ones (the retired `owner`, plus a - nonsense spelling): `reference` is absent, so no spec-compliant object schema could - declare a field that reached those branches. It sat exactly where `owner` sat before - objectui#4814 retired it — dead weight that read as live capability. - -`resolveActionParams` keeps answering for `reference`, deliberately and by a different -route: it is refused by the spec's `ActionParamSchema` too, but the dialog still accepts it -from params already authored with it, and that acceptance belongs to the one alias table in -`paramToField` rather than to a hand-copied membership test. This face now asks the shared -family over the widget key that table produces — the same expression `paramToField` -evaluates one step later, so the half that populates a param's picker config and the half -that forwards it can no longer disagree. - -No face copies the set: each calls `.has()` on the object `@object-ui/core` exports, and -each carries an identity pin (a spy on that `has`) so a member-identical private copy fails -instead of quietly re-forking the table. diff --git a/.changeset/5876-one-lookup-predicate.md b/.changeset/5876-one-lookup-predicate.md deleted file mode 100644 index a774fa8767..0000000000 --- a/.changeset/5876-one-lookup-predicate.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -The dashboard package now holds ONE relation predicate instead of two that -agreed only because a sweep had just aligned them (objectui#5876). - -`computeLookupExpand` in `ObjectDataTable.tsx` carried its own `isLookup`, -byte-identical to the exported `isLookupType` in `recordFields.tsx` after -objectui#5692 pointed both at `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`. -Nothing kept them aligned: a future edit to either — a member added, the -retirement gate moved — would have re-forked the `$expand` decision from the -predicate whose docblock claims to drive it. `computeLookupExpand` now calls -`isLookupType`, which gains its first production consumer, and the module no -longer imports the shared family or the retirement gate at all. - -**No behaviour changes**, and that is measured rather than assumed: - -- The two bodies were identical, so every boolean answer — `tree` is expanded, - `reference` is not, ordinary relations are — is the same before and after. -- The retired-spelling warning is not emitted a different number of times. - `reportRetiredFieldType` dedupes per SPELLING in one module-level set inside - `@object-ui/core`, which both bodies already shared, so routing two callers - through one function cannot change the count. - -Nothing published moves: `isLookupType` is not re-exported from -`@object-ui/plugin-dashboard`'s entry, so this is internal shape only. - -Because a refactor with no observable delta cannot be pinned by a behavioural -test — a byte-identical local copy satisfies every assertion you can write -about `$expand` — the pin is identity, in -`__tests__/expandableFamily.identity-5692.test.ts`: `computeLookupExpand` is -observed CALLING `isLookupType`, and `ObjectDataTable.tsx` is read at source -level to confirm no second body survives for it to call instead. diff --git a/.changeset/5902-marked-refusal-drag-write-surfaces.md b/.changeset/5902-marked-refusal-drag-write-surfaces.md deleted file mode 100644 index 831e3de14c..0000000000 --- a/.changeset/5902-marked-refusal-drag-write-surfaces.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-form': patch ---- - -Let a producer-marked refusal reach the drag-write surfaces (objectui#5902). - -The kanban card-move toast, the calendar drag-to-reschedule toast and the OCC -conflict dialog each substituted a generic string for a refusal the producer had -marked as user-facing (`userMessage`), so a user was told "Save failed" where the -application author had written a sentence addressed to them. All three now read -the marking through the shared `declaredUserMessage` reader, which covers both -places the adapter boundary parks it — the typed member on -`ConcurrentUpdateError` and the details bag on `DataApiValidationError`. - -Nothing unmarked changes: the reader answers `null` for it, so every existing -generic substitution — including the localized "not authorized" message that -keeps raw server diagnostics away from end users — still governs unmarked -refusals exactly as before. - -The two toasts substitute; the conflict dialog augments. Its description also -explains what the destructive "Overwrite" button does, which is affordance copy -that surface owns rather than a refusal message, so the marking leads and that -paragraph stays. diff --git a/.changeset/5903-objectgantt-declared-keys.md b/.changeset/5903-objectgantt-declared-keys.md deleted file mode 100644 index de0c6da6f5..0000000000 --- a/.changeset/5903-objectgantt-declared-keys.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -`ObjectGanttSchema` declares the ten gantt keys `ObjectGantt` actually reads -(objectui#5903, triage 2026-08-24). Every one is a real, working, documented -feature — `readOnly`, `mobileReadOnly`, `markers`, `navigation`, `skipWeekends`, -`holidays`, `criticalPath`, `showBaselines`, `persistLayout`, `viewName` — and -none of them was discoverable from the published type, because all ten were read -as `(schema as any).K`. The cast was the load-bearing part: it kept the read -invisible to `tsc`, to the zod mirror and to the designer's registry `inputs`. - -Both halves move together. The TS declaration (`packages/types/src/objectql.ts`) -and its zod mirror (`src/zod/objectql.zod.ts`) gain the same ten keys at the same -requiredness — all optional — so the `zod-mirror-parity` ratchet stays at zero -drift for this pair and no `KnownDrift` entry is added. `navigation` is taken -from `@objectstack/spec`'s `NavigationConfigSchema` by reference rather than -restated, matching `ObjectGridSchema.navigation`. - -`ObjectGanttProps.schema` is retyped from `ObjectGridSchema` to -`ObjectGanttSchema`. That is what makes the declaration load-bearing: the ten -keys are not grid keys, so with the old prop type, dropping the casts would have -left the reads landing on `BaseSchema`'s index signature — the same invisibility -in different syntax. The grid-style `{ gantt: { … } }` block is unaffected; -`getGanttConfig` reads it through that index signature exactly as before, and the -registered renderer passes `schema: any`, so no runtime shape is turned away. - -Accept-set change, stated plainly: a **declared** key is now type-validated, so -`readOnly: 'yes'` is refused where it used to parse green — the same narrowing -objectui#5074 landed for `viewMode`. An **undeclared** key is still accepted: -`BaseSchema` is `.passthrough()` and carries an index signature (objectui#5155's -structural ceiling), so declaring these ten did not buy rejection of a -misspelling. `packages/types/src/__tests__/gantt-declared-keys.test.ts` pins both -halves so neither can be misread. - -The eleventh reported key, `label`, needed no declaration — `BaseSchema` already -carries it — so only its cast was dropped. diff --git a/.changeset/5916-record-path-stage-state-accessible-name.md b/.changeset/5916-record-path-stage-state-accessible-name.md deleted file mode 100644 index 7b6bf579e1..0000000000 --- a/.changeset/5916-record-path-stage-state-accessible-name.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/i18n': patch ---- - -`record:path` now announces each stage's state, not just its label (WCAG 2.2 SC 1.4.1) - -The lifecycle path distinguished travelled, upcoming and lost-terminal stages with -colour plus a `✓`/`✗` glyph, and both glyphs are `aria-hidden` decoration. -`aria-current="step"` marked the current stage and nothing else, so a screen-reader -user heard a run of identically-announced items — and a rejected stage announced -exactly like an ordinary stage the record had not reached yet. - -Each stage now carries an accessible name composing its (already picklist-localized) -label with its state, from five new `detail.pathStage*` keys translated in all ten -locale packs. The glyphs stay decorative and the readout's `role="listitem"` / -`aria-current` semantics are unchanged. - -The name is composed into `aria-label` rather than visually-hidden text because -`listitem` takes its name from the author only: text placed inside a stage computes -to an empty accessible name, so the visually-hidden shape would have looked right in -the markup and delivered nothing to the accessibility tree. diff --git a/.changeset/5924-live-headroom-invariant.md b/.changeset/5924-live-headroom-invariant.md deleted file mode 100644 index 11faf78eb2..0000000000 --- a/.changeset/5924-live-headroom-invariant.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Build tooling and CI only — `scripts/check-eager-closure-budget.mjs`, its unit -test, and a comment in `.github/workflows/performance-budget.yml`. Nothing ships -from this change. - -The console eager-closure gate stated a binding constraint on its own -sensitivity — the headroom above the measured payload must stay SMALLER than the -89 KiB regression the gate exists to catch — and then checked it between two -constants frozen in the same module. That assertion is an arithmetic fact about -the file, true regardless of what the console weighs. The closure shrank 706,013 -gzipped bytes below the pinned baseline without the ceiling following it down, -the live headroom reached 8.6x the regression size, and the check that was -supposed to notice stayed green throughout: a demonstrated +158,006-byte eager -regression, 1.7x the incident this gate was built for, passed with a green tick. - -`evaluateHeadroomSensitivity` now derives that headroom from the report the gate -just read, for the aggregate ceiling and for each of the three per-chunk -ceilings, and treats a ceiling that has drifted more than one regression above -its own measurement as an ERROR (exit 2) rather than a size failure — it is a -verdict about the gauge, the same asymmetry a budgeted chunk absent from the -report already carried. The constant-vs-constant assertions stay as a secondary -guard. - -The aggregate ceiling is re-baselined downward as the decision this records: -`MAX_EAGER_CLOSURE_GZIP_BYTES` 4,086,000 to 3,345,000 over a `BASELINE` moving -4,005,911 (`4c1623c0c`) to 3,299,898 (`48e53814e`). Headroom goes from 8.63x the -regression size to 0.49x. Lowering a ceiling toward reality is a tightening: no -build that passed before and measures under the new line fails after it. The -floor is unchanged — a ceiling is never put below a measured figure. diff --git a/.changeset/5926-empty-action-visible-when.md b/.changeset/5926-empty-action-visible-when.md deleted file mode 100644 index 4599bf9b70..0000000000 --- a/.changeset/5926-empty-action-visible-when.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/components': minor ---- - -Route a `data-table`'s `emptyAction` node through `SchemaRenderer`, so a `visibleWhen` -authored on it is actually evaluated (objectui#5926 gap 1). - -`visibleWhen` is not a per-block concern in this platform. It is enforced **once, -generically**, in `packages/react/src/SchemaRenderer.tsx`: `shouldHide` tests `visibleWhen` -ahead of the hoisted `visible` (objectui#5454), sets `_hidden`, and the `_hidden` early -return fires **before** the registry dispatches. A block renderer cannot ignore the gate, -because it never sees the node. - -`emptyAction` was the one authored-node exception in the tree. The empty-state CTA slot -resolved the registry **directly** — `ComponentRegistry.get(node.type)` — and mounted the -result itself, so the node never passed through `SchemaRenderer` and its `visibleWhen` was -**never evaluated**. `@objectstack/spec` accepts the key (`SchemaNodeSchema` carries -`visibleWhen`, and `data-display.zod.ts` types `emptyAction` as a `SchemaNode`), so an -author wrote a gate, the platform took it, and nothing enforced it — declared-not-enforced, -the same class objectui#5401 / #5505 closed for `record:alert`, one level down. - -Measured on the branch point, mounting a `data-table` with no rows so the empty state is -actually reached: an `emptyAction` carrying -`visibleWhen: { dialect: 'cel', source: 'features.can_create == true' }` under an ambient -scope of `features.can_create = false` **rendered**, and so did the bare-string spelling of -the same predicate. Both now render nothing. The three must-show cases were pinned in the -same file and were green before and after — an `emptyAction` whose predicate resolves -**true**, one declaring **no** `visibleWhen`, and one whose predicate names an unbound root -(the central gate fails soft to visible, and this slot now gives the same answer as every -other node rather than a private one). - -The fix is a **route**, not a new check: no `visibleWhen` test was added to `data-table.tsx`. -A local check on this slot would have been a fourth evaluator, which is the drift -`page:tabs`' item-level predicate already records on this card. The slot now mounts its -authored node exactly the way the `empty` renderer's `action` slot always has. - -**Behaviour change worth declaring, beyond the gate itself.** No declaration moves and no -new key is accepted, but two host-observable answers change on this slot: - -- An `emptyAction` whose `visibleWhen` resolves false stops rendering. That is the fix. -- An `emptyAction` whose `type` is missing or names an unregistered component now gets the - platform's uniform "unknown component type" report instead of rendering as silent - nothing. Malformed metadata gets one answer across the tree rather than a private one - here — but a page that shipped a typo'd `emptyAction` type went from invisible to visibly - reported. diff --git a/.changeset/5927-zod-mirror-group-a-widenings.md b/.changeset/5927-zod-mirror-group-a-widenings.md deleted file mode 100644 index aae2128dd3..0000000000 --- a/.changeset/5927-zod-mirror-group-a-widenings.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/types': minor ---- - -**`@object-ui/types/zod` now accepts seven spellings its own TypeScript declarations already declared** - -Seven keys across five hand-written zod mirrors refused values the published TS -types invite and the renderer implements — `declared !== enforced` on a published -validator. The mirrors are widened to their declarations. Nothing is narrowed and -nothing previously accepted is rejected, so this is additive for every author and -every host: schemas that parsed before still parse. - -The newly-accepted spellings, so a host can search for them: - -| schema (`@object-ui/types/zod`) | key | now also accepts | -|---|---|---| -| `ButtonGroupSchema` | `variant` | `secondary`, `destructive`, `ghost`, `link` | -| `ButtonGroupSchema` | `size` | `icon` | -| `ObjectChartSchema` | `chartType` | `column`, `horizontal-bar`, `donut` | -| `FormSchema` | `validationMode` | `onTouched`, `all` | -| `SelectSchema` | `defaultValue`, `value` | `boolean` | -| `DataTableSchema` | `selectable` | `'single'`, `'multiple'` (alongside `boolean`) | -| `ViewSwitcherSchema` / `ViewTypeSchema` | `defaultView`, `activeView`, `views[].type` | `chart` | - -**Each one was decided by measuring the renderer, not by matching the declaration.** -Widening a mirror to its declaration is only correct where the running code -implements the missing spelling; where a spelling is dead, the right fix is to -withdraw it from the declaration (ADR-0049 enforce-or-remove), not to teach the -validator to accept something that renders nothing. The read sites: -`buttonVariants`' `cva` map (`components/src/ui/button.tsx`) carries all six -variants and all four sizes; `AdvancedChartImpl` normalizes `column` to `bar`, -maps `horizontal-bar` to a real `BarChart` layout and gives `donut` its own inner -radius; `useForm({ mode })` hands `validationMode` straight to react-hook-form, -whose `isOnTouch` / `isOnAll` branches implement `onTouched` and `all`; -`toControlValue` / `matchOptionValue` (#3090) round-trip a boolean option value -with its type intact; `resolveSelectionMode` implements `'single'` as -replace-on-select with no select-all header, distinct from `'multiple'`; and -`chart` is a rendered view type with its own `case` in both `ListView` and -`ObjectView`. - -Consumer-visible type effect: `z.infer` of these schemas widens accordingly. -Widening an input contract cannot break a caller that was already passing a -narrower value, but code that exhaustively switches on the inferred union — e.g. -a `switch` over `chartType` with no `default` — will want the new arms. - -Refs objectui#5927 (group A of the 17 measured mirror drifts). The remaining -classes are rulings rather than edits and stay in the `KnownDrift` ledger. diff --git a/.changeset/5940-detail-collection-decline-to-fetch.md b/.changeset/5940-detail-collection-decline-to-fetch.md deleted file mode 100644 index 2703d95554..0000000000 --- a/.changeset/5940-detail-collection-decline-to-fetch.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`object-master-detail-form` declines to fetch a detail collection whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. - -`childObject` is REQUIRED on `MasterDetailDetailConfig` and is what every downstream read is keyed -on — `deriveDetail(d.childObject, …)`, the child-schema cache, and the FK scope of each child -fetch. But a detail entry reaches the renderer straight off an authored schema, so a malformed one -arrives with the key `undefined`, and the resolve effect asked the data layer for it anyway. -Measured: mounting the block with a detail entry that carries no `childObject` issued -`getObjectSchema(undefined)` — a real backend receives a query for an object literally named -`undefined`, and whatever it returns becomes the console's problem. - -The resolve effect now skips such an entry and warns, leaving it in place so the grid card shows -its config hint and the row-state array stays index-matched. This is the choice `RelatedList` -already makes for the same class of missing key (*"has no referenceField/parentId — refusing to -fetch all rows"*), and the sibling child-schema-cache effect in this same component already spelled -it `.filter(Boolean)`; the three now agree. A detail collection that names its child object fetches -exactly as before. diff --git a/.changeset/5944-preview-page-source-query-params.md b/.changeset/5944-preview-page-source-query-params.md deleted file mode 100644 index 287a5f7a82..0000000000 --- a/.changeset/5944-preview-page-source-query-params.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only change: the ADR-0080 preview harnesses' page sources are now held to -`object-ui/no-unprefixed-query-params` by that rule itself, run over the source -strings ESLint structurally cannot reach inside a template literal -(objectui#5944). No published behaviour changes — the new file is a test under -`apps/console/src/__tests__/`, and `eslint-rules/` is a repo-local plugin -directory rather than a workspace package. diff --git a/.changeset/5956-5957-record-path-a11y-residue.md b/.changeset/5956-5957-record-path-a11y-residue.md deleted file mode 100644 index d57e248f10..0000000000 --- a/.changeset/5956-5957-record-path-a11y-residue.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/i18n': patch ---- - -`record:path` finishes localizing and de-colouring its accessible names — the two residues -objectui#5916 named and deliberately left behind (objectui#5956, objectui#5957). - -**The list's own label was English on a localized surface, and the other one named nothing.** -Both the desktop and the mobile `role="list"` row did -`aria-label={schema.aria?.label || 'Record path'}`, so a zh/ja/ar session heard `Record path` -for the list while every stage inside it announced in the session locale — one control -speaking two languages at once. The fallback is now `detail.pathLabel`, translated in all ten -packs; the `schema.aria.label` author override still wins ahead of it. - -The lost-terminal alt group was a different defect wearing the same clothes: its -`aria-label="Alternative terminal stages"` sat on a bare `div`, which has the `generic` role, -and browsers expose no accessible name on a generic element. That string reached nobody — -inert, not merely untranslated — so translating it would have shipped copy to ten packs that -no user can hear. It is removed rather than given a role that takes a name, on three -measurements: nothing is lost (it was never announced), it would be redundant (every stage -inside already announces `closed lost` in the session locale after objectui#5916, in the one -place `role="list"` can carry it), and it would fork the two rows (the mobile row renders one -flat list with no alt group, so a named group would make one control expose two structures by -viewport). - -**An unreached goal terminus was distinguished by hue alone.** `railClass` paints it -`bg-emerald-500/30` where a plain upcoming stage gets `bg-muted` — the renderer's own note -calls this "a faint emerald so the goal is legible" — while both announced the identical -`{{stage}}, upcoming`. Two stages ahead of the record painted differently and read the same: -the WCAG 2.2 SC 1.4.1 class objectui#5916 closed, on the one distinction it left behind, and -reachable without authors opting in because `classify()` finds `won` through the `WON_TOKENS` -heuristic as well as an explicit `terminal: 'won'`. New key `detail.pathStageWonUpcoming` -(`{{stage}}, goal stage, not reached`), translated in all ten packs. - -Scoped to the UNREACHED goal, which is a measurement of the stylesheet rather than a -preference: a reached goal terminus paints `bg-primary` when current and `bg-emerald-500` when -completed, byte-identical to any other current or completed stage. Naming it apart would hand -a screen reader a distinction the screen does not make — the mirror image of the defect — so -it is one new key, not a pair, and a test pins that decision so it cannot drift into a fourth -state unnoticed. - -Both new keys also land in `DETAIL_DEFAULT_TRANSLATIONS`, which -`defaults-maps-mirror-en-pack` compares against the `en` pack key by key, so neither can fork -between a provider-mounted console and a provider-less embed. No existing `en` value changes, -so no pack is asked to follow an edit. diff --git a/.changeset/5965-vscode-summary-codeql-claim.md b/.changeset/5965-vscode-summary-codeql-claim.md deleted file mode 100644 index 8af3fbd59a..0000000000 --- a/.changeset/5965-vscode-summary-codeql-claim.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Doc-only removal in the (private, unpublished) `object-ui` VSCode extension package: -`packages/vscode-extension/SUMMARY.md` is deleted. The file asserted a security-scan -outcome that nothing ever produced — "CodeQL扫描通过 / 无安全漏洞 / 无已知问题" -("CodeQL scan passed / no security vulnerabilities / no known issues") — while this -repository has never had a CodeQL workflow, default-setup or otherwise. `CONTRIBUTING.md` -already states the opposite as the repo's canonical fact: it "does **not** run -static-analysis security scanning of its own source code (CodeQL or equivalent)". - -The claim was not one line. It recurred as a release-checklist tick (`- [x] 安全扫描通过`) -and twice in the conclusion, alongside a matching `- [x] 单元测试通过` in a package that -carries zero test files — the document was a frozen one-off session status report -("代码完成度: 100%", "扩展已准备好发布到VSCode Marketplace"), not living documentation. -Nothing in the repository referenced it and it never shipped: the package is -`private: true`, and `.vscodeignore` excludes `*.md` except `README.md`, so it was absent -from the VSIX. Its durable reference content (commands, snippets, configuration, project -structure) is already carried by `README.md`, `DESIGN.md`, and `PUBLISHING.md`. - -Also lowers two `objectui#4938` population floors in `scripts/__tests__/check-doc-links.test.ts` -by exactly this one deletion (packages/* 12→11, combined 15→14), so they keep failing if -the link scanner stops walking the tree. - -No published source, behaviour, or shipped artifact changes. diff --git a/.changeset/5993-button-shared-icon-resolver.md b/.changeset/5993-button-shared-icon-resolver.md deleted file mode 100644 index 3d17aa1e88..0000000000 --- a/.changeset/5993-button-shared-icon-resolver.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`ui:button` resolves its authored `icon` through the shared `resolveIcon` instead of a -byte-equivalent copy of it (objectui#5993). - -`renderers/form/button.tsx` carried its own `toPascalCase`, its own `iconNameMap` holding -the single `Home -> House` entry, and its own index into lucide's runtime `icons` record — -the same algorithm as `renderers/action/resolve-icon.ts`, but not the same function. The -`action:*` family, `complex/data-table.tsx` and both menu renderers already import the -shared one. The hazard was drift, not rendering: an alias added to `resolve-icon.ts` to -absorb a lucide retirement (the objectui#5586 / #5622 mechanism) reached every one of those -sites and silently missed `ui:button`, which would have gone on resolving the retired -spelling to nothing while the rest of the repo resolved it correctly. - -**No behaviour changes, and that is measured rather than asserted.** The two -implementations were compared over 3547 names — every one of lucide's 1767 record keys in -both spellings, plus kebab-case probes (`arrow-right`, `dollar-sign`, `user-plus`), the -`Home` alias, retired spellings and `undefined`: 3539 identical by object identity, 8 -differing only in the nullish flavour returned for a miss (the copy indexed the record and -got `undefined`; the shared resolver `?? null`s it), zero genuine forks. That one -difference cannot reach the DOM — `Icon` is consumed at exactly two sites, both -`{!isLoading && Icon && }` truthiness tests, and React renders nothing for -`null` and `undefined` alike. Icon identity, `h-4 w-4` sizing, `iconPosition`, the loading -state and the `Loader2` spinner are unchanged, and are pinned by -`renderers/form/__tests__/button-shared-icon-resolver.test.tsx`. - -Because behaviour is unchanged, the usual red-before ablation does not exist for this -change and none was manufactured. The one row in that suite that discriminates is -structural: it spies on the shared module and fails when the glyph does not come out of it, -which is red on the copy and green on the import. - -`scripts/check-lucide-icon-record-names.mjs` drops `form/button.tsx` from -`DECLARED_RECORD_READERS` in the same commit — that gate rediscovers record readers from -source on every run and fails on drift in both directions, so the removal is verified by -the gate rather than declared. It is also what now guards the dedupe: a re-inlined copy -would be discovered as an undeclared record reader and fail. The census entry for the -`button` *type* stays, its resolver re-pointed at `resolve-icon.ts`, so `ui:button`'s -authored icon names are still judged against the live record. - -`renderers/basic/icon.tsx` keeps its own copy deliberately and is untouched: `ui:icon` -draws a `SquareDashed` placeholder and warns on an unresolvable name (objectui#5631), which -the shared resolver does not do. diff --git a/.changeset/5996-eager-closure-missing-chunk.md b/.changeset/5996-eager-closure-missing-chunk.md deleted file mode 100644 index 922af1c22b..0000000000 --- a/.changeset/5996-eager-closure-missing-chunk.md +++ /dev/null @@ -1,19 +0,0 @@ ---- ---- - -Build tooling only — `apps/console/vite.config.ts`, which is not published -source (`@object-ui/console`'s `files` list carries `dist`, `plugin.*` and -`README.md`), so nothing ships from this change. - -`emit-eager-closure-report`'s `writeBundle` hook read every member of the eager -closure off disk with an unguarded `fs.readFileSync`. The closure walk seeds -itself from `chunk.imports`, and rolldown lists a chunk's EXTERNAL imports in -that array beside the file names of real chunks — so a bare specifier vite could -not resolve joins the closure under its own name and is then read as a path. The -result was a bare `ENOENT` from `node:fs`, several frames from the cause, naming -neither the plugin nor the unresolved import. - -The read is now guarded by an existence check that `this.error()`s with the -missing name, the chunk that imports it, and the diagnosis. The failure stays -loud — that direction is correct, and the two counter-probes above it exist to -keep it that way; only its message changes. diff --git a/.changeset/5998-record-path-one-classification.md b/.changeset/5998-record-path-one-classification.md deleted file mode 100644 index 0f17f158a1..0000000000 --- a/.changeset/5998-record-path-one-classification.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:path` now derives ONE stage classification and hands it to both of its rows, so a -stage can no longer paint and announce two different ways depending on viewport width -(objectui#5998). - -The renderer draws a desktop row (`hidden sm:flex`) and a mobile row (`flex sm:hidden`) from -the same `stages[]`, and each used to compute its own `terminal` from that array. -`renderStage` passes the same `terminal` to `railClass` and — since objectui#5957 — to -`stageAriaLabel`, so any disagreement surfaced in the colour and in the accessible name at -once. The rows disagreed on two axes: - -Mid-path goal. `WON_TOKENS` matches `完成`, an ordinary word rather than a Salesforce-style -`closed_won` value, so a path like `草稿 → 完成 → 已归档` classified index 1 as `won`. -Desktop declined it, because only the last forward stage can be the goal terminus; mobile -marked it `bg-emerald-500/30` and announced `goal stage, not reached`. - -The lost slice. Desktop renders `stages.slice(firstLostIdx)` as a visually separated alt -group and hardcoded `terminal: 'lost'` on every member of that positionally-defined group, -while mobile classified each stage on its own. A plain stage after a `lost` one -(`草稿 → 失败 → 已归档`) therefore painted destructive and announced `closed lost` on -desktop and plain on mobile; a `won`-classified stage in the same position drew `'lost'` -from one row and `'won'` from the other. - -Both rows now index a single `stageTerminals` array: `lost` is a property of the stage -itself, `won` is the goal terminus and so is the last forward stage or nothing, and the -positional grouping is a layout concern that no longer overrides what a stage is. Behaviour -is narrowed on both axes and never widened — no stage gains a `terminal` on either row that -it did not already carry there, and a goal terminus that really is last keeps its faint -emerald rail and its `goal stage, not reached` name on both rows. diff --git a/.changeset/6004-grid-column-emit-boundary.md b/.changeset/6004-grid-column-emit-boundary.md deleted file mode 100644 index c714459cc1..0000000000 --- a/.changeset/6004-grid-column-emit-boundary.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -fix(plugin-grid): type ObjectGrid's column emit against the `TableColumn[]` slot it fills - -`generateColumns()` had no return annotation and all four of its call sites cast -to `any`, so nothing checked what this producer wrote into -`DataTableSchema.columns: TableColumn[]`. - -Annotating it is not enough, and that is the substance of the change. Measured on -this program: `generateColumns(): TableColumn[]` raises **zero** diagnostics — the -emit literals reach the annotation through `.map()`, which strips the freshness -that excess-property checking depends on, so even an undeclared key written out -longhand is accepted. Underneath that sits the reason the annotation could not -bite at all: `objectSchema` is `useState`, and an `any` spread into an object -literal collapses the **entire** literal to `any`. - -So the fix has three parts: name the four inference locals so `any` stops at the -boundary, carry ADR-0049 `?: never` tombstones **derived** from -`keyof ListColumn` (never hand-listed, so a future spec key is refused by -default), and drop the `any` at every call site — including a fourth the card's -census missed and a fifth (`const generatedColumns: any[]`) inside the producer. - -Key verdicts: `headerIcon`, `pinned` and `wrap` are HELD and now declared at the -seam; `options` is RETIRED — nothing on either side of the seam read it, and -every value it carried still reaches its consumer through the field metadata the -cell closure captures and the object schema the inline editor reads. `type` stays -objectui#5853's and `name` is not emitted here at all. - -No rendering change. diff --git a/.changeset/6011-tosortitems-order-spelling.md b/.changeset/6011-tosortitems-order-spelling.md deleted file mode 100644 index 246ac6fe48..0000000000 --- a/.changeset/6011-tosortitems-order-spelling.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-view': minor ---- - -**Breaking (shipped as `minor` per AGENTS.md §版本号策略).** The published `toSortItems` -export now reads its sort direction from **`order`** only. The retired spelling is -**`direction`** — named here so that a host still writing it can find this entry by -searching the old key (objectui#6011). - -```diff - import { toSortItems } from '@object-ui/plugin-view'; - -- toSortItems([{ field: 'created_at', direction: 'desc' }]); -+ toSortItems([{ field: 'created_at', order: 'desc' }]); -``` - -**What changed, exactly.** `toSortItems` folded `s.order || s.direction || 'asc'`: two -spellings for one key, silently preferring the canonical one. It now folds -`s.order || 'asc'`. Everything else about the helper is unchanged — `id` is still -preserved when present and minted with `crypto.randomUUID()` otherwise, `field` still -defaults to `''`, and a non-array draft still yields `[]`. - -**The failure mode if you do not migrate is silent.** A draft entry spelled -`{ field: 'created_at', direction: 'desc' }` used to produce -`{ field: 'created_at', order: 'desc' }`; it now produces -`{ field: 'created_at', order: 'asc' }` — the documented default for an entry that names -no direction. Nothing throws and nothing warns: the `SortBuilder` row renders, and it -renders **ascending**. If you have a studio inspector draft, a persisted view body, or any -other producer that still writes `direction`, grep for the key and re-spell it to `order`. - -**Why the tolerant read went rather than staying.** objectui#4869 ruled that a spelling the -sink does not recognise gets ruled into the contract or rejected at the producer, never -absorbed by a tolerance layer. objectui#5293 retired the same word on -`ObjectViewProps.views[].sort` and shipped it as a `minor`; this entry finishes the job on -the sort family's public surface, so `order` is now the one spelling repo-wide and declared -equals enforced. The scope note in the objectui#5293 entry — that this export was *not* -retired by that change — described that release's scope correctly and is superseded here. - -`SortUI` is untouched. Its own file-local `toSortItems` is a different symbol, and -`direction` is the key `SortUISchema` legitimately declares. diff --git a/.changeset/6014-tree-grid-field-formatting.md b/.changeset/6014-tree-grid-field-formatting.md deleted file mode 100644 index 94d9c492ed..0000000000 --- a/.changeset/6014-tree-grid-field-formatting.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/plugin-tree': patch ---- - -`ObjectTree` formats its cells the way the flat table does: a lookup column renders the -referenced record's display name and a select column renders its translated option label, -instead of a raw record id and the raw stored value (objectui#6014). - -Reported against the built-in 业务单元 (`sys_business_unit`) page, whose 「组织架构」 tree tab -showed the manager column as a bare user id and the type column as `department`, while the -flat-table tab on the same page — over the same expanded records — showed the user's name -and 「部门」. - -The card carried its own control, and it pointed at the fetch rather than the formatter. The -tree treated "the host passed inline `data`" as "I do not need the object schema" and skipped -`getObjectSchema`, but its record-fetch branch prefers a live object dataSource over any -inline data. On the one mount shape `ListView` actually uses — `objectName` + a dataSource + -its own pre-fetched `data` — the tree therefore issued its OWN query with -`buildExpandFields(undefined)` → `[]` → no `$expand` at all, and had no field definitions to -format cells from. Both reported symptoms fall out of that single gap, which is why the flat -tab was unaffected and why the tree's existing tests (inline data, no dataSource — a path -that never runs the tree's own fetch) could not see it. - -Three changes, all inside `packages/plugin-tree`: - -- The object schema is fetched whenever the dataSource can serve one, not only when no host - passed inline data. The guard inside the fetch already no-ops without a dataSource, so the - pure inline/static path is unaffected. -- Records are no longer fetched until that schema has settled — settled, not necessarily - successful, so a rejected or inapplicable schema fetch can never block the tree. This also - removes a wasted first query whose lookup columns came back as bare ids and were painted - for a moment before the real query landed. -- Cell values route through a field-aware formatter that delegates both decisions rather than - re-deciding them: option labels through the `translateOptions` seam `ObjectGrid` already - uses for the flat tab (so both tabs read one `fieldOptions.*` i18n key, with the same - exact-then-case-insensitive match and `humanizeLabel` fallback as `SelectCellRenderer`), and - expanded references through `getRecordDisplayName`, the unified display-name resolver - (ADR-0079), with the family judged by `isExpandableFieldType` — the same predicate that - decided what to put in `$expand`. - -No new exports and no new package dependencies: both resolvers were already published from -`@object-ui/core`, and `translateOptions` was already reachable through the `useSafeFieldLabel` -hook this component calls for its column headers. - -One visible consequence beyond the report: an expanded record that comes back with no name-ish -field now reads as ADR-0079's `Record #` floor — the string every other surface shows for -it — rather than as the bare id. diff --git a/.changeset/6023-recent-item-type-keys.md b/.changeset/6023-recent-item-type-keys.md deleted file mode 100644 index f65580588d..0000000000 --- a/.changeset/6023-recent-item-type-keys.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -All ten locale packs now define `home.recentApps.itemType.report` and -`home.recentApps.itemType.metadata` (objectui#6023). `RecentItem['type']` is a six-member -union and `useTrackRouteAsRecent` writes both of these at runtime — `metadata` on any -`/metadata//` route visit, `report` on any `/report/` visit — but the -packs defined only four of the six, so the Recently Accessed and Starred cards labelled -those two items from the call sites' inline `defaultValue` instead of from the pack. - -Ten packs missing the same member is full parity, so no pack-vs-pack gate could see it, -and the prefix rule only ever asked whether `home.recentApps.itemType` resolved, which it -did. What made it quiet rather than loud is the `defaultValue`: English readers saw a -plausible `Report` / `Metadata` (lowercase in the rail) rather than a raw key, and the -other nine locales saw those English words — objectui#3517's mechanism for hiding a -missing key for months. - -The nine translations were taken from each pack's own existing rendering of the same word -rather than composed: the singular `Report` that `appDesigner.navReport`, -`appDesigner.navTypeReport` and `search.badgeReport` already carry (ar `تقرير`, de -`Bericht`, es `Informe`, fr `Rapport`, ja `レポート`, ko `보고서`, pt `Relatório`, ru -`Отчёт`, zh `报表`), and the `Metadata` that `layout.metadata.label` already carries (ar -`البيانات الوصفية`, de `Metadaten`, es `Metadatos`, fr `Métadonnées`, ja `メタデータ`, ko -`메타데이터`, pt `Metadados`, ru `Метаданные`, zh `元数据`). No new vocabulary was invented -for any locale. - -The two matching entries in `scripts/i18n-call-site-key-baseline.json`'s `missingMembers` -are cleared, since that list is a ratchet: a baselined entry whose defect is gone fails the -build too. The union itself is untouched — narrowing `RecentItem['type']` is the other -resolution the gate accepts, and it would have been a lie about data both call sites -demonstrably write. diff --git a/.changeset/6024-catalog-own-plugin-pin-generalized.md b/.changeset/6024-catalog-own-plugin-pin-generalized.md deleted file mode 100644 index 3d85725589..0000000000 --- a/.changeset/6024-catalog-own-plugin-pin-generalized.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Tests and test-config only, no shipped code touched: the catalog "entries use their own -plugin" pin in `examples/schema-catalog/test/catalog-gallery-render.test.tsx` is -generalized from the two hand-enumerated categories objectui#5113 and objectui#5856 left -it at (`plugin-view`, `plugin-grid`) to all thirteen `plugin-*` catalog categories. - -The `category -> type` map is now DERIVED from the `register()` calls, by reusing -`deriveRegistryKeys` from `scripts/check-doc-component-types.mjs` and joining its -registration sites on the `packages/` that owns them — the same derivation -`scripts/regenerate-known-schema-types.mjs` already consumes. The value is a SET, which -`plugin-charts` requires (its entries author `chart` and `bar-chart`), and the rule stays -per entry rather than per category. - -objectui#5113's RENDER half (a record that exists only in the gallery data source reaches -the screen) does not transfer to the eleven categories that author their data inline, so -it is replaced for them by a MOUNT half rather than degraded to the STRUCTURE half: -substituting every own-plugin node with an inert probe must put the probe's marker on -screen, which a stray node, a `type` that is not a node, and a node behind a satisfied -`hidden` cannot do. Data provenance is kept, unweakened, for the entries that bind to an -object the gallery fixture serves — derived per entry from `objectName`, not declared. The -gate states its own coverage split so breadth cannot be read as depth: 5 entries carry -structure + mount + data, 34 carry structure + mount, and `plugin-editor` / `plugin-map` -lose nothing to their `EXCLUSIONS` because the probe replaces the renderer that needs -Monaco and WebGL2. - -The derivation earned its keep on its first run: the filing's hand-written table read -`plugin-form form`, and `form` is registered by `packages/components`, not by -`@object-ui/plugin-form`. Both `plugin-form` entries are the third instance of -objectui#5113's class, ledgered against objectui#6167 with a case asserting they still -fail, so the ledger cannot rot green. - -`examples/schema-catalog/tsconfig.test.json` gains `allowJs` (with `checkJs` off) and a -repo-root `rootDir`, the same answer `tsconfig.scripts.json` reached in objectui#3494 for -importing a plain-JS CI helper into a typed pin test. diff --git a/.changeset/6025-declare-plugin-grid-in-gallery-host.md b/.changeset/6025-declare-plugin-grid-in-gallery-host.md deleted file mode 100644 index abfbccde32..0000000000 --- a/.changeset/6025-declare-plugin-grid-in-gallery-host.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Docs-site host and catalog test only: `apps/site/app/components/registerCatalogBlocks.ts` -now declares `@object-ui/plugin-grid` instead of inheriting `object-grid` from -`@object-ui/plugin-view`'s `import { ObjectGrid }` (`ObjectView.tsx:37`). Nothing renders -differently — measured: with exactly the eleven packages the host used to carry, -`object-grid` and every `@object-ui/plugin-form` key already resolved — which is the point. -A component import in another package was the only reason two catalog tiles could draw, and -a refactor of `object-view` that stopped it from drawing a grid itself would have turned -them into OBJUI-001 panels with the cause several files from the symptom. - -`catalog-gallery-render.test.tsx` gains the judge that can tell the two apart: every -`plugin-*` category with catalog entries must be loaded BY NAME in the host's import list, -derived from the categories rather than enumerated, and leaning on the existing parity case -that ties `HOST_PACKAGES` to the host file's literal imports. `ComponentRegistry.get()` -cannot judge this — it is truthy in both worlds. diff --git a/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md b/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md deleted file mode 100644 index 069e67899a..0000000000 --- a/.changeset/6027-sidebar-tailwind-v4-spelling-guard.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Tests only; no published behaviour changes. - -Adds an offline assertion that the shipped -`packages/components/src/ui/sidebar.tsx` contains no Tailwind **v3** -`[--var]` arbitrary values, wired into the existing -`scripts/__tests__/shadcn-local-patches.test.ts` suite (no new workflow). - -The Tailwind v4 custom-property migration (`925051db6`) is an *undeclared* -local edit — prose in `shadcn-components.json` (`localEdits`), with nothing in -`scripts/shadcn-local-patches.mjs` re-applying it — so a `--force` sync drops -it. It is the only one of `sheet`/`sidebar`'s four undeclared edits that no -gate catches: the v3 spelling compiles, renders, and emits invalid CSS that -browsers silently discard. The assertion closes that silence from the cheap -side rather than declaring ~20 class-string anchors. - -Asserted against the file the repo **ships**, not a vendored fixture — the -regression being guarded is "a forced sync overwrote the shipped file", which a -fixture-based check would pass straight through. The block also guards the -inverse (the correct v4 `(--var)` spellings must not trip it) and pins the -tolerated/refused counts, and its docblock records the other three undeclared -edits with the gate that catches each. diff --git a/.changeset/6038-production-predicate-fault-warning.md b/.changeset/6038-production-predicate-fault-warning.md deleted file mode 100644 index 8110de9e00..0000000000 --- a/.changeset/6038-production-predicate-fault-warning.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/react': patch -'@object-ui/components': patch ---- - -A node-gate visibility predicate that FAULTS now says so in a production build, once per -distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the -silence is no longer an accepted property"). Observability only — no verdict moves. - -`SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated -resolves to the same answer as one that said yes, so a gate that stops biting looks -exactly like a gate the author got right. The diagnostic that names it (objectui#5454 / -objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection -channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition` -implements that by evaluating **twice** — too expensive to ship for every predicate of -every node. - -**What production actually printed before, measured per dialect on the built evaluator** -— the card's premise held for one dialect of three, and the other two failed in opposite -directions: - -| dialect | production console, before | -|---|---| -| bare string | **nothing** | -| `{ dialect: 'cel' }` envelope | one generic line, deduped per source | -| `${…}` template | one generic line **per evaluation**, never deduped | - -So the dialect objectstack#11254 measured a live gate breaking on was the silent one, -while the template dialect was the console flood the ruling's rate-limit clause exists to -prevent. - -**The fix reports the fault the evaluator already detected, at the same number of engine -calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s -`ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the -canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer -up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason -passback), and supplying it transfers reporting to the caller so one fault stays one -line. Pinned: the CEL branch performs the same number of record reads with the passback -as without it, and strictly fewer than the `throwOnError` probe. - -`SchemaRenderer` passes it in production and reports through the **same** reporter the dev -branch uses — same message, same severity, same dedupe `Set`, same key. Development and -production now print the identical line for the identical fault; the `__DEV__` gate no -longer decides *whether* a fault is reported, only *how* it is detected. - -`page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same -reporter and the same rate limit. It swallowed the identical fault under a different -helper, and it was the worse of the two: the node gate at least reported in development, -while a faulting item predicate was silent in *both* builds on a gate whose false verdict -removes an entire tab, header and panel. - -**Rate limit:** deduped per (node type, gate key, predicate source) — never per render and -never per node instance. A two-hundred-row list of one broken predicate is one line; a -second distinct predicate source still gets its own line. Both halves are pinned, because -a test that asserts only "a warning was emitted" is equally green on an implementation -that emitted fifty, and one that asserts only "exactly one" is equally green on an -implementation that suppresses everything. - -**Not changed by this card, deliberately:** the fail-open semantics themselves; the -objectui#5687 adapter-only `data.*` report, which stays development-only under its own -2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against -the wrong object); and the `/forms/:name` scope wiring of objectui#6262, which lands in -its own PR. - -`reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`, -`UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported -from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one -reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one -authored predicate would be entitled to one line per package instead of one line. diff --git a/.changeset/6041-designer-reference-key.md b/.changeset/6041-designer-reference-key.md deleted file mode 100644 index 803ca1328c..0000000000 --- a/.changeset/6041-designer-reference-key.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/plugin-designer': patch -'@object-ui/app-shell': patch ---- - -The field designer now reads and writes a lookup field's relationship target under the -spec's spelling `reference` (objectui#6041), in both directions. - -`referenceTo` is not in `FieldSchema`'s accept set. Measured against the installed -`@objectstack/spec` 17.2.0, through the whole object document that -`PUT /api/v1/meta/object/:name` validates: - -``` -ObjectSchema.safeParse({ …, fields: { rel: { type: 'lookup', label: 'Owner', - referenceTo: 'user' } } }) - => success = false - => unrecognized_keys at ["fields","rel"] keys=["referenceTo"] - "Did you mean `referenceTo` -> `reference`?" -``` - -so authoring a lookup field through the designer returned a hard 422 `INVALID_METADATA`, -and — because the key is then stored — blocked **every subsequent save** of that object, -with nothing in the UI to say which key did it. - -The read direction was broken symmetrically and is the half that would have survived a -write-only fix: `toDesignerField` read `raw.referenceTo` while a spec-parsed server sends -`reference`, so every already-saved lookup field loaded into the designer with an **empty -reference box**. Both wire-bound payload shapes move — `FieldMetadataPayload` -(`MetadataService.toFieldPayload`) and `ServerFieldSchema` -(`MetadataFieldsPage.fromDesignerField`). - -`referenceTo` also joins `RETIRED_FIELD_KEYS`. Renaming the emit sites alone does not -unblock an object whose stored fields already carry the misspelling: `carryOver` spreads -the previous server def verbatim, so the key would ride straight back out to the same 422. -The designer's in-memory `DesignerFieldDefinition` keeps `referenceTo` — that is the -internal prop name every other UI surface in this repo already uses (`LookupField`, -`filter-builder`, `ObjectChart`, `ListView`, `UserFilters`), it reaches no wire-bound -shape, and the parity gate classifies it as `uiOnly` rather than a violation. - -No behavioural change for a half-filled draft: the spec's prose calls `reference` -"required for relationship types", but that is not enforced by the zod parse at 17.2.0 — -`{ type: 'lookup', label: 'L' }` parses green at field level and through `ObjectSchema`, -and `undefined` is dropped by `JSON.stringify` under either spelling, so the wire bytes -are identical before and after. diff --git a/.changeset/6043-retire-designer-formula-control.md b/.changeset/6043-retire-designer-formula-control.md deleted file mode 100644 index 00f6f3a85e..0000000000 --- a/.changeset/6043-retire-designer-formula-control.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-designer': minor -'@object-ui/app-shell': minor ---- - -The Field Designer no longer offers a formula-expression textarea, and no designer write -path emits a `formula` key (objectui#6043). - -**This is a behaviour change on an authoring surface: a control is removed.** A field's -`type` may still be set to `formula` — that is a valid spec `FieldType` and stays in the -palette — but the expression itself is no longer authored here. Authors write formula -expressions in metadata-admin's field inspector, where they are checked. - -The control wrote `formula`, which is not in `FieldSchema`'s accept set. Measured against -the installed `@objectstack/spec` 17.2.0: - -``` -FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) - => success = false - => unrecognized_keys ['formula'] "Did you mean `formula` -> `expression`?" -``` - -so `PUT /api/v1/meta/object/:name` returned a hard 422 `INVALID_METADATA` — and because -the key was then stored, it blocked **every later save of that object**, not just the one -that introduced it. - -**The key was deliberately NOT renamed to the spec's `expression`.** `FieldSchema` judges -the key name and never the expression LANGUAGE — measured, it accepts -`expression: 'price * quantity'` and even `expression: '!!!not cel at all!!!'`; only the -empty string is refused. Spec `expression` is CEL rooted at `record` -(`record.amount * 0.1`), whereas this control's own placeholder taught `price * quantity` -— bare field refs, which under the scope formulas bind evaluate to null silently. A rename -would therefore have converted a loud, immediate 422 into a formula that saves clean and -then quietly computes nothing, which is strictly worse than the bug it appears to fix. - -Making refusals loud *in the control* would need CEL lint, autocomplete and `returnType` -inference — that is `CelPredicateField`, which lives in `@object-ui/app-shell`, and -app-shell depends on `@object-ui/plugin-designer`, so it cannot be imported back without a -dependency cycle. Growing a second formula-authoring surface inside plugin-designer is a -feature, not this fix. `returnType` is likewise not authored here: it is only derivable by -inferring the CEL result type, and with no expression control there is nothing to infer -from. - -`formula` joins the retired-key tombstone in `MetadataFieldsPage`, so an object already -carrying the key is stripped clean on its next save instead of staying blocked forever — -which matters more than usual here, because with the control gone an author would -otherwise have no way left to clear it. It is dropped rather than migrated to `expression`, -for the same reason the rename was refused. A `expression` authored in metadata-admin is -**not** touched: it is a real `FieldSchema` key and rides through the designer's -round-trip untouched. - -Also removes the now-unreachable `formula` read/write from -`views/metadata-admin/previews/object-fields-bridge.ts`, which was a third emit site for -the key that neither the card nor the parity gate named. - -The `formula` entry is removed from `check-designer-field-key-parity.mjs`'s -`KNOWN_UNPARSEABLE_KEYS` ledger, which ratchets in both directions — a resolved key that -left a stale entry behind would be as red as a new offender. diff --git a/.changeset/6044-designer-system-key.md b/.changeset/6044-designer-system-key.md deleted file mode 100644 index 09a3c92462..0000000000 --- a/.changeset/6044-designer-system-key.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-designer': patch ---- - -The field designer now reads the system-field marker under the spec's spelling `system`, -and never hands `isSystem` back to the metadata API (objectui#6044). - -`isSystem` is not in `FieldSchema`'s accept set. Measured against the installed -`@objectstack/spec` 17.2.0: - -``` -FieldSchema.safeParse({ type: 'text', label: 'L', isSystem: true }) - => success = false - => unrecognized_keys keys=["isSystem"] "Did you mean `isSystem` -> `system`?" -``` - -Two defects, one misspelling, and they are two different sites. - -**The read was dead** — the quieter and worse half. `toDesignerField` read `raw.isSystem` -while a spec-parsed server sends `system`, so the flag was always `undefined`. Nothing went -red, because the flag is optional and `undefined` is a valid "not a system field". But it is -load-bearing: `FieldDesigner` refuses to delete a system field and disables its name and -type inputs, so with the read dead `organization_id`, `created_at` and friends presented as -ordinary editable, **deletable** business fields. - -**The write had no emit site at all.** `fromDesignerField` never names `isSystem`; its only -route out is the verbatim `...carryOver(prev)` spread, so a stored misspelling round-tripped -back to `PUT /api/v1/meta/object/:name` as a hard 422 `INVALID_METADATA` that blocks every -later save. The repair is a `RETIRED_FIELD_KEYS` tombstone rather than a renamed line — and -it is deliberately paired with the read fix, never a substitute for it: stripping alone would -close the 422 and fossilize the dead detection. The spec spelling `system` is not stripped, -so a server-injected flag rides through untouched and feeds the read. - -`app-shell`'s `FieldMetadataPayload` never declared the key, so `toFieldPayload` had nothing -to fix. The designer's in-memory `DesignerFieldDefinition` keeps `isSystem`: it reaches no -wire-bound shape and the parity gate classifies it as `uiOnly`. diff --git a/.changeset/6045-field-payload-sort-order.md b/.changeset/6045-field-payload-sort-order.md deleted file mode 100644 index c18dc4bd9b..0000000000 --- a/.changeset/6045-field-payload-sort-order.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/types': minor ---- - -The field metadata payload no longer emits `sortOrder`, the key `FieldSchema` refuses by -name (objectui#6045). Field-level sibling of objectui#6223, same objectui#5761 family. - -Measured against the installed `@objectstack/spec` 17.2.0, whose `FieldSchema` accept set -is 71 keys: - -``` -FieldSchema.safeParse({ type:'text', label:'L' }) => success = true (control) -FieldSchema.safeParse({ type:'text', label:'L', sortOrder: 3 }) => unrecognized_keys ["sortOrder"] - -FieldSchema.safeParse({ type:'text', label:'L', sortable: true }) => success = true (control) -FieldSchema.safeParse({ type:'text', label:'L', sortable: 3 }) => success = false -``` - -The control is what makes that a key-by-key result rather than a schema refusing -everything, and the `sortable` pair is what shows the near-spelling is a *different -concept* — a boolean ("whether field is sortable in list views"), not this key's spec -name. - -**The resolution was deletion, not a rename**, which is objectui#4687's shape rather than -objectui#6041's. The spec has no field-level ordering key at all: it models field order by -**declaration order** in the object's `fields` record, so a designer that wants explicit -ordering reorders that record rather than carrying an index. There was nothing to map onto, -and nothing was invented to map onto. - -**It was latent, and that is confirmed on today's tree.** Neither of the two sites that -construct a `DesignerFieldDefinition` — `FieldDesigner`'s create/update handlers and -`MetadataFieldsPage.toDesignerField` — ever named the key, so `toFieldPayload` emitted -`sortOrder: undefined` and `JSON.stringify` dropped it. The key never reached the wire. It -was one reorder feature away from doing so, which is the objectui#4644 shape: a hard 422 -`INVALID_METADATA` that blocks every subsequent save of the object, with nothing in the UI -to say which key caused it. - -Removed in one go from the wire shape (`FieldMetadataPayload`), its writer -(`toFieldPayload`) and the UI model (`DesignerFieldDefinition`), so no declaration is left -behind that no writer fills and no schema accepts. - -**Breaking for TypeScript consumers**: `sortOrder` is gone from `DesignerFieldDefinition` -(`@object-ui/types`) and from `FieldMetadataPayload` (app-shell), so code that set either -stops compiling. - -Two keys share this spelling and are untouched, which is why the census was on the *shape* -— a field-metadata payload key `FieldSchema` refuses — rather than on the identifier: the -**object-level** `sortOrder` (`ObjectSchema`'s, removed from the object wire shape by -objectui#6223 and deliberately kept on the `ObjectDefinition` UI model) and the -**saved-view** `sortOrder` in `ObjectView`, which is per-view display order on a different -document entirely. - -The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes -with the fix — that ledger ratchets in both directions, so an entry left behind for a -resolved key is as red as a missing one. diff --git a/.changeset/6050-gantt-navigation-basepath.md b/.changeset/6050-gantt-navigation-basepath.md deleted file mode 100644 index e7b6c13c0a..0000000000 --- a/.changeset/6050-gantt-navigation-basepath.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -The package README stops documenting a `navigation` key the spec refuses, and the corrected example is now parsed by the schema that validates it. - -`README.md`'s record-navigation override read -`{ mode: 'page', basePath: '/console/apps/.../campaign' }`. `basePath` is not a -member of the spec's `NavigationConfig`, and nothing consumes it: -`useNavigationOverlay` — where a gantt's `navigation` lands — builds no URL out -of the config, and `ObjectGantt` calls the hook with no `onNavigate`, so a -page-mode click falls through to the host's `onRowClick`. The destination route -is owned by the host and was never authorable through this key, under any -spelling. - -That made the snippet worse than inert. `NavigationConfigSchema` is a strict -object with no passthrough, so the undeclared key did not fall away quietly — it -rejected the **whole** config with `unrecognized_keys`, taking down the -`mode: 'page'` the sentence was actually teaching. An author who copied the -documented snippet got a rejected navigation config and no page navigation, which -is the copy-the-snippet-get-rejected shape objectui#5057 / #5012 named on other -keys. - -The example is corrected to `{ "navigation": { "mode": "page" } }` — the shape -the sentence demonstrates — and the prose now says who owns the destination route -and points at `@objectstack/spec`'s `NavigationConfigSchema` for the member list -instead of restating it, matching the derivation `ObjectGanttSchema.navigation`'s -doc comment (objectui#5903) adopted for the same concept. - -`view` is **not** substituted for `basePath`. It is a declared member, but it -names a form view (the spec: *"Name of the form view to use for details"*) and is -forwarded to `onNavigate` as the action argument — it is not a route, so putting -it where `basePath` stood would have replaced an invented key with a wrong one. -It is documented for what it does. - -No gate in this repo could have caught the original defect, and that is why the -fix ships with a measurement rather than a re-reading: `check-doc-snippet-types` -compiles `ts`/`tsx` fences and `check-doc-component-types` reads `type` literals, -and both are structurally blind to a metadata key in a README — the former's own -header records schema-key validity as "a different question … left unruled on -purpose". `src/readme-navigation-example.test.ts` closes that hole for this -example by EXTRACTING the fence from the README on every run and parsing it -against `NavigationConfigSchema`, with a control asserting the same parse still -rejects an undeclared key by name, so the green cannot come from a schema that -accepts everything. - -`tsconfig.test.json` names `node` in `types` for that test to compile, and its -comment — which had recorded that no test in this package touches a Node global -— is corrected rather than left standing. diff --git a/.changeset/6051-gantt-flat-config-declared-keys.md b/.changeset/6051-gantt-flat-config-declared-keys.md deleted file mode 100644 index 7aa10e1dc3..0000000000 --- a/.changeset/6051-gantt-flat-config-declared-keys.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -`ObjectGanttSchema` declares the flattened `GanttConfig` face `ObjectGantt` -actually reads (objectui#6051). `getGanttConfig` has two branches: when -`startDateField` and `endDateField` are both present at the TOP level it builds -its config from top-level keys and returns early; otherwise it reads the `gantt` -block. Everything the first branch reads was undeclared — and unlike -objectui#5903's ten, none of it was hidden behind a cast. `BaseSchema` carries -`[key: string]: any` (objectui#5155's structural ceiling) and the helper's -parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as -`any` with no syntax anywhere to grep for. That is also why the census here is an -AST enumeration and not a compile-and-observe: an index signature absorbs every -literal name, so annotating the parameter compiles clean while enforcing nothing. - -**27 keys join the declared surface, each additive and each with a live read -site.** 24 flattened `GanttConfig` members — `colorField`, `borderColorField`, -`dependenciesField`, `parentField`, `typeField`, `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `tooltipFields`, `baselineStartField`, -`baselineEndField`, `groupByField`, `resourceView`, `assigneeField`, -`effortField`, `capacity`, `quickFilters`, `autoZoomToFilter`, `timeSegments`, -`interactions`, `exportFileName`, `timeZone`, `dependencyTypes` — plus the three -query keys the fetch path reads, `staticData`, `filter` and `sort`. Nothing is -declared that the renderer does not consume. - -**`GanttConfig` itself gains nine members and is a published type**, exported by -name from `packages/types/src/index.ts`: `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, -`timeZone`, `exportFileName`, `interactions`. The entry file's diff is empty only -because the export list already named the type — the widening happened at the -declaration. - -**The 28th measured key, `gantt` (the block face), is deliberately NOT declared** -— see the closing section. - -The 24 are DERIVED from `GanttConfig` rather than restated, so the flat spelling -cannot fork from the block spelling, and the invariant is pinned in the type -system: every key of `GanttConfig` must be declared at the node's top level. -Making that derivation possible moved nine members — `lockField`, `objectField`, -`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, -`timeZone`, `exportFileName`, `interactions` — out of `plugin-gantt`'s -package-private `GanttConfigEx` and into `@object-ui/types`' `GanttConfig`. They -are a MOVE, not new vocabulary: the `gantt` block already honoured all nine, and -a type private to the plugin could be referenced by neither authoring face. - -Both halves move together, as in objectui#5903: the TS declaration and its zod -mirror gain the same 27 keys at the same requiredness (all optional), the -spec-modelled ones taken from `GanttConfigSchema.shape` by reference, so the -`zod-mirror-parity` ratchet stays at zero drift for this pair and no `KnownDrift` -or `UnmirroredDeclared` entry is added. The mirror builds the flat face and the -`gantt` block from one field map, so they are one schema expressed twice. - -Accept-set change, stated plainly. All 27 keys are additive — every one is -optional, and nothing previously legal loses its slot. What changes is that a -**declared** key is now type-validated, so `capacity: 'one'` and -`summaryExtent: 'parent'` are refused where they used to parse green. An -**undeclared** key is still accepted — `BaseSchema` is `.passthrough()`, so this -bought no rejection of misspellings. There is no narrowing anywhere in this -change. - -**`gantt` is severed on purpose (objectui#6475), not overlooked.** It is the 28th -key of the measured residue and a genuine read — `getGanttConfig`'s second branch -honours it in full — but it is the one key whose declaration would NOT have been -additive. It has no mirror entry today, so a block rides through `.passthrough()` -unvalidated; declaring it as `GanttConfig` means it gets parsed against the spec's -`GanttConfigSchema`, which REQUIRES `startDateField`, `endDateField` and -`titleField`, and `ObjectGanttSchema` reaches the CLI's `validate` / `check` -through `AnyComponentSchema`. A published CLI's refusal behaviour is decided on -its own card, where reviewers can see what they are approving; objectui#6475 -carries the full measurement, including the case FOR enforcing it (the renderer -already feeds that block to `GanttConfigSchema.safeParse` and warns, so enforcing -restores declared = enforced rather than inventing a contract). Today's behaviour -is pinned in the test file so the omission is a measured state, not a silent gap. -`packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts` pins both -halves so neither can be misread. - -Which face WINS is unchanged and was not decided here: the flat branch is checked -first and returns early, so a node carrying both spellings still renders the flat -one. (`plugin-map` had the opposite precedence ruled on in objectui#5018; no -equivalent ruling exists for gantt.) diff --git a/.changeset/6052-gantt-export-filename-i18nlabel.md b/.changeset/6052-gantt-export-filename-i18nlabel.md deleted file mode 100644 index 5fffb4af6a..0000000000 --- a/.changeset/6052-gantt-export-filename-i18nlabel.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/plugin-gantt': patch ---- - -`ObjectGantt`'s export filename resolves a locale-map `label` instead of stringifying it -(objectui#6052). A gantt authored as -`{ "type": "object-gantt", "objectName": "task", "label": { "en": "Shift Plan", "zh-CN": "排班计划" } }` -exported its PNG/PDF as `[object Object]-20260825-1030.png`. - -`BaseSchema.label` is `string | I18nLabel` since #4580's revised Q1-A ruling — `I18nLabel` -being the spec's INLINE locale MAP — and the `exportFileName` chain handed that value -straight to `String(...)`. It now goes through `resolveI18nLabel` from `@objectstack/spec/ui`, -the producer's own resolver for that vocabulary, against the display locale the file already -reads via `useDisplayLocale()`. A zh-CN audience gets `排班计划-.png`, an en audience -`Shift Plan-.png`, and a plain-string label is unchanged. - -The next link in the same chain, `objectSchema?.label`, is deliberately left alone: that is -the DATA object's label, declared `z.string().optional()` on the spec's `ObjectSchemaBase`, -which is a `strictObject` — a locale map there is rejected by the producer rather than -resolved by the consumer, and wrapping it would be accepting a second vocabulary at a read -site. No filename sanitisation is added either; `GanttView` already strips -filesystem-hostile characters downstream, and a resolved map entry goes through the same -strip a plain string does. diff --git a/.changeset/6058-zod-mirror-unmirrored-ledger.md b/.changeset/6058-zod-mirror-unmirrored-ledger.md deleted file mode 100644 index ff4311f11e..0000000000 --- a/.changeset/6058-zod-mirror-unmirrored-ledger.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only: `packages/types`' zod-mirror-parity guard now compares the UNION of the -mirror's keys and the declaration's, and ledgers the newly visible half separately -(objectui#6058; the stale prose counts objectui#6141 measured are corrected in the same -file). No published type, mirror or runtime behaviour changes — nothing in `src/` outside -`__tests__/` is touched, so there is nothing to release. diff --git a/.changeset/6063-kanban-resolvedisplay-dead-guard.md b/.changeset/6063-kanban-resolvedisplay-dead-guard.md deleted file mode 100644 index 56fc560050..0000000000 --- a/.changeset/6063-kanban-resolvedisplay-dead-guard.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -`ObjectKanban`'s `resolveDisplay` drops an unreachable relation-type guard, and the rule -that survives it is pinned (objectui#6063). - -The helper that builds card descriptions ended: - -```ts -const isLookup = isExpandableFieldType(def); -if (isLookup && isOpaqueId(raw)) return undefined; -if (isOpaqueId(raw)) return undefined; -return raw; -``` - -The second line subsumes the first for every input — same `raw` (a `const`, unmodified -between the two lines), same predicate, and `OPAQUE_ID_RE` carries no `g`/`y` flag, so -repeated `.test()` on it is stateless. `isLookup` was computed, branched on and discarded. - -**No behaviour changes on any board.** The card named a second reading — that the -unconditional line was the mistake and only relation columns were meant to suppress -id-shaped values — and it was rejected on evidence rather than by tidying first: the -helper's own docblock declares both clauses, the same predicate is already applied with no -type gate to the incoming `description` a few lines down, and `objectDef` is optional at -that read, so a type gate would suppress nothing on exactly the boards whose object schema -is thin or absent. That reading is now a red test, not a comment. - -Deleting the branch also deleted this path's read of `@object-ui/core`'s -`EXPANDABLE_FIELD_TYPES`, so objectui#5874's identity pin for this face is re-anchored onto -the read that is live — `buildExpandFields`, on every fetch — where the membership delta is -observable on the wire (`$expand`) as well. That pairs the identity pin with the -behavioural counter-probe #5874 had to record as missing. diff --git a/.changeset/6067-component-meta-derive-from-canonical.md b/.changeset/6067-component-meta-derive-from-canonical.md deleted file mode 100644 index d524f38c01..0000000000 --- a/.changeset/6067-component-meta-derive-from-canonical.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`ComponentMeta` at the registry is now DERIVED from the one declaration in -`@object-ui/types` instead of restating it, and `tags` / `description` reach the -registration surface (objectui#6067). - -## The convergence - -`packages/core/src/registry/Registry.ts` declared its own `ComponentMeta`: thirteen -keys, of which nine were restated from `@object-ui/types`' `base.ts`, four were -registry-only (`tier`, `namespace`, `skipFallback`, `labelling`), and `tags` / -`description` were **absent** — although both are declared on the canonical type and on -the `ComponentMetaSchema` zod mirror. Two of the three authorities agreed and the -registration surface did not, so those two keys were unwritable at exactly the -declaration most component registrations import. That is the same two-key delta -objectui#5893 had just closed inside `@object-ui/types`, arriving a third time on a -third declaration, and objectui#5671 had already made the identical move for the sibling -type `ComponentInput` in this very file. - -It is now: - -```ts -export type RegistryComponentMetaExtras = { - tier?: 'public' | 'internal'; - namespace?: string; - skipFallback?: boolean; - labelling?: 'control' | 'group' | 'display'; -}; - -export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras; -``` - -`RegistryComponentMetaExtras` is newly exported from `@object-ui/core`. - -**What changes for a consumer: `tags` and `description` become writable on the registry's -`ComponentMeta`. Nothing narrows.** No key is removed, no key is renamed, and no key's -type changes, so no existing registration stops compiling — verified by type-checking all -37 workspace consumers of `@object-ui/core` (`pnpm --filter '...@object-ui/core'`), which -is why this is a widening rather than the contract break a rename would have been. All -four registry-only keys have live consumers, and they are still declared here. - -This is `minor` under this repository's policy that its own breaking changes never declare -`major` (`scripts/check-changeset-no-major.mjs`); nothing here is breaking in any case. - -## Converge rather than rename, and why the four keys did not move - -The alternative dispositions were to rename the type so the name stops claiming a mirror, -or to move the four registry keys onto `@object-ui/types`' `ComponentMeta` and re-export -it outright the way objectui#5671 handled `ComponentInput`. - -Renaming was rejected because it cannot be done without a break: `@object-ui/core` is -published, `ComponentMeta` is exported from it, and dropping the name would break every -external consumer — while keeping it as an alias would leave the mirror claim standing -under a second spelling, which fixes nothing. - -Moving the four keys was rejected because `skipFallback` and `namespace` are registration -mechanics — they describe how the registry keys an entry, not what a component is — and -`@object-ui/types`' `ComponentMeta` is the general, plugin-facing, AI-facing type. The -extension keeps them where they are read, under their own named type, while the eleven -shared members exist in exactly one place and can no longer drift. - -## Pinned by key set, not by assignability - -Every member of both shapes is optional, so `extends` is mutually **true** across the -diverged pair — an assignability assertion is green on the defect and would not have -caught it. Measured on the emitted `.d.ts` of both packages, before and after: - -| reading | before | after | -|---|---|---| -| `Core extends Canonical` | `true` | `true` | -| `Canonical extends Core` | `true` | `true` | -| `Exclude` | `"tags" \| "description"` | `never` | -| `Exclude` | the four registry keys | the four registry keys | - -The new pin asserts the third row and names the fourth explicitly; the assignability pair -is kept beside it, labelled, as the control that shows what it cannot see. A source-level -assertion that the canonical members are not restated locally covers the remaining failure -mode — a member-identical copy, which every `keyof` comparison stays green on and which is -how the copy this replaces began. diff --git a/.changeset/6074-check-ts-stale-migration-comment.md b/.changeset/6074-check-ts-stale-migration-comment.md deleted file mode 100644 index 9789b66adb..0000000000 --- a/.changeset/6074-check-ts-stale-migration-comment.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Comment-only fix in `packages/cli/src/commands/check.ts`: the note beside the -structural marker gate promised that "the corpus migration repays" the recall -debt — the real schema files the checker skips because their root carries no -structural key. No such migration exists and none can be written. The marker it -would stamp is the `$schema` URL the 2026-08-20 ruling declined to mint, and -`OBJECTUI_SCHEMA_URL` / `pointsAtObjectUi()` were deleted before objectui#5334 -merged. The same file already states, twenty lines earlier, that there is -deliberately no `$schema` arm and no URL to declare — so the file contradicted -itself. - -The note now says what is true: the recall debt is real and unpaid, the -`$schema` route was ruled against, a corpus-wide `$schema` sweep is a measured -no-op on the skipped-file count, and the arm can be added later without -invalidating a single file because matching would be host-based. It points the -next reader at the tracking card and its blocker instead of at a dead route. - -No behaviour change and nothing to release — `objectui check` judges, skips and -prints exactly what it did before. diff --git a/.changeset/6075-check-validity-recogniser-recall.md b/.changeset/6075-check-validity-recogniser-recall.md deleted file mode 100644 index 7aeccd03d2..0000000000 --- a/.changeset/6075-check-validity-recogniser-recall.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/cli': minor ---- - -`objectui check` recognises a schema by validating it, and reports broken ObjectUI files instead of filing them as foreign ones. - -A file with a root `type` was judged only when its root carried an ObjectUI -structural key (`children`, `body`, `className`, …). Leaf schemas carry only -their own vocabulary, so nothing checked them: measured on this repository, 475 -files were eligible, 166 were judged and 309 were skipped. - -The command now has a second recogniser arm — the document validates as an -ObjectUI component schema under `@object-ui/types`' own Zod union — which the -maintainer's 2026-08-25 ruling selected over shipping a JSON Schema artifact to -point a `$schema` URL at. It admits 209 of those 309 files. The structural arm -still runs first, so recognition costs nothing for files that already had a -marker, and `package.json` is still never judged: `"type": "module"` names no -component the protocol models. - -Validity alone would have answered two different questions with one word. -A broken ObjectUI schema fails validation exactly as a foreign file does, so a -two-bucket report would have filed it as "not ObjectUI" — and the symptom of -that is an absence: the file simply stops being mentioned. Measured, that bucket -is not empty: 54 files land in it and 53 of them are real corpus content. - -So files the recogniser refuses are split. When the root `type` names a -component this build registers, the file is **listed by name** as ObjectUI -content that did not validate, pointing at `objectui validate ` for the -reason — either the document is off-spec or its component type is not modelled -by `@object-ui/types`. Everything else is counted as skipped, as before. The -printed explanation now describes both arms, and only unreadable JSON still -makes the command exit non-zero. diff --git a/.changeset/6081-release-lane-drop-changeset-prediction.md b/.changeset/6081-release-lane-drop-changeset-prediction.md deleted file mode 100644 index 3d99006fab..0000000000 --- a/.changeset/6081-release-lane-drop-changeset-prediction.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI-only change: the `lane` job in `.github/workflows/changeset-release.yml` no longer -predicts whether `changesets/action` will see a pending changeset. Its bash mirror of -the action's file scan and the `pending_changesets` output it fed are deleted, and the -release job's clear step now runs on every push run — `if: github.event_name == 'push'`. - -This supersedes the mirror that `5775-lane-changeset-reader-mirror.md` describes, and -with it `scripts/__tests__/changeset-release-lane-mirror.test.ts`. The mirror could not -be kept honest: `changesets/action` is used at a moving tag whose bundled -`@changesets/read` is a different major from the installed one, and it is a minified -chunk nothing here installs, so no test could execute it and no lockfile recorded it. -The `changesets/action@v1` ref pin that lived in that test file is re-homed, not -dropped — it now has its own file, -`scripts/__tests__/changeset-release-action-ref-pin.test.ts`, and covers both `uses:` -lines instead of one. - -No published package changes and no release behaviour changes: the clear step is -idempotent and a measured no-op on a tree with nothing pending (`Removed 0`, exit 0, -byte-identical tree), so running it unconditionally on the push lane does what the -prediction was there to arrange. The `github.event_name == 'push'` test is retained -deliberately — dropping it too would empty the tree on the refresh lane and fossilise -the version PR silently. diff --git a/.changeset/6089-hook-selftests-step-names-drop-counts.md b/.changeset/6089-hook-selftests-step-names-drop-counts.md deleted file mode 100644 index d519e4ab50..0000000000 --- a/.changeset/6089-hook-selftests-step-names-drop-counts.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -CI-only change: no published package's `src/` changed. - -Drops the hard-coded case counts from the two step names in -`.github/workflows/hook-selftests.yml` (`guard-main-checkout-bash self-test -(100 cases)` / `guard-shared-stash self-test (32 cases)` → the bare names). -Nothing derived those numbers and nothing re-checked them, and a step name has -no runtime behaviour, so a stale count could only ever show a wrong number on -the checks page. Each run's own tail already prints `N passed, N failed`, and -`guard-shared-stash.sh`'s header carries its count with the recipe to -re-derive it. The `Cost` note keeps its counts, restated as a dated -measurement (`2026-08-24` @ `53dc89db8`) — history, not a live claim — and a -new header section records why the two are treated differently. What the -workflow runs is unchanged: same job, same steps, same `run:` commands. diff --git a/.changeset/6092-entry-guard-sweep.md b/.changeset/6092-entry-guard-sweep.md deleted file mode 100644 index e63819ad6d..0000000000 --- a/.changeset/6092-entry-guard-sweep.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Tooling only, no package released: every `scripts/**` entry guard now goes through one predicate. The 29 hand-typed guards `check-entry-guard.mjs` baselined (nine distinct spellings across 28 `.mjs` files, plus `shadcn-sync.js`) are converted to `isEntrypoint(import.meta.url)` and `KNOWN_HAND_TYPED_GUARDS` is empty. Twenty-eight of them were silently wrong: reached through a symlink they compared two different paths, answered `false`, and did nothing — exit 0 with no output, which a CI wrapper holding `result.status` reads as a pass (objectui#6092). diff --git a/.changeset/6108-sort-axis-convergence.md b/.changeset/6108-sort-axis-convergence.md deleted file mode 100644 index b031228521..0000000000 --- a/.changeset/6108-sort-axis-convergence.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/plugin-detail': patch ---- - -The last three sort-axis consumers read the platform's per-column sortability signal instead -of re-deriving it from the field's type (objectui#6108, inheriting objectstack#10235 ruling A -through objectui#5729's landed contract). ListView's toolbar sort picker and both of -RelatedList's sort entry points — the embedded table's column headers and the `data-list` -sort-button row — now go through `isPlatformSortableField`, the same spelling the grid header -adopted; their `UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType` re-derivations are -deleted. - -The re-derivation was not wrong about `formula`: the platform computes its own projection from -the same `@objectstack/spec` storage fact, which is why the drift went unnoticed across two -cards. It parts company on everything the projection encodes as ABSENCE — an unknown name, a -dotted path a caller can put in a related list's `columns`, an unprovisioned audit column — -where a type read finds no field definition, answers "sortable", and offers a control the -runtime meets with `400 INVALID_SORT`. It parts company again on any refusal that carries no -`reason: virtual-type`, and it cannot follow the platform in the other direction either: a -field the platform now DOES order by stays withheld forever on its type alone. - -Two behaviours are deliberately unchanged. The relational carve-out stays separate from the -signal — the projection answers `sortable: true` for a `lookup` because the platform can order -by the stored foreign key, while the UI withholds because that order means nothing beside a -column of names — so a relational column does not get its sort back. And ListView's picker -still lists a field the CURRENT sort already names, which is the only way to remove a sort the -server refuses outright; that exception now covers platform-refused fields, not just formulas. - -A deployment that served no `sortability` key at all is a different case from "nothing is -sortable": that branch keeps the type read as a compatibility floor, so behaviour on a backend -older than objectstack#10235 (or an inline/mock data source) is byte-identical to before. diff --git a/.changeset/6110-predicate-scope-unbound-evaluators.md b/.changeset/6110-predicate-scope-unbound-evaluators.md deleted file mode 100644 index 4be26dce28..0000000000 --- a/.changeset/6110-predicate-scope-unbound-evaluators.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/console': minor -'@object-ui/plugin-form': minor ---- - -⚠️ **Behaviour change: `current_user` predicates that have been doing nothing on -the console form routes and in the wizard's submit gate now TAKE EFFECT.** Read -this before upgrading if any of your form metadata gates on the session user. - -objectui#6010 bound the host predicate scope on the five authored-predicate call -sites in the components form renderer, so `current_user` (plus the ADR-0068 -`user` / `ctx.user` / `os.user` aliases) resolves on `visibleWhen` / `visibleOn` -there. Two other authored-predicate evaluators were still passing `undefined` -for that argument, so the same authored text meant two different things -depending on which surface opened the form (objectui#6110): - -- **`apps/console`'s form renderer**, on the authed internal route - `/forms/:name`. The internal route is a runtime record surface by ADR-0089 - D1's own words (*"runtime record surfaces bind `record` + `current_user`"*), - and its `visibleWhen` metadata is the same `*.view.ts` FormView the - object-view chain renders — so a role gate authored once behaved differently - depending on which route opened the form. -- **`WizardForm`'s submit-time required re-check** (`missingRequiredByStep`), - the gate that re-checks the whole declared field set at final submit because - `allowSkip` can jump past a step. Its docstring promises *"the same verdict - from all three rather than a second, divergent dialect"*, and since #6010 it - was the divergent one. - -**Why nobody noticed, and why the fix is felt as a change.** `visibleWhen` fails -OPEN: a field on screen is what you get when the predicate resolves TRUE, when -the scope was never bound so the predicate faulted, *and* when the predicate is -broken. Those worlds were indistinguishable, so an app that authored a -`current_user` gate saw the field render and had no way to tell the rule was -inert. After this change the predicate is evaluated for real, and fields and -sections that have always been visible will disappear for the users the rule -excludes. `requiredWhen` fails the other way (CLOSED), so a `current_user` -requiredWhen that has been silently not applying will now start holding submits. - -In the wizard the change is a fix in the user's favour as well: a required field -the wizard HID from this user was still counted as visible by the submit gate, -so the submit was refused on a control the submitter could neither see nor fill -in. - -**Before upgrading**, audit any `visibleWhen` / `visibleOn` / `requiredWhen` in -your form-view and object metadata that names `current_user`, and confirm each -predicate says what you actually want evaluated against `record` + -`current_user`. - -**The public anonymous form `/f/:slug` is deliberately unchanged.** It is -mounted outside `ProtectedRoute` so an anonymous visitor can submit it, there is -no authenticated principal, and no provider is mounted above it — so its scope -is empty and a `current_user` predicate authored on a public form still faults -and still fails open, exactly as before. Nothing new is declared to say so: the -two routes are told apart by which component mounts them. - -`@object-ui/app-shell` exports `buildExpressionUser`, the `ExpressionProvider` -user normalisation, so every console surface that mounts the provider publishes -the same `current_user` shape rather than re-deriving it. diff --git a/.changeset/6111-formsection-visiblewhen.md b/.changeset/6111-formsection-visiblewhen.md deleted file mode 100644 index d17c7fa6e8..0000000000 --- a/.changeset/6111-formsection-visiblewhen.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/plugin-form': minor -'@object-ui/types': minor ---- - -⚠️ **Behaviour change: an authored `FormSection.visibleWhen` that has been doing nothing -will now START HIDING SECTIONS.** Read this before upgrading if any of your metadata -authors a section predicate. - -`@objectstack/spec` declares `FormSection.visibleWhen` and this repo's spec bridge maps it -through, but every plugin-form layout renders a section header as a virtual -`section-divider` pseudo-field and none of them copied the predicate onto it. On the -object-view chain — the create/edit modal, the drawer, the split form, and the full-page -record form — the key was declared, mapped, carried, and then dropped one hop before -anything could evaluate it. The section rendered unconditionally, with no diagnostic -(objectui#6111). - -**Why nobody noticed, and why the fix is felt as a regression.** `visibleWhen` fails OPEN: -a section that renders is what you get when the predicate resolves TRUE, when the predicate -never arrives, *and* when the predicate faults. Those three worlds were indistinguishable, -so an app that authored a section predicate saw its section render and had no way to tell -that the rule was inert. Every such app has been running with the rule switched off, and -some will have been authored — or simply grown used to — that state. After this change the -predicate is evaluated for real, and sections that have always been visible will disappear -for the users the rule excludes. - -This is the intended ADR-0089 contract being delivered, not a new capability: the key was -already declared, already documented, and already honoured by the console form renderer. -The object-view chain was the one that silently ignored it. - -**Before upgrading**, audit any `sections[].visibleWhen` in your form-view metadata and -confirm each predicate says what you actually want, evaluated against `record` + -`current_user`. A predicate that was written speculatively, or left behind after a rework, -now takes effect. - -**Measured scope of the hide.** The predicate gates the section's HEADER row. The renderer -treats `section-divider` as presentational and holds no association between it and the -fields that follow it, so a false predicate removes the heading and the section's fields -keep rendering. The console renderer (`apps/console`) drops the whole `
`, fields -included. That divergence is real, is pinned honestly by this change's tests rather than -implied away, and is filed separately — it needs a renderer-side grouping contract, not -another line in a layout. - -Two hops were dropping the key and both are repaired: `ObjectForm` rebuilds each section -key by key when it delegates to Split/Drawer/Modal (and `ModalForm`'s own `groups` map does -it again), so a key those maps did not copy never reached the layout at all; and the six -`section-divider` synthesis sites across the four layout files. - -`@object-ui/types` gains the matching `ObjectFormSection.visibleWhen` declaration. diff --git a/.changeset/6120-doc-snippet-dependency-resolution.md b/.changeset/6120-doc-snippet-dependency-resolution.md deleted file mode 100644 index 01f3621a10..0000000000 --- a/.changeset/6120-doc-snippet-dependency-resolution.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Doc-snippet gate tooling only, no published package source changed. - -`check-doc-snippet-types` compiles every covered snippet as its own module at the -repository ROOT. Workspace packages resolved there — it builds `paths` from each -package's own `exports` — but a THIRD-PARTY specifier did not: under pnpm, a -workspace package's own dependency is not hoisted to the root, so a snippet that -imports `lucide-react` failed `TS2307` even though `@object-ui/layout` and -`@object-ui/components` both declare it and any reader who installs those -packages gets it. Five correct blocks across `content/docs/layout` were red on -nothing but that, which blocked the whole group from being brought under the -gate. The snippets were right; the resolution environment was the gap. - -The gate now derives `paths` for the specifiers each imported package DECLARES in -its own `dependencies`, resolved from inside that package's directory — the -environment a real consumer has. Deliberately narrow, and it fails closed: -`dependencies` only (not peers, not devDependencies), only packages a covered -document actually imports, only the bare specifier (no subpath wildcard), and a -dependency shipping no types is left unresolvable rather than approximated. The -repository's own manifests are untouched — declaring `lucide-react` at the root -to buy a snippet its coverage would change what this repo claims to need in order -to satisfy a checker. - -A fourth self-control (`undeclared`) now runs on every invocation and keeps that -narrowness honest: a module importing `@floating-ui/react-dom` — installed here -as a transitive of Radix's popper, declared by no package a covered document -imports — MUST still produce `TS2307`. Widen resolution past the declarations and -that control goes green, which is the only way to notice that the gate has become -a rubber stamp no snippet can fail. diff --git a/.changeset/6122-action-prop-renames.md b/.changeset/6122-action-prop-renames.md deleted file mode 100644 index d99824e289..0000000000 --- a/.changeset/6122-action-prop-renames.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Docs only, publishes nothing: five `content/docs/components` reference pages -annotated an event prop as `string | ActionConfig` — a type `@object-ui/types` -has never exported. Each site was resolved individually against the freshly -built `packages/types/dist/*.d.ts`, and in every case the shipped slot is a -plain function callback that the page had also named wrongly: - -| page | documented | shipped slot | declaration | -| --- | --- | --- | --- | -| `form/command.mdx` | `onSelect` | `onChange?: (value: string) => void` | `form.d.ts:1376` | -| `form/radio-group.mdx` | `onValueChange` | `onChange?: (value: string \| number) => void` | `form.d.ts:394` | -| `form/date-picker.mdx` | `onDateChange` | `onChange?: (date: Date \| undefined) => void` | `form.d.ts:642` | -| `form/combobox.mdx` | `onValueChange` | `onChange?: (value: string) => void` | `form.d.ts:1324` | -| `feedback/toast.mdx` | `actionLabel` + `onAction` | `action?: { label: string; onClick: () => void }` | `feedback.d.ts:137` | - -The `string |` half of each annotation goes with the name, and that is the -substance rather than a tidy-up: objectui#4453 narrowed the runtime to -`typeof === 'function'`, so an authored string handler is **dropped** -(`packages/plugin-calendar/src/calendar-view-renderer.tsx`). A reference page -promising `string | Fn` is exactly what makes an AI author emit a handler that -publishes, validates, and silently does nothing. - -No type was minted to make the prose true. Measured with a throwaway re-fence -probe: `TS2304: Cannot find name 'ActionConfig'` **10 → 5** across the -`components` group, and the 5 that remain are objectui#6132's, untouched here. - -Part of objectui#6122. diff --git a/.changeset/6126-fields-unresolvable-imports.md b/.changeset/6126-fields-unresolvable-imports.md deleted file mode 100644 index db0de73c8a..0000000000 --- a/.changeset/6126-fields-unresolvable-imports.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Docs only, publishes nothing: two `content/docs/fields` snippets referenced -identifiers the snippet program cannot resolve, so both pages were held out of -objectui#5867's batch 3 (objectui#6126). Neither is fixed by making the compiler -happy — each was narrowed to what ObjectUI actually owns. - -`auto-number`'s Sequence Management block called `db.transaction` on a `db` -declared nowhere in the workspace and exported by nothing (TS2304, plus TS7006 -on the `tx` that fell out of it). Declaring an ambient `db` would have had the -renderer's documentation mint a backend contract it does not own, so the block -is now the metadata the backend actually reads — a literal annotated with the -exported `AutoNumberFieldMetadata`, carrying `format` and `starting_number` — -and the transactional sketch is prose: one counter per object-and-field pair, -incremented in the same transaction that inserts the record. The page now also -says the thing it never said, which is that ObjectUI never allocates a value at -all and renders a placeholder until the saved record comes back. - -`object`'s Backend Validation block opened `import Ajv from 'ajv'`, and `ajv` is -declared by no `package.json` in this repository and resolves from nowhere -(TS2307). Adding it as a dependency to satisfy a checker was refused, so the -block keeps the ObjectUI half — an `ObjectFieldMetadata` literal whose `schema` -is the JSON Schema a server validates against — and the Ajv call sequence, which -was Ajv's documentation rather than ObjectUI's, is a prose sentence naming it as -one option among any JSON Schema validator. The section now states the fact a -reader most needs: `ObjectField` checks JSON syntax only and never enforces -`schema`, so structural validation is the server's. - -Both pages join the compile population with the batch's own classifier: the four -`plaintext`-fenced blocks whose first line starts with `import` or `interface` -are now `ts`. The gate's blocks-to-compile count rises from 206 to 210 — exactly -those four — with diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and -the covered/ungated and declared-fragment sets unmoved. diff --git a/.changeset/6127-location-map-fence-split.md b/.changeset/6127-location-map-fence-split.md deleted file mode 100644 index 49578b3076..0000000000 --- a/.changeset/6127-location-map-fence-split.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -Docs only, publishes nothing: `content/docs/fields/location.mdx`'s *Integration -with Maps* fence welded two different examples into one block — an import plus a -JSX element (which needs `tsx`), immediately followed by a bare metadata object -literal at statement position (which `tsx` reads as a labelled statement, then -fails on the commas). Measured standalone with the repo's own TypeScript on -`origin/main`: **2 syntactic diagnostics as `ts`, 5 as `tsx`** — it parsed under -neither fence language, which is why `location.mdx` was excluded from objectui#5867 -batch 3 by measurement (objectui#6127). The fence is now **split in two**, each half -fenced for what it actually is: a `tsx` block holding the widget example, made -self-contained so it compiles (it renders `LocationField` with a typed -`LocationFieldMetadata` and a state-held coordinate pair, instead of spreading an -undeclared `props`), and a `jsonc` block holding the `object-map` metadata node — -which is a schema-key question `check-doc-snippet-types` explicitly says it does -not answer. Connecting prose numbers the two halves so the section still reads as -one example. The page's *Field Schema* block, the other fence triage's classifier -calls code, is re-fenced `plaintext` → `ts` in the same pass; the two genuinely -prose fences (`{` and `// Valid coordinates`) are left alone. - -Accounting, stated because splitting a fence breaks the plain "blocks-to-compile -rises by exactly the batch size" identity this card family has used across three -batches: **1 block re-fenced, plus 1 fence split into 2 of which 1 half is -TypeScript, so blocks-to-compile rises by 1 + 1 = 2** — measured 206 → 208, with -diagnostics at 0, declared fragments unmoved at 111, and the covered/ungated sets -unchanged. The page's fence count rises 4 → 5. Both blocks also stop rendering as -unstyled plaintext and pick up TypeScript and JSON-with-comments highlighting, -which is reader-visible. diff --git a/.changeset/6131-sidebar-nav-navgroup-annotation.md b/.changeset/6131-sidebar-nav-navgroup-annotation.md deleted file mode 100644 index bb475295e3..0000000000 --- a/.changeset/6131-sidebar-nav-navgroup-annotation.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the "Complete Example" block in -`content/docs/layout/sidebar-nav.mdx` now annotates its grouped-navigation array as -`NavGroup[]`, importing the type in the same block (objectui#6131). - -Without the annotation the array's `badgeVariant: 'destructive'` widens to `string`, -and `SidebarNav`'s declared `badgeVariant?: 'default' | 'destructive' | 'outline'` -does not accept it — so the whole array fails to assign to -`items: NavItem[] | NavGroup[]`. TypeScript then reports the union's *other* branch, -which is why the message reads "missing the following properties from type 'NavItem': -title, href" and looks at first like a wrong data shape. The data shape was always -right; the annotation was missing. A reader who copies this block into an annotated -position, or assigns it anywhere typed under `strict`, hits the same TS2322. - -Measured on this branch against the built `dist/*.d.ts`, with the block temporarily -re-fenced as `ts` under an `EXIT` trap so it joins the compile population (the fence -itself belongs to objectui#5867's lane and is deliberately left as `plaintext` here): - -- before the annotation, `Semantic phase: 224 of 224 block(s) judged, 1 failed`, gate - exit 1, on `content/docs/layout/sidebar-nav.mdx:235:7 TS2322`; -- after it, `Semantic phase: 224 of 224 block(s) judged, 0 failed`, gate exit 0. - -Removing the annotation again reproduces the identical TS2322, so it is load-bearing -rather than decorative. Declared fragment count is unmoved at 111, no -`FRAGMENT_MARKER` is added, and the covered/ungated sets are untouched. - -The annotation is written so that it **resolves**: the type is imported in the same -block, because every block compiles in isolation. An annotation naming a type the -block cannot see errors on the annotation itself, at which point TypeScript stops -checking the literal underneath and the TS2322 disappears — the false-green shape -already recorded for `guide/theming` in `scripts/check-doc-snippet-types.mjs`. - -This clears the last `content/docs/layout` block that stays red for a reason of its -own, so the layout group is type-clean ahead of objectui#5867 re-fencing it. diff --git a/.changeset/6138-fields-schema-block-parity-pr1.md b/.changeset/6138-fields-schema-block-parity-pr1.md deleted file mode 100644 index ac0d7216fe..0000000000 --- a/.changeset/6138-fields-schema-block-parity-pr1.md +++ /dev/null @@ -1,50 +0,0 @@ ---- ---- - -Docs only, publishes nothing: the `Field Schema` block on every -`content/docs/fields` page declared an interface of its own, so -`check-doc-snippet-types` compiled it **vacuously** — a self-declared interface -with no imports type-checks no matter what it says, because nothing in the block -refers to anything the packages export (objectui#6138). The gate reported those -pages green and structurally could not see whether the documented shape matched -the shipped one. This is batch 1 of 2: the shared page plus four converted -pages, proving the mechanism end to end before it is applied to the rest. - -Each converted `Field Schema` block is now a literal **annotated** with that -field type's exported `*FieldMetadata`, so the sealed type's excess-property -check judges every documented key. The page becomes structurally incapable of -teaching a key the type does not have. - -Measured before converting: all 26 pages diverged from their exported type, and -on 24 of them the divergence was **entirely** `FieldWidgetComponentProps` -members — a real, exported, reader-facing surface filed under a heading that -says "Field Schema". Deleting them would have deleted correct API, so the shapes -are separated instead of one being dropped: a new `content/docs/fields/widget-props.mdx` -documents `FieldWidgetComponentProps` once, with a gate-compiled example and the -type named as the source of truth, and the field pages link to it. That page -carries no hand-maintained key list — the type has 76 members and a prose -restatement of a declared surface is the defect class objectui#6086 is open for. - -Three documentation defects the conversion forced out into the open, each a page -teaching something no shipped type declares: - -- `date.mdx` documented the range bounds as `min` / `max`; `DateFieldMetadata` - declares `min_date` / `max_date`, and the sibling `datetime.mdx` already - documented that spelling. Two adjacent reference pages taught two spellings of - one concept and one of them did not exist. The docs are corrected; the type is - not touched. -- `textarea.mdx` named `TextAreaFieldMetadata`; the export is - `TextareaFieldMetadata` (lowercase `a`). A name that must resolve, so it now - does. -- `rich-text.mdx` named `RichTextFieldMetadata`, which does not exist at all. The - page's own block says `type: 'markdown' | 'html'`, so it resolves against the - existing `MarkdownFieldMetadata` / `HtmlFieldMetadata` pair rather than a - minted type. It also documented `toolbar`, `preview`, `minHeight` and - `maxHeight`, which `RichTextField` reads nowhere (`minHeight` / `maxHeight` - have zero occurrences in `packages/fields/src`), and `rows`, which it does read - through an `as any` while neither metadata type declares it — filed as - objectui#6140 with that measurement in it. - -The gate's blocks-to-compile count rises from 225 to 227 — the new page's two -blocks, the conversions being one-block-for-one-block — with diagnostics at 0, no -new `FRAGMENT_MARKER` declarations, and the declared-fragment count unmoved at 111. diff --git a/.changeset/6138-fields-schema-block-parity-pr2.md b/.changeset/6138-fields-schema-block-parity-pr2.md deleted file mode 100644 index 2bcd93ad29..0000000000 --- a/.changeset/6138-fields-schema-block-parity-pr2.md +++ /dev/null @@ -1,49 +0,0 @@ ---- ---- - -Docs only, publishes nothing: batch 2 of 2 for objectui#6138 — the remaining -**22** `content/docs/fields` pages have their `Field Schema` block converted from -a self-declared interface to a literal **annotated** against that field type's -exported `*FieldMetadata`, each linking the shared -`content/docs/fields/widget-props.mdx` page batch 1 added. - -A self-declared interface with no imports type-checks no matter what it says, so -`check-doc-snippet-types` reported these pages green while being structurally -unable to see whether the documented shape matched the shipped one. An annotated -literal is judged by the compiler instead: the page becomes incapable of teaching -a key the type does not have. - -That property depends on the annotated types being **sealed**, so it was measured -rather than assumed. All 26 types this batch annotates against — the 22 field -metadata types plus `SelectOptionMetadata`, `GridColumnDefinition`, -`UploadedFileMetadata` and `LookupColumnDef` — carry no index signature, and a -nonsense key on each produces `TS2353`. The same control run against the -components lane's `BaseSchema` / `ButtonSchema` produces **zero** diagnostics, -which is what an open type does and why that lane cannot use this mechanism -(objectui#6143). - -Documentation defects the conversion forced out, each a page teaching something -no shipped type declares: - -- `grid.mdx` documented a per-column `editable` flag and a string `width`. - `GridColumnDefinition` declares neither — `width` is a number of pixels — and - no widget reads a column-level `editable`. Both are corrected. -- `formula.mdx` documented `return_type` as taking `currency`; the shipped union - is `'text' | 'number' | 'boolean' | 'date' | 'datetime'`. -- `lookup.mdx` documented option keys `_id` and `name`; the widget matches - options by `value` and labels them by `label`, and those two keys are read off - **records**, not options. -- `user.mdx` documented the value as a user object; the field stores the user's - id and the picker resolves the rest from `sys_user`. - -Two undeclared-but-consumed keys were found by checking each divergence against -its renderer, and are filed rather than deleted or documented as metadata: -`dependsOn` on select and `description` on a lookup's static options -(objectui#6153, the same class as objectui#6140). The location field's stored -`{ latitude, longitude }` value shape is declared by no exported type at all -(objectui#6154), so that page describes it in prose and points at the card. - -The gate's blocks-to-compile count rises from 248 to 249 — 21 conversions are -one-block-for-one-block and `lookup.mdx` becomes two blocks (data-source-backed -and static-option) — with diagnostics at 0, no new `FRAGMENT_MARKER` -declarations, and the declared-fragment count unmoved at 111. diff --git a/.changeset/6143-components-schema-corrections.md b/.changeset/6143-components-schema-corrections.md deleted file mode 100644 index c23ec20ba2..0000000000 --- a/.changeset/6143-components-schema-corrections.md +++ /dev/null @@ -1,38 +0,0 @@ ---- ---- - -Docs only, publishes nothing: four `content/docs/components` reference pages -taught props the shipped types do not have. Each correction was resolved -against a freshly built `packages/types/dist/*.d.ts` **and** against the zod -schemas that are the ruled enforcement boundary, so both layers agree: - -| page | documented | shipped | declaration | -| --- | --- | --- | --- | -| `feedback/toast.mdx` | `variant?: 'default' \| 'destructive'` | `variant?: 'default' \| 'success' \| 'warning' \| 'error' \| 'info'` | `feedback.d.ts:123`, `zod/feedback.zod.js:59` | -| `form/radio-group.mdx` | `direction` | `orientation?: 'horizontal' \| 'vertical'` | `form.d.ts:377`, `zod/form.zod.js:263` | -| `form/combobox.mdx` | `searchPlaceholder`, `emptyText` | neither is declared | `form.d.ts:1283`, `zod/form.zod.js:378` | -| `form/command.mdx` | `CommandItem.shortcut` | `CommandItem` declares only `value`, `label`, `icon` | `form.d.ts:1329`, `zod/form.zod.js:76` | - -`toast.mdx` is the one that mattered most: `'destructive'` is not a member of -the union, it is the Shadcn vocabulary a reader arrives with, and it is the -value most likely to be copied verbatim into an authored schema. - -Each removal was checked against the renderer before it was made, because a -key a renderer genuinely reads is an undeclared capability rather than a doc -error. None of these four is read: `renderers/form/combobox.tsx` contains -neither `searchPlaceholder` nor `emptyText`, `renderers/form/command.tsx` -contains no `shortcut`, and `renderers/feedback/toast.tsx` contains no -`'destructive'`. The same sweep's genuinely-consumed keys went to -objectui#6150 instead of being edited away. - -`emptyText` is removed from `ComboboxSchema` only. `CommandSchema` really does -declare it (`form.d.ts:1368`), so `command.mdx` keeps it. - -No type, export or union member was minted, nothing was re-fenced, and no -block changed shape — these blocks stay `plaintext` and stay outside the -compile population. `check:doc-snippets` reports the same numbers before and -after: 248 blocks to compile, 111 declared fragments, 178 covered / 44 -ungated. The import mechanism this card originally proposed is sequenced -behind objectui#5155 and is deliberately not attempted here. - -Part of objectui#6143. diff --git a/.changeset/6151-stack-schema-omit-collapse.md b/.changeset/6151-stack-schema-omit-collapse.md deleted file mode 100644 index 92f2ab2c8a..0000000000 --- a/.changeset/6151-stack-schema-omit-collapse.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`StackSchema` now SHIPS the members it declares (objectui#6151). Its emitted declaration -carried one property — `type` — where it was meant to carry twenty-five. - -The interface was written `extends Omit`: "everything `FlexSchema` has, -with a different `type`". That spelling erases every named member. `Omit` of a type over a -key set is `Pick` over `Exclude` of `keyof` that type, and `keyof` a type carrying a string -index signature is `string | number` — the literal member names are absorbed. `FlexSchema` -inherits `BaseSchema`'s `[key: string]: any` (objectui#5155), so excluding `'type'` from -`string | number` still leaves `string | number`, and the `Pick` rebuilt a type holding the -index signature and none of the named members. Measured against the built `dist`: -`FlexSchema` declared 25 properties, `StackSchema` declared 1. - -Nothing errored, which is why it survived four releases: the index signature keeps every -absent key assignable and readable as `any`. The cost fell entirely on the tools that READ -the declaration. Editor completion on a `stack` node offered `type` and nothing else — no -`gap`, no `align`, no `justify`, no `children`. And a docs-versus-type sweep read -`stack.mdx` as documenting keys that do not exist: objectui#6143 flagged `gap`, `children` -and `className` there as divergences when the docs were right and the type was wrong. - -Fixed at the mechanism rather than by restating the members. The six flex/stack members now -live in a new exported interface, `FlexLayoutProps`, which does NOT inherit `BaseSchema`, -and `FlexSchema` and `StackSchema` each extend `BaseSchema` and `FlexLayoutProps`. No -`Omit` crosses the index signature any more, and the members are declared once rather than -duplicated. Extending `FlexSchema` directly was measured unavailable: an interface may -narrow an inherited property only to a subtype, and `'stack'` is not a subtype of -`FlexSchema`'s `type: 'flex'` (TS2430). - -`FlexSchema` is unchanged — its six member declarations moved byte-identically, and its -emitted member set is the same 25 names before and after. The only declaration whose shape -changes is `StackSchema`, which goes from 1 property to the same 25. - -**The one way this can newly error**, and why it ships as `minor`: keys on a `stack` node -were previously answered by the index signature as `any`, so `gap: 'large'` type-checked. -`gap` is now `number | undefined` and that line is a `tsc` error. Every value this newly -rejects is one the renderer never honoured — `stack.tsx` feeds `gap` to a Tailwind numeric -scale — so the change reports a defect that was already there rather than removing a -capability. All three in-repo packages that name `StackSchema` or `FlexSchema` -(`@object-ui/components`, `@object-ui/core`, the schema-catalog example) type-check green -unchanged. - -Guarded by `packages/types/src/__tests__/stack-schema-emitted-members.test.ts`, which -asserts against the EMITTED declaration rather than the source. That distinction is the -whole point: a source-level assertion passes on the broken code, because the index -signature answers for the missing key with `any`. The guard emits declarations with the -package's own tsconfig and asserts (1) `StackSchema` declares exactly what `FlexSchema` -declares, and (2) no member of the `LayoutSchema` union has lost any of `BaseSchema`'s -named members — so the next heritage clause that collapses under the index signature reds -for the whole class, not just for this one interface. diff --git a/.changeset/6158-radio-group-orientation.md b/.changeset/6158-radio-group-orientation.md deleted file mode 100644 index 51472aa062..0000000000 --- a/.changeset/6158-radio-group-orientation.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`radio-group` now renders the `orientation` its own type has always declared (objectui#6158). - -`RadioGroupSchema.orientation` was declared in two layers and read by none. The shipped TS -type carries `orientation?: 'horizontal' | 'vertical'` with `@default 'vertical'` -(`packages/types/src/form.ts:383`) and the zod mirror carries the matching -`z.enum(['horizontal', 'vertical'])` (`packages/types/src/zod/form.zod.ts:282`), while -`packages/components/src/renderers/form/radio-group.tsx` contained neither the string -`orientation` nor `direction` and forwarded only `defaultValue`, `className`, the -form-control DOM whitelist and the designer props. - -The consequence was measurable rather than cosmetic: every radiogroup root the library -rendered came back byte-identical on that axis — no `data-orientation`, no -`aria-orientation` — so the docs page's `## Layout Options` section demonstrated a -distinction the product could not make, and its horizontal demo rendered vertically. An -author reading the shipped type had every reason to write `orientation: 'horizontal'` and -no way to discover it was inert. - -The key is now forwarded to the underlying Radix `RadioGroup`, which accepts it natively -with the same two-value vocabulary and puts it on the root as `aria-orientation` and -`data-orientation`; the layout utilities follow it so the visible difference the docs -promise is real. This restores declared = enforced **without widening the acceptance -set** — no new key is accepted, and no spelling outside the declared enum becomes legal. - -Two behaviour notes for anyone already shipping radio groups: - -- The declared `@default 'vertical'` is now actually applied instead of being left to - Radix's own `undefined`. A group that never authored the key keeps the vertical stack it - already rendered, and additionally announces `aria-orientation="vertical"` — the - announced orientation now agrees with the rendered one rather than being absent. Arrow - key roving focus narrows to Up/Down for those groups, which is the correct pairing for a - vertical stack. -- Author `className` still wins: the orientation layout utilities compose first and the - authored class last, so tailwind-merge resolves every conflict in the author's favour. - -Registry meta `inputs` for `radio-group` gains `orientation` in the same change — it was -the third surface that omitted the key, and leaving it out would have kept the designer -palette disagreeing with the type. diff --git a/.changeset/6165-home-item-type-label-parity.md b/.changeset/6165-home-item-type-label-parity.md deleted file mode 100644 index ea059e0160..0000000000 --- a/.changeset/6165-home-item-type-label-parity.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Home renders one agreed label for an item kind that has no translation key. - -The rail (`HomeContinue`), `RecentApps` and `StarredApps` all resolve the same -`home.recentApps.itemType.*` label, and each spelled the lookup itself. They had -drifted: the rail fell back to the bare kind (`report`) where both card surfaces -fell back to the capitalised one (`Report`) — two spellings of the same word on -one screen. All three now resolve through a single `recentItemTypeLabel` helper, -so the fallback cannot drift apart again. - -User-visible: the rail's label for an unkeyed kind changes from `report` to -`Report`. Every kind shipping today carries a key, so no label changes for them; -this is about the next kind added, and any host passing a kind the locales do -not carry. diff --git a/.changeset/6166-zh-breadcrumb-reports.md b/.changeset/6166-zh-breadcrumb-reports.md deleted file mode 100644 index 99da771680..0000000000 --- a/.changeset/6166-zh-breadcrumb-reports.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -The zh pack's `console.breadcrumb.reports` renders 报表, the noun the rest of the pack -already uses for the report feature (objectui#6166). - -**The authority for this edit is the maintainer ruling of 2026-08-25, not an occurrence -count.** The card was filed explicitly as a native-speaker call and explicitly forbade -resolving it by normalising to the majority: 报表 (a tabular/data report) and 报告 (a -written/narrative report) are not interchangeable, so a pack that spells one key -differently from its siblings is evidence of a majority and never, on its own, evidence of -a mistake. The maintainer ruled that no deliberate narrative-report distinction was -intended here and that the breadcrumb names the same report feature the rest of the pack -calls 报表. - -The render context corroborates the ruling and closes the confidence gap triage recorded -when it declined to decide this itself. `console.breadcrumb.reports` labels the -`routeType === 'report'` **list** route in app-shell's `AppHeader` — a structural sibling -of the `dashboards`, `pages` and `system` segments beside it — and drilling through it -appends a metadata report definition, the same feature named by -`console.commandPalette.reports`, `console.nav.navReport`, `search.typeReports`, -`search.badgeReport` and `search.reportNotFound`. Nothing narrative renders beneath it. - -A comment at the key records that this was **ruled** rather than counted, and states the -报表/报告 distinction it was ruled against, so the next reader measuring pack consistency -neither re-files it nor quietly restores 报告 after reading the render context and -disagreeing. That comment is half of the deliverable; the value change alone would leave -the decision unrecorded, which is the failure mode the card was most concerned about. - -**Scope: one key, one pack.** This is not a licence to normalise vocabulary across the ten -packs — the card names that hazard explicitly and says it would need its own ruling, and -this ruling grants nothing beyond the single key it names. No `en` value changes, so no -other pack is asked to follow, and no other pack was touched. diff --git a/.changeset/6167-plugin-form-catalog-entries.md b/.changeset/6167-plugin-form-catalog-entries.md deleted file mode 100644 index 0a31d99a68..0000000000 --- a/.changeset/6167-plugin-form-catalog-entries.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Docs and catalog fixtures only, no published package source touched: the two `plugin-form` -catalog entries are now real `object-form` nodes instead of hand-built `form` schemas. -`content/docs/plugins/plugin-form.mdx` mounted `basic-form` and `contact-form` under -`PluginLoader plugins={['form']}` while both authored a root `type: "form"` with `input` / -`select` / `checkbox` / `textarea` fields — every one of those registered by -`@object-ui/components`, none of them by `@object-ui/plugin-form`, whose own keys are -`object-form`, `embeddable-form`, `form-analytics`, `object-master-detail-form`, -`record:line_items` and `view:form`. They are replaced by `object-form-record` (one `users` -record, fields derived from the object's metadata, `fields` / `columns` shaping the grid) -and `object-form-tabbed-sections` (the same record with `formType: 'tabbed'`, so the -declared `sections` become tab panels of one form). - -The two hand-built forms are legitimate `@object-ui/components` examples filed under the -wrong plugin, so they are re-seated into `components-form-form` as `basic-form` and -`demo-request-form` rather than deleted — deleting a catalog entry moves corpus-wide -counters (`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx` and in -`form-control-dom-leak-5632.test.tsx`, and the `className`-carrying layout node and `stack` -node floors in `layout-props-conversion.test.tsx`), and a counter that moves because a -fixture was deleted is indistinguishable later from one that moved because coverage -regressed. All of them are unchanged, with no floor edited. - -`apps/site/app/components/registerCatalogBlocks.ts` now declares `@object-ui/plugin-form`, -and its header no longer claims the import list is "exactly the packages that census -resolves to". The two `OWN_PLUGIN_DEBT` lines in `catalog-gallery-render.test.tsx` are -deleted, so both entries are held to the pin like every other one, and the pin's -provenance instrument now reads the fixture record out of a form control's value as well as -out of the tile's text — a form puts its record in `input.value`, where `textContent` -cannot see it. diff --git a/.changeset/6170-timeline-schema-declared-keys.md b/.changeset/6170-timeline-schema-declared-keys.md deleted file mode 100644 index da89bd6eca..0000000000 --- a/.changeset/6170-timeline-schema-declared-keys.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-timeline': minor -'@object-ui/types': minor ---- - -`TimelineSchema` now declares the presentational keys the timeline renderer actually reads -(objectui#6170, maintainer ruling 2026-08-25 — the same family rule adopted on -objectui#6172: the exported type aligns to the measured authored + read set). - -Before this, `TimelineSchema` declared `events` (required), `orientation` and `position`, -and nothing else. `TimelineRenderer` is annotated `schema: TimelineSchema` and reads nine -keys off that node — `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`, -`rowLabel`, `scale`, `timeScale` — and **none** of the three that were declared. The docs -property table and the registration's own designer `inputs` had agreed with the renderer -all along; only the exported type disagreed. It was invisible to `tsc` because `BaseSchema` -carries `[key: string]: any`, so every undeclared key resolved as `any` and the annotation -constrained nothing. - -The most visible casualty was the docs page's own TypeScript example, which did not -compile: `Property 'events' is missing in type '{ type: "timeline"; variant: string; items: -… }' but required in type 'TimelineSchema'`. The page taught an authoring form its own -published type refused. - -**Declared now** (TS interface and the `@object-ui/types/zod` mirror together): `variant`, -`items`, `dateFormat`, `scale`, `timeScale`, `rowLabel`, `minDate`, `maxDate`. `onItemClick` -is deliberately left undeclared — it is a runtime slot `ObjectTimeline` installs, and this -package keeps callback-shaped keys off the authored surface. - -**`scale` is the canonical axis key.** It is `@objectstack/spec`'s `ui/TimelineConfig.json` -spelling and the one `resolveTimelineScale` reads first (`scale ?? timeScale`). The designer -now offers it, with all six buckets: `hour` / `quarter` / `year` have rendered correctly -since objectui#2942 but were offered by neither the designer (which listed three) nor the -exported type (which listed none), so they were authorable and undiscoverable. `timeScale` -stays as a deprecated alias so stored JSON keeps working; retiring it is routed separately. - -**`events` is now optional.** It was required, which is why the documented authoring form -did not type-check. That widening is the only non-additive change here — strictly more -programs compile and strictly more input parses than before. `events`, `orientation` and -`position` remain declared and remain read by nothing; a timeline authored with `events` -still renders an empty rail. Their removal is a breaking narrowing of a published type and -is routed through ADR-0049 enforce-or-remove as its own change, not smuggled into this one. - -Accept-set note for consumers: keys that previously resolved as `any` are now typed, so a -value the renderer never implemented — `variant: 'diagonal'`, `dateFormat: 'medieval'`, -`scale: 'fortnight'` — is a type error and a Zod rejection where it used to pass silently. -Nothing that renders today stops rendering. `BaseSchema`'s index signature is untouched, so -an undeclared key is still accepted by both halves (objectui#5155 / objectui#6269 own that -ceiling). diff --git a/.changeset/6175-column-state-persistence.md b/.changeset/6175-column-state-persistence.md deleted file mode 100644 index ae287843cf..0000000000 --- a/.changeset/6175-column-state-persistence.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/plugin-grid': patch ---- - -Column width and order that a user drags in `ObjectGrid` now actually persist -(objectui#6175). Both halves of `saveColumnState`'s only two call sites were dead, so a -drag was written nowhere — not to `localStorage`, not through `onColumnStateChange` to the -host's `dataSource.updateViewConfig`. The saved state was read back correctly forever; it -was simply never written. - -Two independent breaks, one per package: - -- **`@object-ui/components`** — `DataTableSchema` has declared - `onColumnResize?: (columnKey, width) => void` all along, and `data-table.tsx` invoked it - **nowhere**: the resize drag updated the table's local `columnWidths` state and stopped - there. It now reports the settled width once, at `mouseup`. Once, deliberately — the host - turns this callback into a write to shared view config, so a per-`mousemove` callback - would be a write storm. -- **`@object-ui/plugin-grid`** — `ObjectGrid` emitted `onColumnReorder` (singular) while the - renderer invokes the near-duplicate `onColumnsReorder` (with the `s`), a different declared - key with a different signature. The producer now emits the spelling the renderer actually - invokes, mapping the reported `TableColumn[]` to the `accessorKey` order `columnState` - stores. - -**Nothing is retired.** Both spellings remain declared on `DataTableSchema`; -`onColumnReorder` stays declared and stays unwired, exactly as the `RuntimeOnlyDeclared` -ledger in `zod-mirror-parity.test.ts` records it. Which of the two survives is a -declared-surface ruling that stays open and is deliberately not settled here. - -⚠️ Behavioural note for hosts: `onColumnStateChange` now fires where it previously never -did, which means `dataSource.updateViewConfig` is now reached on a column drag. That call -was unreachable by this path before, so any permission gate on that write now sees traffic -it never saw. - -The renderer's resize/reorder gestures, the inbound seeding of `columnState`, and the -declared surface are all unchanged. diff --git a/.changeset/6178-detail-section-header-color.md b/.changeset/6178-detail-section-header-color.md deleted file mode 100644 index 9a1a18887d..0000000000 --- a/.changeset/6178-detail-section-header-color.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`DetailSection` now resolves `section.headerColor` through a lookup of complete -Tailwind class literals instead of building `bg-` + the authored value as a -template literal (objectui#6178). - -Tailwind v4 has no runtime — it builds the stylesheet by scanning source text -for complete class tokens, and this workspace ships no `bg-*` safelist — so the -old expression contributed nothing to the compiled CSS. Measured, not assumed: -compiling `apps/console/src/index.css` with that expression deleted produced a -byte-identical stylesheet (same sha256). An authored value styled the header -only when some other source file happened to author the identical class -literally, which is why both documented examples appeared to work: `bg-muted` -occurs 691 times and `bg-primary/10` 63 times elsewhere in the workspace. That -liveness was accidental and moved with unrelated edits in unrelated packages. - -The shape matches the sibling this repo already solved the same way — -`useRowColor`'s `COLOR_TO_CLASS` in `@object-ui/plugin-grid`: - -- a lookup of literal, tint-only design-system classes: `muted`, `muted/50`, - `accent`, `primary/10`, `secondary/10`, `destructive/10`. Both values the - `@object-ui/types` mirror documents (`muted`, `primary/10`) are in it, so - nothing that rendered before renders differently now; -- a value that is already a complete `bg-*` class is passed through untouched. - This is new — `headerColor: 'bg-muted'` previously produced the meaningless - `bg-bg-muted`; -- anything else contributes no class at all, instead of a fabricated one. - -Behaviour change to be aware of: an undocumented bare suffix outside the -vocabulary (say `headerColor: 'blue-100'`) no longer reaches the DOM as -`bg-blue-100`. It rendered before only where another file happened to author -that exact class; write it as the complete class (`headerColor: 'bg-blue-100'`) -to keep it, on the same terms as any `className` a schema carries. No value is -rejected and the declared type is unchanged. - -`headerColor` remains undeclared on the strict `@objectstack/spec` -`record:details` section schema, which refuses it today on the strength of this -defect (objectstack#11661). Declaring it, and with which vocabulary, is a -separate spec decision. diff --git a/.changeset/6188-line-items-child-object-decline.md b/.changeset/6188-line-items-child-object-decline.md deleted file mode 100644 index 13a4a04f6c..0000000000 --- a/.changeset/6188-line-items-child-object-decline.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`record:line_items` declines to fetch the child schema of a panel whose child object it never resolved, instead of calling `getObjectSchema(undefined)`. - -`childObject` is declared `required: true` on the block's registry entry and typed `string` on -`LineItemsPanelSchema`, but nothing enforces either — `inputs[].required` is designer metadata, and -the block has no spec schema — so a node reaches the renderer straight off an authored schema with -the key `undefined`, and the child-schema effect asked the data layer for it anyway. Measured: -mounting the block through the registry with `childObject` unset issued -`getObjectSchema(undefined)`, and a real backend receives a query for an object literally named -`undefined`. The effect's `.catch` then turned the answer into a null child schema, so the visible -outcome was a silently unsanitized child grid rather than an error. - -The effect now declines and warns, naming the key and what to set it to, and clears the cached child -schema so a later save is never sanitized against a previous object's fields. This is the choice -`RelatedList` already makes for the same class of missing key (*"has no referenceField/parentId — -refusing to fetch all rows"*), and the one `object-master-detail-form` makes on this exact key. A -panel that names its child object fetches exactly as before. diff --git a/.changeset/6194-line-items-row-fetch-decline.md b/.changeset/6194-line-items-row-fetch-decline.md deleted file mode 100644 index 312e431c29..0000000000 --- a/.changeset/6194-line-items-row-fetch-decline.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`record:line_items` declines to LOAD OR WRITE the rows of a panel whose child object it never resolved, instead of calling `find(undefined, …)` — the sibling site of the child-schema decline, in the same component. - -`LineItemsPanel` read `schema.childObject` at two sites. The first now declines; the row load still -asked the data layer to `find` an object literally named `undefined`, scoped by -`{ [relationshipField]: parentId }`. `load` guarded the *data source* and the *parent id* — the two -things `RelatedList` calls "can I scope this query" — but not the *object being queried*. - -Declining that fetch is not enough on its own, and this is the part worth reading: `load` owns -`loading`, and the panel branched `loading ? "Loading…" : !parentId ? "Save the record first…" : -`. So the moment the fetch declined, an unresolvable panel with a parent id bound fell to the -third branch and showed an **empty editable grid with an Add button, over an object that does not -exist** — a worse outcome than the fetch it replaced. Measured on the pre-fix component: one -keystroke in the grid's always-present ghost row materialised a row, which enabled Save, which -reached `batchTransaction([{ object: undefined, action: 'create', data: { qty: 3, invoice: 'inv-1' } }])`. -The bad *read* was one keystroke away from a bad *write*. - -An unresolvable panel therefore gets its own render branch — a config hint naming `childObject` and -what to set it to, following the precedent `object-master-detail-form` set for this exact key and -`AdvancedChartImpl`'s refusal placeholders. It is checked ahead of `loading`, because nothing is -pending: the schema itself already says the panel can never resolve, so there is no honest moment at -which "Loading…" is true. `save` takes the same one-line guard, for the one route the render branch -cannot close — a schema edited to drop `childObject` while rows are already dirty. - -A panel that names its child object loads, renders and saves exactly as before. diff --git a/.changeset/6213-core-adapters-readme-owns-its-directory.md b/.changeset/6213-core-adapters-readme-owns-its-directory.md deleted file mode 100644 index 2f152e2974..0000000000 --- a/.changeset/6213-core-adapters-readme-owns-its-directory.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/data-objectstack': patch ---- - -`packages/core/src/adapters/README.md` now documents the adapters that are actually in that -directory, and the ObjectStack material it carried moved to the package that owns the behaviour -(objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a -README rides every tarball — so this was published documentation describing the wrong package. - -The page had been left behind when the ObjectStack adapter moved out to -`@object-ui/data-objectstack`: its headings, feature list, filter-operator table and -query-parameter table were all about that adapter, and its one-entry "Available Adapters" list -told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`. -`ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and -`emulateBatchTransaction` — the five exports that directory really ships — were named nowhere. - -- **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a - usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the - ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was - always about this directory. -- **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the - filter-operator and query-parameter mapping tables, the AST conversion example and the sorting - example. That material existed **only** in the `core` copy — this package's README documented - query translation as a single feature bullet — so it is ported, not dropped. - -No runtime behaviour changes; the duplicate copy of one package's documentation living under -another package is what goes away. diff --git a/.changeset/6223-object-payload-spec-keys.md b/.changeset/6223-object-payload-spec-keys.md deleted file mode 100644 index e6b7eb8071..0000000000 --- a/.changeset/6223-object-payload-spec-keys.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@object-ui/plugin-designer': minor -'@object-ui/app-shell': minor ---- - -Object-level metadata payloads no longer emit the three keys `ObjectSchema` refuses by -name — **group**, **sortOrder** and **relationships** (objectui#6223). - -Measured against the installed `@objectstack/spec` 17.2.0, whose `ObjectSchema` accept set -is 42 keys: - -``` -const base = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } }; - -ObjectSchema.safeParse(base) => success = true (control) -ObjectSchema.safeParse({ ...base, isSystem: true }) => success = true (control) -ObjectSchema.safeParse({ ...base, pluralLabel: 'A' }) => success = true (control) - -ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"] -ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"] -ObjectSchema.safeParse({ ...base, relationships: [ … ] }) => unrecognized_keys ["relationships"] -``` - -The two controls are what make that a key-by-key result rather than a schema refusing -everything. Each key was resolved on its own, as the objectui#5761 family ruling requires: - -- **group** — the Object Manager's grouping is a UI-only display category. The spec has no - object-level grouping key (`fieldGroups` groups the fields *inside* one object), so the - grouping control and its column stay, and the value is now DERIVED from the spec key that - is accepted (`isSystem`) instead of round-tripped. `MetadataObjectsPage` also strips a - `group` already stored by an earlier build, because its save-back spreads the server - document verbatim and would otherwise keep re-sending it forever. -- **sortOrder** — what populated it was the array index the converter happened to be at, - i.e. the order the list was already in. The declaration is removed from the object - payload. The field-level `sortOrder` is a different key with a different card - (objectui#6045) and is untouched. -- **relationships** — the spec models relationships on the FIELD (`reference` / - `master_detail`, plus object-level `indexes`). The object payload stops declaring and - sending an object-level relationship array; what the designer should author for a - relationship is a data-model question this change does not settle. - -**Breaking for TypeScript consumers of `ObjectMetadataPayload`** (exported from app-shell): -the three properties are gone from the published type, so code that set them stops -compiling. That is the point — setting any of them produced a payload the metadata route -refuses. `ObjectDefinition` (the designer's UI model) is unchanged and still carries all -three. - -The parity gate built for objectui#5761 now has a **second oracle**: every shape in -`PAYLOAD_SHAPES` names the schema that judges it, `ObjectSchema` alongside `FieldSchema`, -and reach is resolved within an oracle rather than across one — `group` is a legal -`FieldSchema` key and a refused `ObjectSchema` key at the same time. That extension found a -fourth object-level key (`enabled`, objectui#6238) and a value-level rejection the key-name -check cannot see (`fields` sent as an array where the spec wants a map, objectui#6240); -both are filed and ledgered rather than fixed here. diff --git a/.changeset/6230-budget-comment-half-status.md b/.changeset/6230-budget-comment-half-status.md deleted file mode 100644 index 3b79f6fff9..0000000000 --- a/.changeset/6230-budget-comment-half-status.md +++ /dev/null @@ -1,29 +0,0 @@ ---- ---- - -CI-only: the Bundle Analysis PR comment now names *which* eager-closure half -objected, instead of reporting only that something did. - -`scripts/check-eager-closure-budget.mjs` evaluates three halves and publishes a -verdict for each to `$GITHUB_OUTPUT` — `closure_status` (the aggregate ceiling), -`closure_chunk_status` (the per-chunk ceilings) and `closure_headroom_status` -(ceiling sensitivity). `.github/workflows/performance-budget.yml` passed only -the first into the comment step, so the two others were published and never -read. The step's exit code folds all three into one `budget_status`, which meant -the comment could say a budget objected but not which half — a reader had to -open the job log to learn whether the total grew, one chunk grew, or a ceiling -had stopped measuring anything. - -- Both missing verdicts are now passed into the comment step and rendered. -- The healthy comment is unchanged: the breakdown appears only when a half is - not `pass`, verified byte-for-byte against the previous renderer. -- A drifted ceiling (exit 2) reads as a broken **gauge** rather than a size - failure, and no longer claims "nothing was measured" while showing two - ceilings that passed. -- `render-budget-comment.test.ts` now fails if the checker publishes a - `closure_*` verdict the workflow does not wire through, so a fourth half - cannot repeat this. - -The exit-code mapping is untouched: exit 2 still maps to `budget_status=error`, -any other non-zero to `fail`, and `error` still outranks `fail` across all three -halves. diff --git a/.changeset/6235-mergedsort-wrap.md b/.changeset/6235-mergedsort-wrap.md deleted file mode 100644 index 7c90482826..0000000000 --- a/.changeset/6235-mergedsort-wrap.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView` wraps `table.defaultSort` before handing it to a delegated list view, so a -view whose only ordering is the deprecated key actually sorts (objectui#6235). - -`ObjectGridSchema.defaultSort` is declared a SINGLE `{ field, order }` object — the zod -mirror agrees (`z.object({ field, order })`, not a union) — while the `list-view` node's -`sort` slot is declared `string | SortConfig[]`, imported by reference from the spec's own -`ListViewSchema`. `mergedSort`'s last branch forwarded the bare object into that slot -unwrapped. The three branches ahead of it all produce an array or a string, so this was the -one shape the slot never declared. - -Nothing crashed and nothing warned: every reader of that slot drops an unparseable sort -silently. `ListView.parseSortConfig` and `ObjectGrid.parseSchemaSort` both open -`typeof sort === 'string' ? [sort] : Array.isArray(sort) ? sort : []`, so a bare object -yields `[]`; the shared sink `convertSortToQueryParams` returns `undefined` for it. Both -in-tree hosts feed the slot straight into `ListView` (`app-shell`'s `fullSchema` and -Studio's `renderStudioGridList`), so the symptom was an unsorted list with no error — -while the SAME metadata sorted correctly as a grid, because `ObjectGrid` performs this -lowering for the same pair. - -The wrap is verbatim the one the non-grid fetch path in this same file already applies -(`|| (schema.table?.defaultSort ? [schema.table.defaultSort] : undefined)`), so all three -consumers now agree and no fourth dialect is introduced. The shared sink is deliberately -NOT widened to accept a bare `{ field, order }`: that is the widening the maintainer ruling -of 2026-08-22 rejected on the merits, because the same slot legitimately carries -`$orderby`'s own `Record` map, in which `{ field: 'desc' }` is a legal -ordering by a column literally named `field`. - -Precedence is unchanged — a named view's sort still outranks `table.sort`, which still -outranks `table.defaultSort`. Only the final branch changes shape. - -One behaviour note for hosts writing off-schema metadata: an ARRAY in `table.defaultSort` -was previously forwarded verbatim by this path alone and is now lowered like every other -resolver in the repo, which leaves it unreadable rather than rescuing it. That input is -already refused by the zod mirror and already behaves this way on the fetch path and in -`ObjectGrid`; the canonical slot for an array is `table.sort`. diff --git a/.changeset/6238-object-payload-enabled.md b/.changeset/6238-object-payload-enabled.md deleted file mode 100644 index fc618f3cd9..0000000000 --- a/.changeset/6238-object-payload-enabled.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`MetadataService`'s two delete methods no longer PUT a hand-written tombstone. They call -the metadata API's own delete door instead, and the latent `enabled?: boolean` on -`ObjectMetadataPayload` is gone with it (objectui#6238). Object-level member of the -objectui#5761 family, surfaced by the `ObjectSchema` oracle objectui#6223 added to -`scripts/check-designer-field-key-parity.mjs`. - -**What the tombstone actually did.** `deleteObject` and `deleteMetadataItem` wrote -`{ name, enabled: false, _deleted: true }` through `client.meta.saveItem`, i.e. -`PUT /api/v1/meta/:type/:name`. Measured against the installed `@objectstack/spec` 17.2.0 -using `getMetadataTypeSchema` — the registry the framework's own `saveMetaItem` resolves a -PUT's validator from — across all 26 registered overlay schemas: - -``` -ObjectSchema.safeParse({ name, label, fields }) => success = true (control) -ObjectSchema.safeParse({ name, label, fields, isSystem: true }) => success = true (control) -ObjectSchema.safeParse({ name, enabled: false, _deleted: true }) => unrecognized_keys ["enabled","_deleted"] - -25 of 26 registered overlay schemas refuse `enabled` and/or `_deleted` BY NAME - 1 of 26 (`view`) tolerates them; 4 kinds have no registered schema at all - 0 of 26 strip them -``` - -So there were two failure modes, not one. Where the type has a strict schema — `object` -among them — the delete was a hard `422 INVALID_METADATA`, so nothing was ever recorded. -Where the schema is tolerant or absent, the framework stores the request item **verbatim** -(it deliberately persists the body rather than `parsed.data`), and `_deleted` has no reader -anywhere on the platform — so the "soft delete" was a silent no-op that left the item live -carrying two junk keys. Neither outcome deleted or disabled anything. - -**The resolution is a mechanism change, not a rename**, and there was nothing to rename to. -`ObjectSchema`'s 42-key accept set has no on/off flag; the near-spelling `enable` is -`ObjectCapabilities`, a system-features *module object*, so `enabled: false` → `enable: -false` fails on the value where it passes on the name. No wire key was invented: a metadata -soft-delete convention would be a `@objectstack/spec` contract addition, and the platform -does not have one. - -**Both sites now call `client.meta.deleteItem(type, name)`** — `DELETE -/api/v1/meta/:type/:name`, the same request `MetadataClient.reset` issues, which is the -mechanism `MetadataObjectsPage.handleObjectsChange` and `ResourceEditPage` already used for -deletes. Two mechanisms for one operation had disagreed; now there is one. The delete route -is generic over `:type` on the same route family and capability gate as the PUT, so this -holds for every category the generic `deleteMetadataItem` serves, not just `object`. The -doc comment claiming the API "exposes `saveItem` but no dedicated `deleteItem`" was stale: -`@objectstack/client` 17.2.0 declares `meta.deleteItem` on the very client this service -already holds. - -`reset` semantics are the overlay's, and that is the governed answer rather than a -shortfall: it removes the customization row — which *is* deletion for an object the -designer authored — and restores the artifact for one a package declares, an object you are -not allowed to delete. Which of the two an item is, is what the API's own `deletable` / -`resettable` verdicts report, not something a client-side flag should decide. - -**No published type changed.** `ObjectMetadataPayload` is exported from its module but that -module is not re-exported by `packages/app-shell/src/index.ts`, the package's only entry, so -the removed `enabled?: boolean` was never on the published surface and no `**/src/index.ts` -is touched. What consumers *can* observe is behaviour: `MetadataService` is reachable -through the published `useMetadataService()` hook, both method signatures are unchanged -(`Promise`), and the HTTP request they issue changes from a `PUT` with a body to a -`DELETE`. - -The `KNOWN_UNPARSEABLE_KEYS` entry in `scripts/check-designer-field-key-parity.mjs` goes -with the fix — that ledger ratchets in both directions, so an entry left behind for a -resolved key is as red as a missing one. It is now empty for the first time, which is the -ratchet arriving where it was pointed; the self-test's non-vacuity guard moved onto a -fixture accordingly, so an empty ledger reads as success rather than as a demand that some -key stay unresolved. diff --git a/.changeset/6240-object-payload-fields-map.md b/.changeset/6240-object-payload-fields-map.md deleted file mode 100644 index 8262d3150c..0000000000 --- a/.changeset/6240-object-payload-fields-map.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService`'s object writers PUT `fields` as the name-keyed MAP `ObjectSchema` -requires, not an array (objectui#6240). Both of the designer's write paths were affected, -and `saveFields` ran the conversion in the wrong direction outright: the server's own -document arrives with `fields` as a map, and `fields.map(toFieldPayload)` turned it into an -array on every field save. - -Measured against the installed `@objectstack/spec` 17.2.0 and against the framework's own -write door. `ObjectSchema.fields` is a required record: an array — empty or not — is -refused `invalid_type @ fields`, a map parses. `metadata-protocol`'s `saveMetaItem` -resolves metadata type `object` to that same `ObjectSchema`, `safeParse`s the whole item -and throws `422 INVALID_METADATA` **before** persisting, so the array was refused rather -than stripped or stored: every designer object save and every designer field save that went -through this service was a 422 that wrote nothing. - -This is the value-level half of the objectui#5761 parity family and is invisible to that -family's key-name gate — `fields` sits in the accept set under either shape, which is the -gate's own coverage note 4. The pins are runtime assertions on the captured request bytes. - -The conversion refuses, loudly, what it cannot key: a field with a missing or blank `name` -throws instead of writing a `{ undefined: … }` entry (measured: the spec ACCEPTS that -document, so nothing downstream would have caught it), and a duplicate name throws instead -of letting the later field silently replace the earlier — a loss an array does not have. -`saveFields` keeps preserving unknown keys of the fetched server document, which now -actually reaches storage. `saveObject` with no `existingFields` still omits the key rather -than writing `{}`: a PUT is an upsert, so `{}` would delete every field of an object on a -save that only meant to rename it. - -`saveObject(obj, existingFields)` keeps its `FieldMetadataPayload[]` parameter type — the -array is converted inside — so no caller's call site changes. diff --git a/.changeset/6241-fieldgroups-section-heading-pin.md b/.changeset/6241-fieldgroups-section-heading-pin.md deleted file mode 100644 index fce15362ee..0000000000 --- a/.changeset/6241-fieldgroups-section-heading-pin.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Tests only, no release: pin the synth → renderer seam for the default -`fieldGroups` record detail page (objectui#6241). - -`buildDefaultPageSchema` synthesizes one detail section per declared -`fieldGroups` entry and app-shell's `RecordDetailView` renders that output as -the default record page for every object declaring `fieldGroups` with no -assigned page — but nothing asserted end-to-end that those synthesized sections -render their headings. The synthesizer's own suite asserts its return value and -never renders; the `record:details` renderer suites render hand-authored -section fixtures and never consume synthesizer output. Measured on `9ea4cdee3`: -with the consumer's read of the emitted heading removed, all 109 files / 1031 -tests of `packages/plugin-detail/` still passed. - -The new file renders the real `buildDefaultPageSchema` output through the real -registry into the DOM and asserts the declared heading TEXT, its declared -order, and that a group's internal key never stands in for its label. It -asserts no key spelling, so it stays independent of the `title` / `label` -convergence open in objectui#6190 / objectstack#11661. diff --git a/.changeset/6250-toast-demo-shapes.md b/.changeset/6250-toast-demo-shapes.md deleted file mode 100644 index 88aa0717a3..0000000000 --- a/.changeset/6250-toast-demo-shapes.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Docs and fixtures only: the seven `components-feedback-toast/*` and seven -`components-feedback-sonner/*` `SchemaExample` fixtures hung an action object off -`onClick` (`{"type":"button", …, "onClick":{"action":"toast", …}}`) — a shape -`ButtonSchema` declares as a FUNCTION and no dispatcher reads, so all fourteen were a -RED `safeParse` on the envelope and clicking the rendered demo raised no toast -(objectui#6250). They now author the registered spellings the engine already executes, -`type: 'toast'` and `type: 'sonner'`, whose renderers draw their own trigger button and -call sonner's `toast()` from it. `feedback/toast.mdx` and `feedback/sonner.mdx` follow, -including two keys sonner's page taught that neither `SonnerSchema` declares nor its -renderer reads (`duration`, `action`). - -No package source or behaviour change; fixtures, docs and pins only. Two things the -fixture correction deliberately does NOT do, both left to the maintainer: declare an -action union on `ButtonSchema.onClick` and build a dispatcher for it, and give the toast -renderers the in-toast action button and promise form the removed demos implied. diff --git a/.changeset/6269-objectview-slot-key-lists.md b/.changeset/6269-objectview-slot-key-lists.md deleted file mode 100644 index 12c8d4b312..0000000000 --- a/.changeset/6269-objectview-slot-key-lists.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`ObjectViewSchema`'s `table` and `form` slots now ship the members they promise -(objectui#6269). Both were declared by deriving from the schema they document — `table?: -Partial>`, `form?: Partial>` — and both derived types declared **zero** properties. - -`Omit` is `Pick>`, and `keyof T` on a type carrying a string index -signature is `string | number` — the literal member names are absorbed. `ObjectGridSchema` and -`ObjectFormSchema` both inherit `BaseSchema`'s `[key: string]: any` (objectui#5155), so each -`Pick` rebuilt a type holding the index signature and none of the named members. Measured -through the TypeScript checker: `ObjectGridSchema` 61 members, the `Omit` of it 0; -`ObjectFormSchema` 67, the `Omit` of it 0. This is objectui#6151's collapse in *property* -position — #6151's guard walks the `LayoutSchema` union and cannot see properties on -`ObjectViewSchema`. - -Nothing errored, because the index signature answered every key as `any`. The visible costs -were the ones only a reader of the declaration meets: `table: { colunms: 3 }` type-checked, -`table: { pageSize: 'ten' }` type-checked, and editor completion inside `table: { … }` offered -nothing at all for a slot documented as "inherits from ObjectGridSchema". - -Each `Omit` is now a `Partial>` over an explicit key list — 59 keys for `table`, 64 for -`form`, i.e. every declared member minus the identity keys the view itself fixes. `Pick` with -literal keys never computes `keyof T`, so it cannot collapse the same way. The key lists are -pinned against silent drift by `packages/types/src/__tests__/object-view-slot-key-lists.test.ts`, -which recomputes each source schema's declared members through the TypeScript checker and -requires set equality; a member added to `ObjectGridSchema` and not to the list turns it red. - -**Tightening, deliberately.** Restoring named members re-enables excess-property checks on -object literals assigned into these two slots, so a misspelled key there is now an error -instead of silently doing nothing. That is the intent of the fix. The slots' member *types* are -unchanged — every key that resolved to a real declared type before still does. - -The `Pick` lists exist only because `BaseSchema` carries a root string index signature. When an -objectui#5155 phase removes it, `Omit` stops collapsing and the lists (plus their pin) become -removable; the pin's own comment records the condition, and one of its assertions is the -tripwire that will notice. diff --git a/.changeset/6271-kanban-fetch-gate.md b/.changeset/6271-kanban-fetch-gate.md deleted file mode 100644 index dd74c6c0bb..0000000000 --- a/.changeset/6271-kanban-fetch-gate.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -`ObjectKanban` no longer queries twice on mount (objectui#6271). A standalone board issued -its first `find` before the object definition resolved — so `buildExpandFields` saw no -fields and that query carried no `$expand` at all — then issued a second, expanded one once -the definition landed: - -``` -['deal', { $top: 100 }] -['deal', { $top: 100, $expand: ['owner'] }] -``` - -The definition now GATES the query instead of refining it afterwards: one query per mount, -carrying the expansion the first time. - -Decided on measurement rather than on the two shapes' relative appeal. The first response -never reached the screen in the regimes that matter: with the schema resolving no slower -than the row query (profiles schema/find = 30/30, 30/60, 5/30 ms), the definition lands -first, the effect re-runs, its cleanup flips `isMounted` false, and the unexpanded rows are -discarded on arrival — a DOM probe polling every 2ms for a title only that response carried -never fired once. What the gate costs is one schema resolution ahead of the query, and that -read is cheap and shared: one small GET behind the same discovery call `find` already -awaits, served thereafter from `MetadataCache` (5-minute TTL, concurrent readers coalesced -onto one request). Measured against the real `ObjectStackAdapter` over loopback HTTP, 22 -reads of one object produced exactly one metadata request and every read after the first -returned in 0.01ms. End to end the board is not slower for it — same harness, before → -after, time to the fully populated board: 156.9 → 145.2ms (30/30), 119.8 → 110.6ms (30/60), -54.7 → 52.4ms (5/30). - -The gate is on the definition read having **settled**, not on the definition being truthy: -an adapter that exposes no `getObjectSchema`, and a read that throws, both settle with -nothing to report and the board falls through to an unexpanded query rather than waiting -forever. Boards fed rows by a parent (`data`, `bind`, inline `schema.data`) are untouched — -they never ran this effect, and they still read the definition for lane titles and labels. - -The `isOpaqueId` suppression in the card-description path is unchanged and keeps its -comment beside it: part of what it hid was this fetch ordering, but unexpanded rows still -reach it from parents that pass rows they fetched without `$expand`, from author-supplied -data, and from backends that decline an expansion. diff --git a/.changeset/6278-context-menu-item-icon.md b/.changeset/6278-context-menu-item-icon.md deleted file mode 100644 index 080e0bf78f..0000000000 --- a/.changeset/6278-context-menu-item-icon.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@object-ui/components": patch ---- - -`ui:context-menu` now resolves a menu item's authored `icon` to a glyph. It previously never read the key at all. - -Both arms of `renderContextMenuItems` — the leaf `ContextMenuItem` and the `ContextMenuSubTrigger` — ignored `icon` entirely, so an item authored as `{ "label": "Copy", "icon": "copy" }` drew its label and nothing else. The name is now resolved through `resolveIcon`, the same lucide **record** surface `ui:button`, `ui:dropdown-menu` and the `action:*` family already resolve against: a live name draws its glyph, and an unknown or retired spelling draws nothing rather than degrading to a wrong glyph. This mirrors the repair `ui:dropdown-menu` received for the identical defect. - -The `components-overlay-context-menu/basic-context-menu` catalog fixture already declared four live names — `copy`, `scissors`, `clipboard`, `trash` — which drew nothing before this change and draw their glyphs now. Those names are also brought under `check:lucide-icon-record-names` by a new `context-menu` census entry, so a future retired spelling fails the gate instead of silently drawing nothing. diff --git a/.changeset/6280-scan-surface-claims.md b/.changeset/6280-scan-surface-claims.md deleted file mode 100644 index e4cdf7472e..0000000000 --- a/.changeset/6280-scan-surface-claims.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Doc-only fixes in `@object-ui/components` and the docs site: two stale claims about -`scripts/check-doc-links.mjs`'s scan surface, both frozen at an earlier `SCAN_ROOTS` -shape (objectui#6280). - -- `packages/components/src/__tests__/readme-shadcn-sync-categories.test.ts`'s - `## Scan surface` docblock said `README_SHADCN_SYNC.md` had "never been scanned by - anything" — false since objectui#4938, whose `packages/*` row excludes only the - basenames `README.md`/`CHANGELOG.md` and so does include this file. Rewritten to - argue from the current tree: the file IS scanned, but check-doc-links only inspects - `[text](href)` markdown-link syntax (never the backticked code spans this README - uses for every in-repo path) and has no notion of prose-vs-manifest consistency — - so the hand-rolled checks below survive regardless of scan surface, for reasons - unrelated to whether the surface reaches this file. -- `content/docs/guide/ci-cd-pipeline.md` described the scan surface twice (prose and - the two-link-checkers table), both frozen at the objectui#3622 shape ("the internal - `docs/` tree and every package `README.md`"). Re-derived from the live `SCAN_ROOTS` - table (17 rows) and rewritten to include the app READMEs and root-level markdown - (objectui#4148), the rest of each package/app directory tree (objectui#4938), and - every nested `README.md` (objectui#6026). - -No source or behaviour change; text and a test docblock only. diff --git a/.changeset/6285-capability-labels-derive-from-spec.md b/.changeset/6285-capability-labels-derive-from-spec.md deleted file mode 100644 index 5c5c1affa0..0000000000 --- a/.changeset/6285-capability-labels-derive-from-spec.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/i18n': patch ---- - -The capability picker localizes `manage_sharing` (objectui#6285). Before this, "Manage -Sharing" was the one platform capability in `sys_permission_set`'s picker that rendered in -English in every locale, beside seven siblings that translated — a user-visible missing -translation, in all ten packs at once. - -The cause was an unchecked copy. `CURATED_CAPABILITY_LABELS` in -`CapabilityMultiSelectField.tsx` listed seven capability names under a doc comment claiming -it mirrored `@objectstack/spec/security`'s `PLATFORM_CAPABILITIES`; the spec grew an eighth -member and the list did not follow, so `manage_sharing` fell through to the English label -the `sys_capability` registry serves. Nothing could catch it: the i18n gate reads that list -as this key family's vocabulary and checks the members it names — all seven had keys — and -no instrument compared the vocabulary to the array it was named after. - -`capability.label.manage_sharing` is now authored in all ten packs and in the field widgets' -provider-less defaults map, the list carries the member, and the prose claim is replaced by -a check: `CapabilityMultiSelectField.specParity-6285.test.tsx` imports `PLATFORM_CAPABILITIES` -and fails on any difference in either direction, reading the declaration through the i18n -gate's own source reader so what it pins is exactly what that gate consumes. `labelFor` also -gains a `defaultValue`, so a capability that arrives in a future spec bump before its -translation is authored degrades to the registry's English label rather than rendering a raw -i18n key at the user. diff --git a/.changeset/6286-explain-batch-cap-from-spec.md b/.changeset/6286-explain-batch-cap-from-spec.md deleted file mode 100644 index 67e18ef880..0000000000 --- a/.changeset/6286-explain-batch-cap-from-spec.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -The batch-explain cap the row-verdict hook paginates under is now imported from -`@objectstack/spec/security` instead of re-declared locally (objectui#6286). -`useRecordCrudVerdicts` carried `const EXPLAIN_BATCH_MAX_RECORD_IDS = 200`, a hand copy of -a SERVER contract constant, under a doc comment that named its own expiry condition: the -pinned `@objectstack/spec@17.0.0-rc.6` predated the batch form, and the pin bump would -supersede the declaration. It has. - -**No value changes and no behaviour changes.** The spec exports `200`, which is what the -local copy said, verified by resolving the installed package and reading the export — both -statically (`dist/security/index.d.mts`) and at runtime through the same specifier the -source now uses. What changes is reference identity: if the server relaxes or tightens the -cap and the spec follows, the client follows too, instead of paginating at the old boundary -with no signal anywhere. The cap's whole point is that an over-cap request is refused with -`400 VALIDATION_FAILED` rather than truncated, so a client that silently disagrees with it -is exactly the drift `scripts/check-spec-symbol-derivation.mjs` argues about — and could -not catch here, because both of its scanners skip non-exported declarations and this const -was module-local (objectui#5899). - -The declared floor already carries the symbol, so no range moves: `@objectstack/spec@17.0.0` -— the minimum `^17.0.0` admits — exports `EXPLAIN_BATCH_MAX_RECORD_IDS = 200` from -`./security`. Measured against the published tarballs of `17.0.0-rc.6`, `17.0.0`, `17.1.0` -and `17.2.0`: only the rc lacks it. The declaration was therefore expired one release -earlier than the card that found it assumed. - -The half of the comment that explains *why* the cap exists and what the server does with an -over-cap request is kept and now sits on the import; only the half explaining why it was -declared LOCALLY is gone, since that is the part that stopped being true. - -Covered by a new reference-identity test rather than a value assertion. Every assertion on -`200` passes on both sides of this change — a ghost — so -`useRecordCrudVerdicts.batchCap.test.tsx` stands the spec module in at a cap no hand copy -could produce and asserts the request chunking follows it, with a control case proving the -stand-in installed and differs from the shipped value. The pre-existing cap assertion in -`rowRecordCrudVerdict.test.tsx` now derives its fixture and its bound from the same export -instead of re-typing `200`, which removes the last hand copy on this surface without -pretending to be a two-world test. diff --git a/.changeset/6287-flownode-description-key.md b/.changeset/6287-flownode-description-key.md deleted file mode 100644 index e14fbd707d..0000000000 --- a/.changeset/6287-flownode-description-key.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The flow designer's node inspector no longer offers a **Description** field, and strips a -stored `description` off a node the first time an author edits it (objectui#6287). - -`FlowNodeSchema` is `.strict()` (objectstack#4001) and refuses that key by name — measured -on the installed `@objectstack/spec@17.2.0`: - -``` -FlowNodeSchema.safeParse({ id, type, label, description, config }) - -> unrecognized_keys: ["description"] -``` - -By this package's own reading of that mechanism, the cost is not untidiness but an -unsavable draft: the key "surfaces as `unrecognized_keys` in the live client validation and -as a 422 on save" (`flow-canvas-layout.withCanonicalGeometry`, on the identical retired `ui` -case). So the field was not merely describing a shape the contract refuses — it was -producing one, on every keystroke, and nothing anywhere read the value back. The spec's flow -node has eleven keys and no note key of any spelling, so there was no reader to grow into. - -Stored flows heal on the author's first edit, the same migrate-on-write boundary the retired -`ui` geometry gets, and for the same reason: with the field gone there would otherwise be no -way left to clear a `description` an author had already saved. - -The three hand-written copies of the node and edge shapes that let this drift go unseen are -now one declaration each — `FlowNodeInspector`'s node and edge types alias the canonical -`FlowNodeLike` / `FlowDesignerEdge`, and `flow-decision-edges`' fourth edge copy aliases the -same canvas edge instead of restating it with a `condition?: unknown` that had already -outlived objectui#3202's narrowing by months. diff --git a/.changeset/6293-conditionbuilder-reference-value.md b/.changeset/6293-conditionbuilder-reference-value.md deleted file mode 100644 index 1bee446a33..0000000000 --- a/.changeset/6293-conditionbuilder-reference-value.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ConditionBuilder`'s row mode now compiles a value that is plainly a **reference** as one, -instead of quoting it into a string literal (objectui#6293). - -`fmtValue` quoted anything that was not a number / `true` / `false` / `null`, and the value -box is free text — so an author building "this field differs from its prior value", the idiom -that *defines* a change-detection predicate, got `previous == 'previous.status'`. That is -syntactically valid CEL, `previous` is a declared root, and a string literal's contents are -deliberately not scanned for references by `flow-ref-check` or by the server-side validator. -The predicate parsed, registered, evaluated — and was always false, with no author-time signal -at any layer, at all five surfaces that mount this builder. - -A value matching a **declared root prefix** now emits as the reference: `record`, `previous`, -`parent`, `user`, `current_user`, `org`. That set is this builder's own vocabulary — `record` -/ `user` / `org` are exactly what its subject dropdown offers one control to the left, -`previous` and `parent` are bound by `evalFieldPredicate` and by the server-side hook / -validation evaluators, and `current_user` is the ADR-0068 spelling of the same identity object -`user` names. Roots this builder never offers (`data`, `os`, `app`, `features`, `input`, -`vars`, `page`) are deliberately excluded: `data.csv` is a plausible literal and `data` *is* -bound, so capturing it would trade one silently-false predicate for another rather than for a -loud one. Declaring which roots a mounting surface actually binds is caller-supplied -vocabulary and belongs to objectui#6296. - -The test is "a dotted path under a declared root", not "contains a dot" — a version string -(`1.2.3`), a filename, and a path under an unbound root all stay literal text. The literal and -number controls are unchanged: `done` still compiles to `'done'`, `42` still to `42`. - -**Nothing already stored is rewritten.** A persisted `previous == 'previous.status'` no longer -round-trips byte-for-byte, so the builder's existing safety rule hands it to the raw CEL editor -rather than reinterpreting it — the author sees both readings and decides. In the other -direction a hand-authored `record.status != previous.status` now round-trips *into* the row -builder, which it could not before. - -The repair is at the authoring surface, where the ambiguity is: no consumer-side tolerance is -added, and the emitted reference is now an identifier the existing reference checkers can see, -where a string literal's contents were invisible to them. diff --git a/.changeset/6301-packaged-automation-setup-page.md b/.changeset/6301-packaged-automation-setup-page.md deleted file mode 100644 index f8b9ddb079..0000000000 --- a/.changeset/6301-packaged-automation-setup-page.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Setup gains a **Packaged automation** page — the operational surface for the flows an -installed package ships (ADR-0126 §7.4, objectui#6301). Reached the way every other -framework-contributed Setup surface is: the page registers the component-registry ref -`automation:packaged`, so app navigation names the ref and `ComponentNavView` resolves it -at `/apps//component/automation/packaged`. No bespoke route is added — a second way -in would be a URL the app metadata does not know about. - -Per packaged flow the page does exactly two things: - -- **on/off for this scope** — reads the activation state the engine reports - (`GET /api/v1/automation/_status`, backed by the ADR-0126 §7.2 `sys_metadata_activation` - ledger) and flips it through `POST /api/v1/automation//toggle`; -- **clone** — `POST /api/v1/automation//clone` with a mandatory new machine name and - label (§7.1). The carried-over definition is never offered as editable form fields; the - copy is edited in Studio like any other flow. - -Authoring stays in Studio. The list is scoped to packaged flows by the server's own -three-clause provenance test (`isCodeArtifactBody`, ADR-0029 D9.6) rather than the -`_packageId`-only shortcut, which classifies a tenant overlay bound to a package as -packaged — the cloud#970 misread, and here it would put a tenant's own flow behind an -install-wide switch. - -**Server refusals reach the operator verbatim** — no client-side softening or rewording. -Three shapes are relayed as sent: the §5 posture gate (403 `PERMISSION_DENIED`, whose -message names the tenancy posture *and* the sanctioned clone path), the §7.3 subflow guard -(409 `DELETE_RESTRICTED`, which names the packaged callers that would break mid-run — a -list nothing on the client could reconstruct), and the §7.1 clone name conflict (409). - -⛔ **No drift or ancestry surface** (§9): no diff-vs-base, no "customized" badge, no -base-moved notice, no link from a clone back to its source. Cloned-without-disabled and -disabled-without-clone are ordinary states, shown plainly. Tests pin the absence, including -the case where a response carries a `clonedFrom` key anyway — the platform does not track -that lineage, so a page that displayed it would be displaying something it invented. - -`@object-ui/i18n` gains the `packagedAutomation` key group — 24 keys in `en` and real -translations in all nine other packs, matching each pack's existing vocabulary for "flow" -and "package" (zh 流程/软件包, ja フロー/パッケージ, ko 플로우/패키지, de Flow/Paket, -fr Flux/Package, es Flujo/Paquete, pt Fluxo/Pacote, ru Поток/Пакет, ar التدفق/الحزمة) and -each one's quotation convention. The group deliberately carries no drift or ancestry -wording, and no server refusal text: those arrive as server-authored prose and are -rendered verbatim. diff --git a/.changeset/6306-action-icon-type-resolution.md b/.changeset/6306-action-icon-type-resolution.md deleted file mode 100644 index 741c32c49f..0000000000 --- a/.changeset/6306-action-icon-type-resolution.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/components': patch ---- - -An `action:icon` hosted by an `action:bar` now reaches its handler. It forwarded the -COMPONENT id as the action type, so the click resolved nothing at all — no error, no -toast, a button that silently did nothing (objectui#6306, the objectstack#2169 "Mark Done -does nothing" shape). - -`action:bar` does not route members through `SchemaRenderer`. It pulls each member's -renderer off the registry and RENAMES the declared type as it spreads it onto the child: -`type` becomes the component id (`'action:icon'`) and the real declaration moves to -`actionType`. `action:button` has always resolved that pair when it forwards -(`schema.actionType || schema.type`); `action:icon` read `schema.type` alone and dropped -`actionType` entirely. `ActionRunner.execute` resolves its handler from -`action.type || action.actionType || action.name`, and `'action:icon'` binds no registered -handler and no builtin — for a declaration carrying `target` rather than `endpoint` it does -not reach the legacy `navigate`/`api` fallback either, so it fell through to -`executeActionSchema` and the authored action never ran. - -**The bug was a function of the layout, not the declaration.** One authored action executed -or did nothing depending on which `component` the host picked for it — the same asymmetry -objectui#5493 fixed on this renderer for `onSuccess`, one key over. - -`check:action-forward-parity` could not have caught this and its green run was never -evidence: `type` **is** in the forward whitelist, and that gate diffs key PRESENCE against -the owed set. This is a wrong-VALUE defect behind a present key, a class the gate has no -opinion on by construction. The existing icon coverage could not catch it either — it -rendered `action:icon` bar members three times and asserted only `visible`/`enabled`, never -that a click reached a handler, which is exactly how this shipped. - -Pinned by `action-bar-member-type-resolution.test.tsx`, which executes clicks rather than -inspecting props. Every row that reads the icon member's zero renders a sibling -`action:button` member of the SAME declaration in the SAME bar and reads its one first, so -a zero cannot be "the harness never executed anything". One row registers a trap handler -keyed on the component id, making the unfixed behaviour a positive artefact (the trap -fires) rather than only a missing call. A standalone row stays green in both worlds on -purpose: it refuses a "fix" written as `schema.actionType` alone, which would trade this -defect for its mirror image on the surface where `type` IS the action type. - -Scope is this one renderer. `type: schema` appears in exactly two files under -`renderers/action/` — `action-button.tsx` (already correct) and `action-icon.tsx`; -`action:group` and `action:menu` compose their members differently and are untouched. diff --git a/.changeset/6307-bold-version-literals.md b/.changeset/6307-bold-version-literals.md deleted file mode 100644 index 9b3e4f62ca..0000000000 --- a/.changeset/6307-bold-version-literals.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Docs and gate change; no published surface. - -Two consumer-facing guides stated Node/pnpm floors this project neither declares nor -tests: `content/docs/guide/quick-start.md` carried `**Node.js** 20+` / `**pnpm** 9+ or -npm/yarn`, and `content/docs/guide/building-crud-app.md` the same pair on one line. Both -pages address the reader's OWN project, so the root `engines` field never governed them, -and no published package supplies a floor either — of the 46 workspace manifests, zero -declare `engines.node` or `engines.pnpm` (the only `engines` block outside the root is -`packages/vscode-extension`'s `engines.vscode`). The pages now state what is measurable -instead: the packages are built and tested on Node 22.x with pnpm 10.x — 26 of the 27 -`node-version:` declarations in `.github/workflows` read `'22.x'` and the 27th reads -`'22'`; the root `packageManager` field is `pnpm@10.31.0`, which is what `corepack enable` -hands every CI job. Worded as what CI exercises, not as a requirement the project has not -measured. - -The reason nothing objected: `scripts/__tests__/doc-version-claims.test.ts` scans -`content/docs` for exactly this, and the `SEP` character class between a toolchain name -and its version admitted backticks, quotes, whitespace, colons, commas, pipes and brackets -— but not `*`. So `**Node.js** 20+` never matched `TOOLCHAIN + SEP + VERSION`, and the -ratchet reported green over four literals it had never examined. `SEP` now admits `*` and -`_`; measured over the 241 files the three scan roots resolve to, the corpus goes from 33 -matched literals to 37 — exactly those four, none lost. None of the four was ledgered: -they were deleted, which is what the gate's own failure message asks for when a literal -restates no manifest and no lane tests it, and the sentences replacing them are inventoried -as `anchored` entries naming the anchor each can be re-measured against. - -A new fixture test keeps the widening measurable now that the repaired corpus carries no -emphasised claim at all, and pins the one boundary it does not cross: `_Node.js_ 20+` stays -invisible, because `_` is a word character and the `\b` on each side of the toolchain name -therefore fires on neither side of it (measured: zero such spellings in the corpus today). - -objectui#6307. diff --git a/.changeset/6308-living-workflow-example.md b/.changeset/6308-living-workflow-example.md deleted file mode 100644 index 25ffccc64a..0000000000 --- a/.changeset/6308-living-workflow-example.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Docs change; no published surface. - -`content/docs/guide/ci-cd-pipeline.md`'s "Adding a New Workflow" section told a -contributor to "follow the existing pattern for pnpm + Turbo setup" and then gave a copied -YAML block in which every line had drifted: `actions/setup-node@v4` where every workflow -in this repository now uses `@v7`, a hardcoded `node-version: 20` where every workflow -declares `'22.x'` (and 20 sits below the floor the root `package.json`'s `engines` field -now declares), and `pnpm/action-setup@v4`, which no workflow here has ever used — pnpm -comes from `corepack enable` plus the root `packageManager` field instead. Re-measured on -this change's own HEAD: `actions/setup-node@v7` — 28 occurrences, no other version; -`node-version:` — 27 `'22.x'` and 1 `'22'`; `pnpm/action-setup` — 0. - -A copied block is a fossil by construction, so the section now points at -`readme-exports.yml` as a living example instead of repeating one: it is short, runs on -every pull request, and its setup is the complete pattern most new build/test/lint -workflows need (checkout, `corepack enable`, `actions/setup-node` with pnpm's cache, -`pnpm install --frozen-lockfile`, then a `turbo run build` step). Only the two steps that -hold regardless of which Node or pnpm version the repository is on — the checkout step and -`corepack enable` — stay quoted on the page; the reader copies the version-specific steps -from the workflow itself. No new version literal was introduced, so no -`doc-version-claims.test.ts` `KNOWN_CLAIMS` entry was needed. - -objectui#6308. diff --git a/.changeset/6309-delete-object-fields-bridge.md b/.changeset/6309-delete-object-fields-bridge.md deleted file mode 100644 index 18804b6669..0000000000 --- a/.changeset/6309-delete-object-fields-bridge.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/types': patch -'@object-ui/fields': patch ---- - -Delete the dead `metadata-admin/previews/object-fields-bridge.ts` module, and the three -prose references that still described it as wired. - -The module exported `bridgeFromDraft`, `commitToDraft` and `FieldsBridgeResult` and had -**zero importers** — re-measured on the merged base, not inherited from the filing. Nothing -in the repository could reach it either: `@object-ui/app-shell`'s `exports` map declares -only `.` and `./styles.css`, so the file was not addressable as a deep import even from -outside the workspace. - -Removing it is not the whole change. Three comments — in `types/src/designer.ts`, `types`' -`designer-field-types.test.ts` (twice) and `fields`' `richtext-cell-renderer-5452.test.tsx` -— cited the bridge as a live corroborating source. Left behind, they would have swapped -dead code for false documentation: three in-repo pointers telling a future reader that this -bridge mediates between the framework field record and `FieldDesigner`, and nothing telling -them it is unreachable. The two that named it as the consumer deriving an editable-subset -check from `DESIGNER_FIELD_TYPES` now name `MetadataFieldsPage`, which does exactly that -with the same idiom and the same `objectui#3017` anchor. The third cited the bridge's -`richtext` → `html` mapping as one of three corroborations that `richtext` stores HTML; the -other two (the showcase seed and the field-type decision tree) are live and carry the point -on their own, so that clause is dropped rather than repointed. - -No behaviour changes: nothing imported the module, so there is nothing to migrate. diff --git a/.changeset/6310-designer-formula-key-retired.md b/.changeset/6310-designer-formula-key-retired.md deleted file mode 100644 index 905322ab4c..0000000000 --- a/.changeset/6310-designer-formula-key-retired.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/plugin-designer': patch ---- - -`appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten -locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310). - -objectui#6043 retired the Field Designer's formula-expression textarea, which was the -key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label: -t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in -eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in -`packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner > -fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`. - -Removed under objectui#4658's evidence standard, re-measured on this branch rather than -inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could -reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its -sole textual occurrence anywhere in the repo was the defaults-map row this change removes -with it — so the key goes from NEEDS-REVIEW to no footprint at all. - -The map and all ten packs move in one commit, which is what keeps -`defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack -lacks, and `all-locales-key-parity` fails a pack left behind. - -Not touched: `designer.field.formula` (`'Formula (CEL)'`) in -`packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging -to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula -expressions. - -`packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the -removal by name, following the four prior retirements (objectui#4145, objectui#4392, -objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can -see a dead key come BACK into the packs: the reverse sweep that found this one is report-only -by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and -`check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted — -reviving the row in all ten packs turns exactly that one case red, naming each pack, while the -parity gate and the defaults-map mirror stay green. diff --git a/.changeset/6313-quick-reference-node-floor.md b/.changeset/6313-quick-reference-node-floor.md deleted file mode 100644 index c67eeeddbc..0000000000 --- a/.changeset/6313-quick-reference-node-floor.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Tooling-only fix (objectui#6313): `QUICK_REFERENCE.md`'s "Current Release" pin -(`scripts/__tests__/quick-reference-current-release-4143.test.ts`) and the sync -generator (`scripts/sync-quick-reference-release.mjs`) derived the Node/pnpm -version floors with `match(/(\d+)/)?.[1]`, which keeps only the LEADING integer -group. Against root `engines.node: ">=22.11"` (objectui#5306 / PR #6311) that -produced a floor of `22`, silently discarding the `.11` — so a row reading -exactly `≥ 22` passed a pin whose whole premise is disagreeing with its own -cited anchor. Both derivations now strip the comparator and keep the WHOLE -version string; `pnpm quick-reference:sync` regenerated the Node.js row to -`≥ 22.11`. Added regression coverage pinning that a `≥ 22`-shaped row is now -rejected and that a `≥ 220`-shaped row still is (objectui#4913), and corrected -the test file's docblock, which had gone stale on the same anchor one decimal -place up. No published package source changed. diff --git a/.changeset/6316-docs-route-eager-closure-gate.md b/.changeset/6316-docs-route-eager-closure-gate.md deleted file mode 100644 index d39e34ce12..0000000000 --- a/.changeset/6316-docs-route-eager-closure-gate.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Tooling-only change; no published behaviour changes. The docs route's eager closure now has -an instrument. `check:eager-closure` reads `apps/console/dist/eager-closure.json` and weighs -the console, so the budget objectui#4616 set over `/docs/[[...slug]]` — the route every one -of the docs pages shares, and the one `apps/site/app/components/registerCatalogBlocks.ts` -adds side-effect imports to — was governed by nothing, and its only measurement was -reconstructed by hand, once. `pnpm check:docs-route-closure` weighs it structurally instead -of in bytes (objectui#6316, triage ruling shape 2, so no docs-site build in CI): every -package the registrar names must be already reachable from that route's static module graph -— a declaration and no payload, which the gate proves for `@object-ui/plugin-form` and -`@object-ui/plugin-grid` through `@object-ui/plugin-view` — or recorded in the script's -`MEASURED_PAYLOAD` with what it is for. Anything else is a genuinely new graph, and it fails -so that a human argues for it in review. diff --git a/.changeset/6320-check-nested-dist-ignore.md b/.changeset/6320-check-nested-dist-ignore.md deleted file mode 100644 index 9f99ca1fc5..0000000000 --- a/.changeset/6320-check-nested-dist-ignore.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -Fix `objectui check` scanning build output because its ignore list only excluded a -root-level `dist/` / `node_modules/` (objectui#6320). - -`packages/cli/src/commands/check.ts` passed `ignore: ['node_modules/**', 'dist/**', -'.git/**']` to `globSync`. `glob` matches `ignore` patterns against the path relative to -`cwd`, so an unanchored `dist/**` / `node_modules/**` excludes only a directory of that -name at the scan root — every nested `packages//dist/`, `examples//dist/`, -`apps//dist/` (and their `node_modules/`) was still scanned. In a built workspace -this means `objectui check` re-reads the author's own schemas a second time from build -output, roughly doubling every count it reports (measured on this repository: 617 → 1047 -files globbed after a full build) with nothing in the output explaining why. - -The ignore patterns are now anchored at every depth (`'**/dist/**'`, `'**/node_modules/**'`), -matching the fix's stated intent: exclude build output and installed dependencies wherever -they live, not only at the project root. A root-level `dist/` / `node_modules/` remains -excluded, unchanged. - -Confirmed before widening: no example, template, or docs fixture in this repository -authors a schema under a directory literally named `dist` — the widened pattern excludes -only generated content. diff --git a/.changeset/6321-changeset-guard-self-coverage.md b/.changeset/6321-changeset-guard-self-coverage.md deleted file mode 100644 index 25123a3f24..0000000000 --- a/.changeset/6321-changeset-guard-self-coverage.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Internal CI-only change: `changeset-guard.yml`'s `paths:` filter now includes its own -YAML and `scripts/check-changeset-no-major.mjs`, so a change to the gate is exercised by -the PR that makes it instead of by the next unrelated `.changeset/**` PR (objectui#6321). -`content/docs/guide/ci-cd-pipeline.md` is updated to match. No published package changed. diff --git a/.changeset/6329-actionparam-one-authority.md b/.changeset/6329-actionparam-one-authority.md deleted file mode 100644 index ac3ed137ee..0000000000 --- a/.changeset/6329-actionparam-one-authority.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ActionParam` has one authority again (objectui#6329). The name was declared three times, -not twice as the card counted: `@object-ui/types` publishes it — derived from the spec's -`ActionParamSchema` input, with its own parity suite — and `ActionDefaultInspector.tsx` and -`ActionPreview.tsx` each carried a module-local `interface` of the same name. app-shell -already read the published one elsewhere (`utils/resolveActionParams.test.ts`), so both -locals were shadows. They are deleted, not reconciled against each other, under the -2026-08-25 family ruling 甲A1. - -Neither shadow needed a member the published type lacks, so nothing was added to the -published surface. What the shadows got wrong was the DECLARATION, in the direction that -lets wrong metadata compile: - -- The inspector's copy carried `[k: string]: unknown`. An index signature admits every key - at type `unknown`, so a commit of a key `ActionParamSchema` rejects by name — `.strict()`, - and `referenceTo` is listed in its alias map — type-checked here and failed on save. It - also made the two copies look compatible when they were describing different authoring - surfaces: `options` / `helpText` / `defaultValue` were declared outright on one side and - swallowed as `unknown` on the other. -- The preview's copy declared `label?: string | { en?: string }`, admitting the `en` tag and - no other, while its own `localize` helper has always read `Object.values(o)[0]`. An inline - locale map keyed `fr-FR` rendered correctly and failed `tsc`. The published `I18nLabel` - admits both authorized forms, so the type now matches what the code already did — this - widens the declaration, not the runtime's acceptance. - -Two behaviour-visible consequences, both of them the local `type?: string` being withdrawn -in favour of the published `ResolvableParamFieldType` (the spec's 49-member `FieldType` plus -objectui's three declared param aliases): - -- `ActionPreview.renderFieldMock` no longer branches on `long_text` or `integer`. Neither is - in that vocabulary — `long_text` belongs to the console form-builder dialect and `integer` - to JSON Schema — so a param spelled either way is a parse rejection on the server and - could never have reached the preview. The two comparisons compiled only because the local - copy typed `type` as `string`. -- The inspector's param-type dropdown narrows its commit through the runtime witnesses - `@object-ui/types` exports (`ACTION_PARAM_FIELD_TYPES` + `OBJECTUI_LOCAL_PARAM_FIELD_TYPES`) - rather than writing the raw DOM string. An unrecognised spelling clears the key instead of - being written into metadata the server would refuse; the eight offered spellings are - unaffected, and are now checked against the vocabulary at compile time. diff --git a/.changeset/6332-explain-request-adoption.md b/.changeset/6332-explain-request-adoption.md deleted file mode 100644 index b6952b279c..0000000000 --- a/.changeset/6332-explain-request-adoption.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Type-only: `useRecordCrudVerdicts` now builds its `POST /api/v1/security/explain` -body against `@objectstack/spec/security`'s `ExplainRequest` instead of an -untyped object literal, and `RecordCrudOperation` is now a *declared* subset of -the spec's eight-verb `ExplainOperation` rather than a coincidental one. - -No release: `satisfies` and the subset wrapper are erased at compile time, the -emitted JavaScript is unchanged, and `RecordCrudOperation` is not part of -`@object-ui/plugin-grid`'s public `.d.ts` (verified — it does not appear in -`dist/index.d.ts`). - -What it buys is a class of compile errors the untyped literal accepted: -a renamed or mis-cased request key (`recordIDs`, `objectName`) and a verb the -explain API does not accept are now `tsc` failures at the call site instead of -a `400 VALIDATION_FAILED` — or, for a mis-cased `recordIds`, a request the -server reads as "no ids at all". Pinned at compile time in -`useRecordCrudVerdicts.explainRequest.test.ts`. - -The narrowing at the heart of the hook is deliberately preserved: the two kebab -verbs stay written out locally, so an upstream release that adds a ninth verb -cannot widen what this list asks about. Only the request side is adopted — the -response stays `unknown` on purpose, because asserting the spec's entry type -would make the hook's fail-open runtime guards look like dead code. diff --git a/.changeset/6335-starred-apps-nav-exclusion.md b/.changeset/6335-starred-apps-nav-exclusion.md deleted file mode 100644 index d56cbe2400..0000000000 --- a/.changeset/6335-starred-apps-nav-exclusion.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`StarredApps` now filters out `nav`-type favorites before rendering (objectui#6335). - -`FavoritesProvider` has always documented `nav` favorites (sidebar entries pinned via -the in-tree pin toggle) as "Excluded from Home/Starred and from the generic sidebar -Favorites list so it doesn't render twice" — but `StarredApps` filtered nothing by type, -so a `nav` favorite handed to it rendered anyway. `FavoriteItem['type']` also has no -`nav` key under `home.recentApps.itemType.*` (only five of its six members do), so a -`nav` card that reached `StarredApps` fell through to the raw `"nav"` label instead of a -translated one. - -`StarredApps` now filters `items` to `type !== 'nav'` before rendering — the same -exclusion already applied to the sidebar Favorites list in `AppSidebar.tsx` and -`UnifiedSidebar.tsx` — restoring declared-equals-enforced behaviour and making the -missing locale key correct by construction. No locale packs were touched, and the -`FavoriteItem` / `RecentItem` union types are unchanged. diff --git a/.changeset/6336-changeset-overwrite-guard.md b/.changeset/6336-changeset-overwrite-guard.md deleted file mode 100644 index 4dd585854d..0000000000 --- a/.changeset/6336-changeset-overwrite-guard.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Internal CI-only change: a new report-only `Changeset Overwrite Report` job in -`changeset-guard.yml` names any `.changeset/*.md` a change modified or deleted -without having added it, and prints the release declaration that was there. - -No published package changes, so this releases nothing. - -objectui#6336 — a hand-picked `changesets`-style filename can land on one that -already exists, and the overwrite is silent in both directions that should catch -it: `git status` reports ` M` rather than `??`, and a deleted release declaration -is flagged by nothing downstream. The cost lands on a third party — whichever -earlier pull request's declaration disappears — and surfaces only when a package -fails to bump. - -Report-only is measured, not cautious: across all 5281 first-parent commits on -`main`, every one of the 19 modifications of a pre-existing changeset was -legitimate, so a blocking gate would have failed all of them. -`OS_CHANGESET_OVERWRITE_ENFORCE=1` flips it for whoever revisits that with a new -measurement. diff --git a/.changeset/6342-dead-migration-guide-pointers.md b/.changeset/6342-dead-migration-guide-pointers.md deleted file mode 100644 index a4a89c9c2a..0000000000 --- a/.changeset/6342-dead-migration-guide-pointers.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Two deprecation warnings pointed at `MIGRATION_GUIDE.md`, a file deleted from the -repository in `8c5d20455` (objectui#6342). - -`Registry.register()`'s missing-namespace warning now points at the live docs page -that documents namespaced registration -(`/docs/guide/plugin-development#namespaced-registration`) instead of the deleted -guide. `ValidationEngine`'s function-based-condition warning drops its `See:` line -entirely: the deleted guide covered component namespaces and lazy field -registration and never documented conditions at all, so that pointer was -misdirected as well as dead, and the warning already carries the complete -before/after migration inline. - -Both are console messages shipped to application developers, so neither can use -the immutable `git show ^:` provenance form objectui#6275 used for a -docblock — a reader of the npm package has no repository to run it against. diff --git a/.changeset/6353-timeline-bare-key-ownership.md b/.changeset/6353-timeline-bare-key-ownership.md deleted file mode 100644 index 84d83034d6..0000000000 --- a/.changeset/6353-timeline-bare-key-ownership.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-timeline': patch ---- - -The bare `timeline` component key is now owned by DECLARATION rather than by -module-evaluation order (objectui#6353). - -`packages/plugin-timeline` registers the same short name twice — -`plugin-timeline:timeline` (`src/renderer.tsx`, the presentational renderer) and -`view:timeline` (`src/index.tsx`, the object-bound `ObjectTimelineRenderer`). Neither -passed `skipFallback`, so under `Registry.register` both also claimed the **bare** -`timeline` key and the last module to evaluate won it. `src/index.tsx` re-exports -`./renderer` (line 300) before its own `import` (line 307), so the presentational one -registered first and the object-bound one overwrote it. - -The resolved outcome was the intended one and **does not change here**: `type: -'timeline'` still renders `ObjectTimelineRenderer`, which delegates inward to the -presentational renderer. What changes is that it is now decided rather than inherited. -Reordering those two lines would previously have handed `type: 'timeline'` to the -presentational renderer, which reads none of the object-bound keys (`object`, `filter`, -`sort`, `limit`) — an authored timeline would have stopped fetching, with no error and -no failing test. The registry's own collision guard names this remedy in its warning -text; this applies it. - -`src/renderer.tsx` now registers with `skipFallback: true`, so only `view:timeline` -claims the bare key, in any evaluation order. The presentational renderer stays -reachable under its explicit `plugin-timeline:timeline` key, which is the lookup a -presentational host already uses — no consumer-visible resolution changes. - -`src/__tests__/timeline-bare-key-ownership.test.ts` is the half that outlives the fix: -it fails if the declaration is dropped, if a third registration starts claiming the bare -key, or if resolution becomes order-dependent again. It reads both registrations' real -declared metadata back out of the registry and replays them into a fresh `Registry` in -**both** orders, so order-independence is a property under test rather than a property -of the file the test happens to import. diff --git a/.changeset/6360-master-detail-declined-detail-config-hint.md b/.changeset/6360-master-detail-declined-detail-config-hint.md deleted file mode 100644 index e074fb8689..0000000000 --- a/.changeset/6360-master-detail-declined-detail-config-hint.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`object-master-detail-form` now renders a config hint naming `childObject` for a detail -collection whose child object never resolved, instead of `Loading columns…` forever -(objectui#6360). - -`MasterDetailForm` already declines to fetch the schema of such a detail (objectui#5940) -and returns the entry unresolved, which is correct — asking the data layer for an object -literally named `undefined` is what that guard removed. But the decline is precisely the -guarantee that the entry's columns can never arrive, and the render branch it fell into -read `!d.columns?.length ?

Loading columns…

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

Loading columns…

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

Loading columns…

`, -and that message never ended: the derive is not retried, so those columns could never -arrive. Same unbounded-wait-shown-as-a-spinner family as objectui#5940 / objectui#6188 / -objectui#6194 / objectui#6360 / objectui#6372. - -The entry now carries `status: 'underivable'`, and the renderer gives it a branch of its -own that names both ends of the relationship it could not find and the key to set: - -> Could not work out how `po_line` links to `purchase_order`: no lookup or master_detail -> field on it references the parent. Set `relationshipField` on this collection to the -> field that holds the parent record. - -⛔ Deliberately **not** objectui#6372's refusal placeholder, which states the schema could -not be loaded — false for a schema that loaded fine. The two failures keep separate copy -because they have different remedies: one is "check the object exists and reload", this one -is "set this key". The thrown error is still logged with its stack (objectui#6372), since -the placeholder shows the author the key rather than the raw message. - -Behaviour is unchanged for the other two arms and for a detail that is genuinely still -fetching — that one keeps `Loading columns…`, where the message is true. diff --git a/.changeset/6396-previous-values-dom-leak.md b/.changeset/6396-previous-values-dom-leak.md deleted file mode 100644 index 7033c319c4..0000000000 --- a/.changeset/6396-previous-values-dom-leak.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The form renderer no longer leaks `FormSchema.previousValues` onto the `
` DOM node -(objectui#6396). - -`previousValues` is a declared schema key with a real consumer: the renderer's -`previousRecord` memo, which binds `previous` for field-rule CEL predicates and is the -INSERT/UPDATE signal the read-only submit strip gates on (objectui#3484). That consumer -reads it off `schema` and is unchanged. The defect was on the other channel — -`SchemaRenderer` spreads every non-metadata top-level schema key as a React prop *in -addition* to handing the node over as `schema`, so an edit-mode host (`ObjectForm`, which -is what the `object-master-detail-form` header composes) delivered a second, top-level copy -in `...props`. The renderer already consume-and-drops that whole family before its DOM -spread — `objectName`, `onDirtyChange`, `defaultValues`, `fields`, `layout`, … — -and `previousValues` was the one member missing from the list. - -Two things followed, on every edit-mode header render. React declined the prop and printed -`React does not recognize the previousValues prop on a DOM element`, and — measured on -React 19, and not recorded on the card — the persisted record was still stamped onto the -element as `previousvalues="[object Object]"`. Consume-and-dropping the duplicate removes -both. - -Scope is the runtime leak only. The declared key stays exactly as declared -(`packages/types/src/form.ts`, `packages/types/src/zod/form.zod.ts` are untouched): it has -a live consumer, so there is nothing here for the enforce-or-remove channel. diff --git a/.changeset/6397-partial-schema-collapse-pin.md b/.changeset/6397-partial-schema-collapse-pin.md deleted file mode 100644 index 244bf72b19..0000000000 --- a/.changeset/6397-partial-schema-collapse-pin.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`PartialSchema` is pinned as collapsed, and its doc comment now says so -(objectui#6397). Nothing about the type changes — the declaration is deliberately -left exactly as written. - -The alias promises "all properties optional except the type" and does not deliver -it. Every instantiation declares exactly ONE property, `type`, and carries a live -`[key: string]: any`, so it accepts any key at `any`. Measured through the -TypeScript checker against the emitted `index.d.ts` — the same instrument that -produced objectui#6269's 61 -> 0 reading: - - PartialSchema -> 1 declared property: type (source: 61) - PartialSchema -> 1 declared property: type (source: 67) - PartialSchema -> 1 declared property: type (source: 42) - PartialSchema -> 1 declared property: type (source: 27) - -`Omit` is `Pick>`, and `keyof T` on a type carrying a -string index signature is `string | number` — the literal member names are -absorbed. Every `T extends BaseSchema` inherits `BaseSchema`'s `[key: string]: any` -(objectui#5155), so `Partial>` rebuilds a type holding the index -signature and none of the named members. This is objectui#6151's collapse -(heritage clause) and objectui#6269's (property position) in a third position: a -generic mapped-type alias, which is why neither of their guards sees it — #6151's -walks the `LayoutSchema` union, #6269's reads `ObjectViewSchema`'s two slots. - -**Why a pin and not a repair or a retirement.** Triage ruled on 2026-08-25 that -retiring the alias is a removal of a published export of `@object-ui/types` — a -breaking removal of published capability, which sits on the human floor — and that -escalating it today would spend the maintainer's attention on a question -objectui#5155 is expected to moot. Repair in place is unavailable: `T` is generic, -so there is no literal key list to `Pick` the way #6269 could for its two concrete -schemas, and every generic re-spelling collapses for the same `keyof T` reason. -Once #5155 removes the root index signature, the alias starts working as written -with no edit at all. What this ships is the removal of the one impermissible -state — *declared, published, collapsed, and unpinned*. - -No runtime code, no type declaration and no accepted value changes; a consumer's -`PartialSchema` means exactly what it meant before. Declared `patch` rather -than as a no-release so the doc-comment warning actually reaches the published -`.d.ts` a consumer reads — that warning is the deliverable half of the card. diff --git a/.changeset/6400-workflow-version-anchor.md b/.changeset/6400-workflow-version-anchor.md deleted file mode 100644 index 05085edcaa..0000000000 --- a/.changeset/6400-workflow-version-anchor.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Test-only (objectui#6400): the version-claim ledger's `Node 22.x` entries no -longer restate a hand-written count of `.github/workflows`, they are asserted -against it. - -`scripts/__tests__/doc-version-claims.test.ts` gains a `workflowVersionKey` -field on `KNOWN_CLAIMS` — the objectui#6400 counterpart to `skeletonDep`. It -names the GitHub-Actions key (`node-version`) whose declarations across -`.github/workflows` are a claim's anchor, and a new -`describe('doc version claims - the workflow-version assertion')` reads those -declarations on every run and demands the page state their major. - -The entry that motivated it was `anchored` and both halves of its reason had -gone false: it named "the 14 `node-version: 22.x` declarations" against a tree -holding 28 across 23 files, one of which is spelled `'22'` rather than `'22.x'`, -and it credited `ci-cd-pipeline-doc.test.ts` with pinning the line when that -file contains no `node-version` and no `22` at all. The claim itself stayed true -throughout — what rotted is the sentence a reviewer re-measures it by, which is -the entire load an `anchored` entry without a machine-checked field carries. - -Three entries carry the new field (`ci-cd-pipeline.md`, `building-crud-app.md`, -`quick-start.md` — all three counts were stale), and none of their reasons -states a count any more. The comparison is on the **major**, not the spelling, -so the one lane declaring `'22'` is inside the anchor rather than a blind spot; -a value the parser cannot read is reported rather than skipped, and the line -parser is cross-checked against a counter that knows only the key. - -No package source changed and nothing is published by this. diff --git a/.changeset/6405-fields-shared-stylesheet-builder.md b/.changeset/6405-fields-shared-stylesheet-builder.md deleted file mode 100644 index 8ae7c7db09..0000000000 --- a/.changeset/6405-fields-shared-stylesheet-builder.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Build-tooling consolidation with no published bytes changed: -`packages/fields/scripts/build-css.mjs` no longer carries its own copy of the -components-sheet subtraction and now runs the shared -`scripts/build-plugin-stylesheet.mjs` builder that `@object-ui/plugin-grid` and -`@object-ui/plugin-kanban` already use (objectui#4929). One implementation of -the subtraction now exists, so a fix to it reaches every package that ships a -supplement stylesheet instead of only the one it was applied to. - -Declared as **no release** deliberately. `@object-ui/fields` ships -`dist/index.css` to consumers, so the acceptance gate for this change was byte -identity of that artifact, not equivalent-looking code — and it holds: the -emitted sheet is `git hash-object f9cd6504a9e3a9603836230aea27c5a3a7e4e431`, -22761 bytes, `167 rules kept (163 classes)`, both before and after the rewrite, -and now from any working directory. Nothing a consumer installs changes, so -there is no behaviour to release; bumping the fixed group of packages for a -build script that emits identical bytes would put a version in the changelog -that describes nothing. - -Fields keeps what is specific to it — its `MUST_SURVIVE` sentinels, its -`CLASS_CEILING` of 600 and its stylesheet banner, the last through a documented -`build({ header })` hook added to the shared module rather than a retained -second copy of the builder. diff --git a/.changeset/6409-keyed-version-recogniser.md b/.changeset/6409-keyed-version-recogniser.md deleted file mode 100644 index 9961167d95..0000000000 --- a/.changeset/6409-keyed-version-recogniser.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -The doc version-literal ledger can now see a version written behind a -`-version:` key — `node-version: 20`, the shape every workflow example in -these docs uses (objectui#6409). Test-only: `scripts/__tests__/doc-version-claims.test.ts` -gains a second recogniser for that shape, plus the permanent fixture that rebuilds the -pre-fix recogniser and asserts it fails on the same line the new one matches. No -published package source changes. diff --git a/.changeset/6412-packaged-actions-activation.md b/.changeset/6412-packaged-actions-activation.md deleted file mode 100644 index cf50e3c65b..0000000000 --- a/.changeset/6412-packaged-actions-activation.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Setup › **Packaged automation** gains a packaged **actions** section beside its flows one — -the surface half of ADR-0126 §8 item 2 (objectui#6412; the engine, ledger and both dispatch -doors landed in objectstack#12348). The maintainer's pull, verbatim and untranslated: -「动作 可能是需要开关的,因为有的 action 我不想启用。」 - -Per packaged action the section does exactly **one** thing: **on/off for this scope**. That -is all the `sys_metadata_activation` ledger knows about an action, and the section claims -nothing more. - -- **State** comes from the ledger's `metadata_type: 'action'` rows, read through the generic - data API list the object itself sanctions for exactly this - (`apiMethods: ['get', 'list']` — *"Reads stay open so operability surfaces can answer - 'what is disabled here?'"*). **Absence of a row means active**, so a stock boot shows - everything armed. -- **Discovery** mirrors the runtime's own `collectActionDeclarations`: object-embedded - `actions[]` from `GET /meta/object` **plus** standalone items from `GET /meta/action`, with - the object-embedded declaration winning a `:` key clash. Listing only the - first source would leave an administrator with no off-switch for a packaged standalone - action. -- **Flips** invoke the L6 write door `POST /actions/_activation/:object/:action` with exactly - the one key its body declares (`{ enabled }`); `global` is the object segment for an - object-less action, the same spelling both dispatch doors take. - -**⛔ No clone for actions.** The flows section keeps its own (§7.1); amendment ruling 3 -charters the *switch* for actions and nothing else, and §8 keeps the clone half pre-chartered -until real pull appears. A clone control here would advertise machinery that does not exist — -which is also why the server's §5 refusal for actions recommends the platform operator and an -ordinary sibling action where the flow refusal recommends a clone. - -**⛔ No drift or ancestry surface** (§9): no "customized" badge, no diff-vs-base, no -base-moved notice. The platform tracks no such lineage, so a surface showing it would be -showing something it had to invent. The absence is pinned against a response that smuggles -`clonedFrom` / `baseVersion` in, so it is enforced at the renderer and not merely by the wire. - -Server refusals reach the operator **verbatim** — no client-side softening, no retry loop. -Three shapes are reachable in tests, each transcribed character-for-character from the -runtime's own message builders: the §5 posture gate (403 `PERMISSION_DENIED`, naming the -posture *and* the sanctioned path), the ambiguous-name refusal (409 `RESOURCE_CONFLICT`, -naming the objects a machine name collides across — a list nothing on the client could -reconstruct) and the no-ledger outage (503 `SERVICE_UNAVAILABLE`). - -One further refusal is the section's own and it points the same way: a `hasMore` on the -ledger read is treated as a **load failure** rather than rendered. A dropped row reads as -"active", so a partial ledger would show a switched-off action as armed — the one direction -this section must not fail in. - -The flows section is unchanged in behaviour. It gains a heading beside the new one, and the -page subtitle now says "Flows and actions" — that string moved in all ten packs together. -Nine new `packagedAutomation.*` keys land in `en` **and in all nine other packs** as real -translations; the two toggle-failure keys are artifact-neutral by wording and are reused -rather than duplicated. diff --git a/.changeset/6416-plugin-report-namespace.md b/.changeset/6416-plugin-report-namespace.md deleted file mode 100644 index 41d0b87d0c..0000000000 --- a/.changeset/6416-plugin-report-namespace.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/plugin-report': patch -'@object-ui/cli': patch ---- - -`@object-ui/plugin-report` now registers its three components under namespace -**`plugin-report`**, the spelling its consumers already declare (objectui#6416). - -It used to register `report`, `spec-report` and `report-viewer` under namespace -`report`, while `apps/console` declared the lazy stubs for the same three short -names under `plugin-report` and the CLI's known-type whitelist shipped the -`plugin-report:*` spellings as renderable. Two things followed from the -disagreement: - -- **`plugin-report:report`, `plugin-report:report-viewer` and - `plugin-report:spec-report` could never be satisfied.** `Registry.register` - clears the lazy stub for the type IT registers, and that type was - `report:report`, so those three stubs were never cleared and no component was - ever stored under them: `get('report', 'plugin-report')` returned `undefined` - and `hasLazy('report', 'plugin-report')` stayed `true` forever. A schema - authored with any of the three whitelisted keys resolved to nothing — the - gate handed authors a green light for a key the runtime could not satisfy. -- **The bare `report` key was claimed twice under two different namespaces.** - `Registry.register` and `Registry.registerLazy` share the - `meta?.namespace && !meta?.skipFallback` branch, so what bare `report` - *declared* depended on whether the plugin chunk had loaded yet — the - objectui#6353 shape. - -**No authored metadata changes.** The direction was chosen by measurement: -nothing in this repository, and nothing in the sibling `objectstack` checkout, -authors a `report:*` spelling (0 hits), while the bare spellings are authored in -48 places. `type: 'report'`, `type: 'spec-report'` and `type: 'report-viewer'` -resolve exactly as before; the three unreachable `report:*` keys are retired and -the three `plugin-report:*` keys now name real components for the first time. - -`packages/cli/src/utils/known-schema-types.ts` is regenerated from the -registrations, dropping `report:report`, `report:report-viewer` and -`report:spec-report`. - -Two pins are the half that outlives the fix: -`packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts` replays -this package's real declared metadata and a console-shaped lazy stub into a -fresh `Registry` in **both** registration orders, checking the bare key's -declared namespace after every step, so order- and phase-independence are -properties under test rather than properties of the file the test imports. -`scripts/__tests__/report-namespace-agreement-6416.test.ts` re-derives both -sites from source and fails if the plugin, the console stubs and the generated -whitelist ever disagree again. diff --git a/.changeset/6419-objectview-expand-gate.md b/.changeset/6419-objectview-expand-gate.md deleted file mode 100644 index 8b53c9e1a0..0000000000 --- a/.changeset/6419-objectview-expand-gate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView`'s non-grid fetch now carries `$expand` (objectui#6419). The effect built its -expand set from `objectSchemaRef.current` — a ref assigned in the render body, deliberately -kept out of the effect's dependency list so the effect would run exactly once per mount. On -that one run the ref was still `null`, so `buildExpandFields` saw no fields and the query -went out as `{ $top: 100 }` with no `$expand` at all; because the effect never re-ran on the -schema's arrival, it never went out with one either. - -`ObjectView` hands the rows it fetches to the child view as `data={data}`, which suppresses -that child's own fetch. So every lookup / master_detail / user / tree field in the six -non-grid views it hosts — kanban, calendar, gallery, timeline, gantt, map — rendered from -raw foreign-key ids: blank on the kanban (its `resolveDisplay` suppresses opaque ids) and -potentially the raw id on the other five. - -The object schema and the fact that its read has SETTLED are now one piece of state, keyed -by object name, and the record query waits on it — the shape `ObjectKanban` adopted in -objectui#6271. The gate is on the read having settled, **not** on a truthy schema: a view -whose adapter exposes no `getObjectSchema`, or whose read threw, still queries (unexpanded) -rather than waiting forever, and switching objects closes the gate in the same commit rather -than sending the previous object's expand set. - -The trade was measured on this effect rather than inherited, because it has five more -dependencies than the board's. With an instrumented adapter (schema and `find` both 30ms) -across four host regimes: before, one query with no `$expand` and one raw delivery to the -child; with `objectSchema` merely added to the dependency list, two queries and two -deliveries — `raw` then `expanded`, a visible two-step paint, because here the raw rows -settle into state *before* the re-run's cleanup rather than being discarded as they were on -the board; gated, one query carrying `$expand` the first time and a single expanded -delivery, with correct rows landing at the same wall clock as the dependency version. diff --git a/.changeset/6421-concurrent-update-code-optional.md b/.changeset/6421-concurrent-update-code-optional.md deleted file mode 100644 index bc5118a490..0000000000 --- a/.changeset/6421-concurrent-update-code-optional.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`isConcurrentUpdateError` no longer promises a `code` its runtime check -knowingly accepts values without. - -The exported predicate matches on two limbs — `code === 'CONCURRENT_UPDATE'` -(the wire shape) **or** `name === 'ConcurrentUpdateError'` (the deliberate -cross-realm discriminator, for a host that bundles the adapter twice so -`instanceof` fails). Its narrowed type declared `code: 'CONCURRENT_UPDATE'` as -a **required** literal, so for exactly the case the second limb exists to serve -the predicate handed the caller a property the value does not have. `code` is -now declared optional (`code?: 'CONCURRENT_UPDATE'`), stating only what both -limbs guarantee. - -**Type-level only; zero runtime change.** The two-limb check is byte-identical -— it is deliberate and documented, and the diff touches nothing but the return -type and the comment above it. There was no runtime symptom to fix: the only -consumer (`InlineEditSaveBar`'s conflict builder) reads just the optional -`currentVersion` / `currentRecord` fields. - -**Breaking for TypeScript consumers that read the narrowed `code`.** After -narrowing, `err.code` is now `'CONCURRENT_UPDATE' | undefined` instead of -`'CONCURRENT_UPDATE'`, so code that assigned it to a non-optional `string` will -newly fail to compile. That failure is the point: on a name-only error the -value was already `undefined` at runtime, and the old declaration was the -reason the compiler could not say so. Guard the read (`err.code === -'CONCURRENT_UPDATE'` still narrows fine) or branch on the predicate itself -rather than on the field. - -Pinned by `ConcurrentUpdateDialog.narrowedCode-6421.test.tsx`, which asserts -the invariant by assignability — the value the runtime accepts through the -`name` limb must be assignable to the type the predicate returns — alongside -runtime coverage of both limbs, which `plugin-detail` had none of. diff --git a/.changeset/6425-objectdatatable-column-holds.md b/.changeset/6425-objectdatatable-column-holds.md deleted file mode 100644 index d222901e91..0000000000 --- a/.changeset/6425-objectdatatable-column-holds.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -No published behaviour changes, and nothing declared or retired. - -`ObjectDataTable.enrich()` read six field-meta overrides off the AUTHORED column — three -through `(col as any)`, two more through the column bag's `[key: string]: any`, which -answers `any` just as loudly without the tell (objectui#6425). Those reads now go through -a local `AuthoredColumnOverrides` keyhole: every honoured key carries a written verdict -and the card that owns it, and every other `FieldMeta` member is refused by a DERIVED -`?: never` band, so a seventh one has to be adjudicated instead of admitted by silence. - -Type-only at the seam plus a new test file. `@object-ui/types`' `TableColumn` and its zod -mirror are untouched: whether `format` / `options` / `referenceTo` / `currency` / -`decimals` get declared on the published type or retired stays objectui#6425's open -ruling, and this change is the per-key evidence that ruling needs, not the ruling. diff --git a/.changeset/6426-dashboard-test-header-compiled.md b/.changeset/6426-dashboard-test-header-compiled.md deleted file mode 100644 index 2e6e50ba75..0000000000 --- a/.changeset/6426-dashboard-test-header-compiled.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -Comment-only fix: two headers in `@object-ui/plugin-dashboard` still said this -package's tests are "compiled by nothing", a debt that has since been paid. The -paragraphs did not merely describe the old state — they prescribed against -writing a compile-time pin in this package, and nearly misrouted the legitimate -one that objectui#6373 landed a few files away. - -- `src/__tests__/MetricWidget.domPassthrough.test.tsx` — the header claimed - `tsconfig.json` excludes the tests, that the package is "the sole remaining - `TEST_DEBT` entry", and that a `@ts-expect-error` written there "would be - especially dishonest". -- `src/domPassthroughPins.ts` — the counterpart half of the same contract - carried the same premise (plus a stale "6 errors, objectui#4118" count), so - fixing only the test file would have left the two halves disagreeing. - -Both now state today's arrangement and name the config that does it: -`tsconfig.test.json` type-checks this package's tests, chained from the -package's `type-check` script (`tsc --noEmit && tsc -p tsconfig.test.json`). -The `src/` placement of the pins is kept and re-framed as a preference rather -than a constraint — they still belong next to the contract and still emit zero -runtime bytes, but a pin in a test file is checked now too. - -Measured on this tree: `tsconfig.test.json --listFiles` includes 81 of this -package's test files (both edited-adjacent files among them); -`node scripts/check-type-check-coverage.mjs` reports `test type-check coverage: -41/41 packages compile their tests, 0 declared debt`; and an `@ts-expect-error` -injected into the test file guarding nothing is reported as -`error TS2578: Unused '@ts-expect-error' directive`. - -No behaviour change, no public surface change. diff --git a/.changeset/6431-param-dialog-close-parity.md b/.changeset/6431-param-dialog-close-parity.md deleted file mode 100644 index 071f1fc0ef..0000000000 --- a/.changeset/6431-param-dialog-close-parity.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`app-shell`'s two action runtimes now reset `ActionParamDialog` the same way when it closes -(objectui#6431). `useConsoleActionRuntime` closed with `setParamState({ open: false, params: [] })` — -replacing the whole state object, emptying `params` and dropping `title`, `description` and -`resolve` — while `RecordDetailView`, which mounts a second runtime into the same dialog, -closed with `setParamState(s => ({ ...s, open: false }))`. The console runtime moves onto the -field-preserving shape. - -The user-visible effect is in the fade-out. `DialogContent` carries -`duration-200 data-[state=closed]:animate-out`, so Radix keeps the content mounted through -its exit animation and the dialog goes on rendering off `state` for the whole ~200ms. Under -the blanking shape a params form the user had just filled in re-titled itself from the -action's own label to the generic "Action parameters", swapped the action's description for -the generic one, and dropped every param row — an empty, generically-labelled box fading out -where a form had been. The confirm pair converged on the same shape for the same reason in -objectui#6034; this is that ruling re-measured on this dialog rather than inherited, because -`ParamDialogState` carries a form rather than display text and "blank it on close" could have -been deliberate here. - -It was not, and nothing else changes for the user. The values a user types are not in -`paramState` at all — they live in `ActionParamDialog`'s own `values` state, which its -`useEffect` reseeds from the param defaults on every `state.open` false→true edge, so a -reopened dialog starts blank under either reset shape. The retained `resolve` is inert: the -dialog settles the promise before it asks for the close, and the open path replaces the whole -state object. diff --git a/.changeset/6442-catalog-blocks-claimant-list.md b/.changeset/6442-catalog-blocks-claimant-list.md deleted file mode 100644 index 724ec3a477..0000000000 --- a/.changeset/6442-catalog-blocks-claimant-list.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in the docs-site gallery host, -`apps/site/app/components/registerCatalogBlocks.ts`. Its `IMPORT ORDER IS CONTRACT` -note justified the import order with a bare-keyword claimant list naming two packages -that do not claim the key: it said bare `chart` is claimed by `plugin-charts`, -`plugin-dashboard` and `plugin-report`. Re-derived with the repo's own -`deriveRegistryKeys` (`scripts/check-doc-component-types.mjs`), bare `chart` has one -package claimant, `plugin-charts` — `plugin-dashboard` registers `dashboard`, -`dashboard-grid`, `metric`, `metric-card`, `object-data-table`, `object-metric`, -`object-pivot` and `pivot`, `plugin-report` registers `report`, `spec-report` and -`report-viewer`, and neither registers `chart` at all. The two `apps/console` sites -that also hold the key are `registerLazy` stubs whose loader is `@object-ui/plugin-charts` -itself, which `Registry.register` drops unconditionally once the real component arrives, -so they are placeholders for that package rather than competing claimants of the keyword. - -The same re-derivation extends the correction to the `calendar` half the issue had not -checked in depth: bare `calendar` likewise has one package claimant, `plugin-calendar`, -because `@object-ui/components` registers the date-picker primitive as `ui:calendar` -with `skipFallback: true` precisely so it does not take the bare keyword. The note -therefore no longer opens by asserting that several packages register the same bare -keyword — measured across the workspace, no bare key is claimed by two of the thirteen -packages this file imports. The mechanism (last registration of a bare keyword wins) and -the reason the order stays fixed are kept. No code, no imports and no assertions changed, -and no published behaviour changes. diff --git a/.changeset/6443-nav-visible-fault-diagnostic.md b/.changeset/6443-nav-visible-fault-diagnostic.md deleted file mode 100644 index 516875ab35..0000000000 --- a/.changeset/6443-nav-visible-fault-diagnostic.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A nav / area / field `visible` predicate that FAULTS now says so, in both builds, once per -distinct predicate source (objectui#6443). Observability only — no verdict moves. - -`ExpressionProvider.evaluateVisibility` is the gate behind a navigation item's `visible`, -an area's derived visibility, and the field list `RecordFormPage` renders. It is fail-open: -a predicate that cannot be evaluated returns `true`, so a menu entry whose role gate has -stopped working renders **for everyone — including the role it was written to exclude** and -looks exactly like an entry the author meant to show. - -That fault was swallowed one layer down. `evaluateCondition` is fail-soft: it answers an -unevaluable predicate with `true` from its own `catch` and does not throw, so this site's -`try/catch` never saw a predicate fault at all. Measured per dialect at this site before the -fix — the bare-string dialect, the one a live gate was measured breaking on, printed -**nothing at all**: - -| dialect | console at this site, before | after | -|---|---|---| -| bare string | nothing | one named line | -| `{ dialect: 'cel' }` envelope | one generic line | one named line (the generic one is *replaced*, not added to) | -| `${…}` template | one generic line **per evaluation** | one named line, deduped | - -The fix wires `EvaluationOptions.onFault` (the seam objectui#6038 landed) to -`reportUnresolvableVisibilityPredicate`, exported from `@object-ui/react` — the same -reporter, message, severity, dedupe `Set` and rate limit the node gate and `page:tabs` -already use, so one authored predicate is entitled to one line rather than one line per -package. It costs no extra engine call: the evaluator hands back the reason at the point it -already knows the predicate faulted, with no `throwOnError` double evaluation. - -A nav item is not a schema node, so the reporter's `type` slot — which, with the gate key -and the predicate source, is the dedupe key — is the constant `app-shell:visible`. The rate -limit is therefore **one line per distinct authored predicate source**, not one per menu -entry: a broken role gate copy-pasted across eight entries is one authoring mistake, in one -string, fixed in one edit. - -**Fail-open is unchanged.** The item still renders for everyone on a fault. Flipping that to -fail-closed is a permission-boundary change, not a diagnostic, and is not this change's to -make; the change makes the silence stop and nothing else. diff --git a/.changeset/6444-evaluator-fault-warn-dedupe.md b/.changeset/6444-evaluator-fault-warn-dedupe.md deleted file mode 100644 index afdf051590..0000000000 --- a/.changeset/6444-evaluator-fault-warn-dedupe.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`ExpressionEvaluator.evaluate` now reports a faulting `${…}` at most **once per authored -source** instead of once per evaluation (objectui#6444). It is the hottest of the three -predicate paths in this area — `SchemaRenderer` calls it for every `properties.*` value, -every `props.*` value and `content`, for every node, on every render — so a single broken -`${…}` prop in a 200-row list wrote 200 console lines per render, and 200 more on the next -one. Measured on the built evaluator before the fix: three identical faulting -`evaluateCondition` calls produced 3 lines where the `{ dialect: 'cel' }` envelope produced -1; the 200-row list produced 200. After: 1 in every case. - -This is the one-per-source rate limit both sibling reporters already carry -(`warnPredicateFailure` in `fieldRules.ts`, `visibilityDiagnostic.ts` in `@object-ui/react`), -not a third mechanism. The dedupe key is the predicate's **authoring** identity — the fault -site plus the source text, never the scope it ran against — which is both the siblings' -precedent and the defect itself: the 200-row flood is one authored source evaluated against -200 distinct scopes, so a scope-sensitive key would emit all 200 lines again. - -Nothing else moves. The two message texts are unchanged, a distinct broken source still gets -its own line, `EvaluationOptions.onFault` still fires on every fault (objectui#6038's passback -contract, so a caller doing its own warn-once bookkeeping keeps control of it), `throwOnError` -still throws on every evaluation, and no symbol is added to the published surface. diff --git a/.changeset/6445-disabled-gate-fault-diagnostic.md b/.changeset/6445-disabled-gate-fault-diagnostic.md deleted file mode 100644 index ac2eff72dd..0000000000 --- a/.changeset/6445-disabled-gate-fault-diagnostic.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/react': minor ---- - -A `disabled` / `disabledOn` predicate that cannot be evaluated is now reported on the console, in development **and** in production — and the message says what this gate's fail-soft default actually did. - -`SchemaRenderer` routes six visibility legs (`visibleWhen`, `visible`, `visibleOn`, `visibility`, `hidden`, `hiddenOn`) through one reporter, and called `evaluateCondition` **bare** on the two enablement legs — the only uninstrumented predicate pair in the file. A faulting `disabled` predicate had therefore never been reported in any build, in any dialect that does not report on its own. - -It is also the pair whose fail-soft answer **bites**. `evaluateCondition` answers an unevaluable predicate with `true`; on the negated visibility legs that means SHOWN, here it means GREYED OUT. So the user got a control they could see and could not use, and the author got nothing to grep for. - -- **Wiring only, one engine call.** Both legs pass `EvaluationOptions.onFault` (objectui#6038's seam), which hands back the fault the evaluator has already caught. No `throwOnError`, no second evaluation, no `__DEV__` split — dev and production print identical bytes. -- **No verdict moves.** The fail-soft `true` is preserved deliberately: a faulting `disabled` predicate still disables, exactly as before. Flipping that is a shipped-behaviour change and is not part of this. -- **Its own copy, not the visibility reporter's.** The shipped line says the safe default meant the gate "did NOT bite", which is written about a gate that shows the node. This gate's line says the opposite, because the opposite is true: `[ObjectUI] An enablement predicate could not be evaluated`, then the node, the key, the source, the engine's reason, and that the node renders disabled — on screen, greyed out — with nothing else on screen to say a predicate failed. One reporter, one dedupe, one severity; a second message. -- **Rate-limited exactly as the visibility gate is**, per `(node type, key, predicate source)`: two hundred rows of one broken predicate print one line, a second distinct source still prints, and the same source authored on `disabled` and on `visibleWhen` prints two — the gates cannot silence each other. diff --git a/.changeset/6453-calendar-expand-gate.md b/.changeset/6453-calendar-expand-gate.md deleted file mode 100644 index 4400c0ec3a..0000000000 --- a/.changeset/6453-calendar-expand-gate.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -A standalone `object-calendar` bound to an object now queries WITH its `$expand`, so -lookup / master_detail / user / tree fields render the related record instead of a raw -foreign-key id (objectui#6453). - -`ObjectCalendar`'s fetch effect built its expand set from a ref assigned in the render body -(`objectSchemaRef.current = objectSchema`) and left `objectSchema` out of its dependency -list. That bought the effect exactly one run per mount and paid for it with the expansion, -permanently: on that one run the ref was still `null`, `buildExpandFields` saw no fields, -the query went out with no `$expand` at all, and nothing re-ran the effect when the schema -landed. Only the standalone calendar reached this path — one hosted by `ObjectView` or -`ListView` receives its rows as `data`, which objectui#6419 already covers. - -The ref is replaced by a settled-and-keyed resolution (`{ key, def } | null`) that GATES the -record query, the third member of the family after objectui#6271 (`ObjectKanban`) and -objectui#6419 (`ObjectView`). Measured on this component rather than inherited: gated, the -calendar issues one query carrying `$expand` in every latency profile; the alternative of -adding `objectSchema` to the dependency list issued two, and when the schema read was the -slower of the two it painted raw ids, reverted to the "Loading calendar..." placeholder, -then swapped — a three-step paint the correct rows do not arrive any later than. - -The gate is on the schema read having SETTLED, never on a truthy schema: an adapter that -exposes no `getObjectSchema`, and a read that throws, both settle with nothing and the -calendar still queries (unexpanded) rather than waiting forever. An inline `value` data set -is deliberately not gated — it issues no metadata read, so there would be no resolution to -wait for. diff --git a/.changeset/6455-listview-sort-persist-leak.md b/.changeset/6455-listview-sort-persist-leak.md deleted file mode 100644 index c082fcf6a1..0000000000 --- a/.changeset/6455-listview-sort-persist-leak.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`ListView`'s toolbar sort picker no longer persists a sort the platform refuses to order by. - -The picker keeps a platform-refused field listed while the CURRENT sort names it -(#6108). That exception is deliberate and stays: it is the only way a user can -REMOVE a sort the server answers `400 INVALID_SORT` for — withholding the option -unconditionally renders a blank row nobody can delete, and drops the sort silently -on the next edit. - -What was wrong is that the picker rendered and emitted from the same array. Editing -anything ELSE in that popover — adding a second sort key, resetting to the view's -default — re-emitted the whole array with the refused entry still in it, and the -host's `onSortChange` turned that into `persistViewPatch({ sort })`: a -personalization PUT storing a refused column, written by a user who never touched -that row. A view stored before the sortability signal existed therefore kept -re-persisting its refused `$orderby` indefinitely. - -Every `onSortChange` this component emits — the builder, the column-header sort and -"reset to default" — now crosses one boundary that drops what the served projection -refuses, while `currentSort` keeps the array whole. So what the picker LISTS and -what it PERSISTS are separate: the refused entry stays visible and removable, -removing it persists the removal, and no write carries it. This is the separation -#5729 already made at the grid seam (`ObjectGrid`'s `manualSort` / -`manualOnSortChange` pair); the picker was the second door onto the same stored -view state. - -Only under a served sortability projection (objectstack#10235 ruling A). `undefined` -means NO SIGNAL SERVED — an older deployment, an inline/mock data source — not -"nothing is sortable", and that branch is byte-identical in behaviour to before. diff --git a/.changeset/6460-objectview-fetch-view-identity.md b/.changeset/6460-objectview-fetch-view-identity.md deleted file mode 100644 index 0b919f24c1..0000000000 --- a/.changeset/6460-objectview-fetch-view-identity.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ObjectView`'s non-grid fetch no longer re-queries once per parent render when the host -passes an inline `views` array (objectui#6460). - -The effect that fetches rows for the six non-grid view types (kanban, calendar, gallery, -timeline, gantt, map) listed `activeView` — an **element of the `views` prop array** — among -its dependencies. A host writing `views={[{ id: 'cal', type: 'calendar', label: … }]}`, which -is how this component's own docs write it, produces a fresh element object on every one of -its own renders, so the dependency changed identity every render and a new `find()` went out -each time. Measured with an instrumented adapter and three parent re-renders: **4 queries -where a hoisted array gives 1**. Because `ObjectView` hands its rows to the child view as -`data={data}`, each extra query also re-delivered a fresh row array downstream — the -"duplicate events in child views like the calendar" hazard, from the re-run direction. - -The effect now depends on the **values it reads** — the active view's `filter` and `sort`, -plus its `id` — held at a steady reference while they are structurally unchanged, instead of -on the view object's identity. Asking hosts to hoist the array was considered and rejected: -that is a contract change on every caller of a published component, and it leaves the defect -live for every host that does not comply. - -Nothing about precedence moves: a named `listViews` config's `filter`/`sort` still outrank -the view's, which still outrank `table.filter`/`table.sort` and their deprecated aliases. -Changing a view's filter, changing its sort, and switching the active view all still -re-fetch. The comparison never serializes, so it stays correct for filter and sort values -that have no faithful stringification — a `Date`, a function, a `Map`, `NaN`, or plain -key-order instability — and every case it cannot model resolves to "changed", which costs a -redundant query rather than withholding a needed one. diff --git a/.changeset/6464-recall-submitter-gate.md b/.changeset/6464-recall-submitter-gate.md deleted file mode 100644 index 541e31c934..0000000000 --- a/.changeset/6464-recall-submitter-gate.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -The record page's approval band offers its **Recall** button to the approval's submitter -only (objectui#6464). - -Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval; -user B — not the submitter, read access, not an admin — opens the record and the band still -lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes -on submitter identity and refuses everyone else, so the only outcome available to that -button was a failure toast. Record state was never at risk; this was purely a -writability-feedback mismatch, the same family as objectui#3794. - -The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at -all" — which is a question about the DataSource, not about the viewer. Identity now joins -it, threaded the way every other signal on that band already travels: the HOST resolves it -and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and -never re-derives who submitted what. - -- `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on - `InlineEditContextValue`. Additive and optional; no existing prop changes. -- `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a - resolved `false`. -- `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing - approvals read and threads it. - -**The signal is tri-state, and the third state is the load-bearing one.** `true` offers -recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity -— renders exactly as it did before this release.** Omission preserving prior behaviour -mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide" -would have traded a cosmetic defect for a functional loss: every host whose band runs off -the record's `approval_status` mirror alone would silently lose its submitter's only way to -unlock their own record. - -**Withdrawn rather than disabled-with-reason.** The card offered either. For a -non-submitter this control is never actionable on any pending record, so a permanently -disabled button is standing clutter rather than a lesson; and the two sibling submitter -levers already hide — the approvals panel's Remind button, and the declared -`approval_recall` action's `visible` predicate. The band, its quorum tally and the -approvals timeline still tell a non-submitter exactly what state the record is in. Only the -lever they can never pull is gone. - -**This changes no permission.** Nothing about what the server allows moves, `canEdit` and -the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an -authorization verdict — the recall endpoint remains the sole authority, and it refused -these callers before this change and refuses them after. There is deliberately **no admin -carve-out** (the reporter ruled that case out, cf. objectstack#9464). - -The derivation itself is now one function, `isSubmitterOf` — server-resolved -`viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends -that predate it, joined with `??` so a server that resolved `false` is believed rather than -re-litigated client-side. The approvals panel's Remind gate, which already carried that -expression inline and whose behaviour is unchanged, now reads the same answer: two copies -would have been two definitions of who submitted. - -The **untranslated refusal text** the reporter also saw ("No pending approval request found -for this record", concatenated after a localized prefix) is a separate defect and is not -addressed here; it is tracked on objectstack#11993. diff --git a/.changeset/6465-stale-typecheck-headers.md b/.changeset/6465-stale-typecheck-headers.md deleted file mode 100644 index 755b985bf4..0000000000 --- a/.changeset/6465-stale-typecheck-headers.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Comment-only fix: eleven test headers across `@object-ui/app-shell` and -`@object-ui/core` still described a type-checking arrangement that no longer -exists. Two distinct stale claims, both now corrected: - -1. **"this package's tests are compiled by nothing."** Both packages graduated - in objectui#4040; each `type-check` script is now - `tsc --noEmit && tsc -p tsconfig.test.json`, and `TEST_DEBT` in - `scripts/check-type-check-coverage.mjs` is literally `{}`. -2. **Present-tense citations of `tsconfig.typetests.json`,** of which zero exist - repo-wide (objectui#4291 retired the last of them). Several headers told the - reader that a listing in such a project is what makes their assertions - load-bearing — a file the reader cannot find. - -Both matter beyond tidiness because several of these headers *prescribe*: one -said a `@ts-expect-error` here "would be read by no compiler", another that "a -new type-assertion test file is unchecked until it is added to that include -list". Both instruct the next author away from assertions that are in fact -checked, and away from the config that checks them. That is the same shape -objectui#6426 was filed and fixed for in `@object-ui/plugin-dashboard`. - -Where a header is now positive ("this file IS compiled"), the claim was -confirmed for **that file**, not inferred from the package-level coverage -reading: `tsc -p tsconfig.test.json --listFiles` lists all 8 edited app-shell -files (of 4455) and all 4 edited core files (of 634), and both projects exit 0. -Surviving mentions of `TEST_DEBT` and `tsconfig.typetests.json` in these files -are deliberate, and are now past-tense — they record why the header used to say -otherwise. - -No behaviour change, no public surface change, no assertion or directive -touched. diff --git a/.changeset/6469-gantt-block-precedence.md b/.changeset/6469-gantt-block-precedence.md deleted file mode 100644 index ffb8feea59..0000000000 --- a/.changeset/6469-gantt-block-precedence.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-gantt': minor -'@object-ui/types': minor ---- - -**plugin-gantt: the `gantt` block now outranks the flat top-level spelling, and -the losing face's keys are named instead of dropped.** - -`getGanttConfig` checked the flattened top-level spelling first and returned -early, so a node carrying both spellings rendered the flat one and every key -inside an authored `gantt` block was discarded with **no diagnostic** — not even -the `GanttConfigSchema.safeParse` warning, which sat behind that early return. - -`plugin-map` had the identical two-faces shape ruled the other way (maintainer -ruling on objectui#5018, 2026-08-17, landed in PR #5156): the block wins, with a -dev-mode warning naming the ignored top-level keys. objectui#6469 inherits that -ruling, so the two sibling view plugins now answer the same question the same -way. - -What changes: - -- A node carrying **both** spellings now renders the **`gantt` block's** values. - The block is taken **whole** — the flat keys are not merged into it. -- In dev, `[ObjectGantt] … these top-level keys are IGNORED: …` names every - shadowed flat key, once per distinct shadowing. -- Nothing else moves. A node with only the flat spelling, or only a block, is - read exactly as before. - -**Producer-safe:** `ObjectView` (`case 'gantt'`) and `ListView` (`case 'gantt'`) -both flatten `options.gantt` onto top-level keys and emit **no** `gantt` key, so -every gantt reached through either view layer still takes the flat branch, and -the new warning cannot fire on that path. This is the same producer check the -`plugin-map` flip pinned, re-run on today's `main`. - -This **supersedes** the precedence sentence in the objectui#6051 changeset -(`.changeset/6051-gantt-flat-config-declared-keys.md`), which recorded the flat -branch winning — accurate for that change, which deliberately did not touch -precedence, and reversed by this one. diff --git a/.changeset/6471-ganttconfigex-restatement-pin.md b/.changeset/6471-ganttconfigex-restatement-pin.md deleted file mode 100644 index f919b860f2..0000000000 --- a/.changeset/6471-ganttconfigex-restatement-pin.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Pin every member `plugin-gantt`'s `GanttConfigEx` restates on top of `GanttConfig` -against its twin, so two declarations of one key can no longer drift apart in silence -(objectui#6471). - -**No release.** Type-only and package-private: no runtime code changes, and the published -surface is untouched on both sides. `packages/types` has zero changed files, so -`GanttConfig`'s member list in the built `dist/index.d.ts` is byte-for-byte what it was; -the new `GanttConfigRestated` alias is exported from `ObjectGantt.tsx` for the pin to -import but is not re-exported by the package entry, and `plugin-gantt/dist/index.d.ts` -carries zero occurrences of it after a real build (positive control: `QuickFilterDef`, -which *is* re-exported, appears there). - -`GanttConfigEx` is `GanttConfig & { … }` and the intersection's second half re-declares -keys the first half already declares — nine of them arriving from the spec's -`GanttConfigSchema`, which declares 19 keys as of rc.6. The restatements are deliberate: -their JSDoc is the only prose in this repo describing what the renderer does with each -key, and the spec emits no per-member documentation, so deleting the members deletes the -documentation. What was missing was any assertion that the two declarations still agree. - -The local half is now a named type, and that is the whole mechanism rather than a tidying -step. An assertion phrased over `GanttConfigEx` cannot measure anything: `GanttConfigEx[K]` -is already `GanttConfig[K] & local[K]`, so it is assignable to `GanttConfig[K]` by -construction and stays green no matter how far the two declarations drift. Naming the -local half gives the pin two independent operands. - -Measured, not assumed: **all twelve** restated members are mutually assignable with their -`GanttConfig` twin today — including `quickFilters` and `timeSegments`, which the card and -its triage both describe as load-bearing NARROWINGS. On current `main` they narrow -nothing. objectui#6051/#6472 lifted `timeSegments` onto `GanttConfig` in the shape that is -structurally `ShiftSegmentsConfig`, and rc.6's `GanttConfigSchema.quickFilters` already -models `field` / `label` / `options` exactly as `QuickFilterDef` does. Both are kept and -pinned as a measured state rather than deleted, so a future spec bump surfaces as a -decision instead of a silent widening. diff --git a/.changeset/6473-metadatatypeactions-param-dialog-close.md b/.changeset/6473-metadatatypeactions-param-dialog-close.md deleted file mode 100644 index a1be13d209..0000000000 --- a/.changeset/6473-metadatatypeactions-param-dialog-close.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The metadata-admin type-action param dialog no longer blanks itself while it closes -(objectui#6473). - -`MetadataTypeActions` is the **third** consumer of `ActionParamDialog` in this package, -after `useConsoleActionRuntime` and `RecordDetailView` — and it was the last one still -writing `setParamState({ open: false, params: [] })` on close, replacing the whole state -object. `DialogContent` carries `duration-200 data-[state=closed]:animate-out`, so Radix -holds the content mounted through its exit animation and the dialog goes on rendering off -`state` for the whole fade-out: a user who cancelled "Test connection" or a datasource -sync watched the heading revert from the action's own label to the generic -`actionDialog.title` and every param row disappear, for 200ms, on the way out. Close now -flips `open` and keeps every other field, which is the shape objectui#6431 converged the -other two consumers on. - -Not a user-data change: the values typed into the dialog never lived in `paramState` — -they live in `ActionParamDialog`'s own `values`, reseeded from the param defaults on every -open — so a reopen still starts from the defaults, pinned as a control. - -The pre-reset `paramState.resolve?.(null)` is dropped as well, on an enumeration rather -than on "resolving twice is a no-op": `onOpenChange` is reachable from exactly three places, -all inside `ActionParamDialog`, and every one settles the promise before asking for the -close — `handleSubmit`, `handleCancel`, and the Radix root handler that delegates to -`handleCancel` (the single route Escape, an overlay click and the header close button all -take). All four routes are driven in the new test, with a census over -`ActionParamDialog.tsx` so a later call site that skipped the settle is red there instead -of leaving a promise pending forever. diff --git a/.changeset/6477-buildconflict-narrowed-type.md b/.changeset/6477-buildconflict-narrowed-type.md deleted file mode 100644 index 770cdc3fa7..0000000000 --- a/.changeset/6477-buildconflict-narrowed-type.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Type-only change in `@object-ui/plugin-detail`: `InlineEditSaveBar`'s internal -`buildConflict` callback now takes the type `isConcurrentUpdateError` narrows to, -instead of `any`, and the `as Record | null` cast that the `any` -made necessary is gone. - -Nothing releases. Measured, not assumed: building the package before and after -the change and comparing all 52 emitted `.js` / `.cjs` / `.d.ts` artefacts leaves -51 byte-identical, including `dist/index.js`, `dist/index.umd.cjs` and -`dist/index.d.ts`. The single delta is `dist/InlineEditSaveBar.d.ts`, which gains -the `BuildConflict` type its pin test reads; the package's `exports` map declares -only `"."`, so that file is not resolvable by any consumer, and the published -`dist/index.d.ts` does not mention the name. Published behaviour and published -types are both unchanged. diff --git a/.changeset/6480-strip-read-decorations.md b/.changeset/6480-strip-read-decorations.md deleted file mode 100644 index 37be1ae602..0000000000 --- a/.changeset/6480-strip-read-decorations.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService.saveFields` no longer PUTs the framework's own read decorations back (objectui#6480). - -`saveFields` fetches the current object and spreads it verbatim (`...existingObject`) so that every key the service does not model survives a field save. That spread does not distinguish keys the **author** owns from keys the **framework** adds on the way out: `@objectstack/spec` declares `_diagnostics` and `_draft` as `METADATA_READ_DECORATIONS` — stamped onto served metadata documents by the read path — and `ObjectSchema` refuses both **by name**. A served document carrying either one was therefore spread straight back into the body of `PUT /api/v1/meta/object/:name`. - -The body now passes through the spec's own exported `stripReadDecorations` before it is sent, so the list of decorations stays the spec's rather than a local copy that goes stale the next time the framework adds one. This is the strip-on-write shape `MetadataObjectsPage.handleObjectsChange` already uses for `group`, applied where the spread is — simply not writing the key is not enough when the spread is verbatim. - -The strip is deliberately bounded to those two keys and is not a general "remove whatever the schema refuses" pass: an off-spec key the author owns still goes out and is still refused loudly, where someone can see it. Nothing is lost by dropping the decorations even though a PUT is an upsert — `_diagnostics` is the read-path validation verdict, recomputed on every read, and `_draft` reflects the row's `state` column and the `mode` parameter, never the body. The ADR-0010 protection envelope (`_lock`, `_provenance`, …) *is* write-path state the server merges back, and the spec deliberately keeps it out of the decoration list, so it is untouched. diff --git a/.changeset/6487-visibility-advice-per-tier.md b/.changeset/6487-visibility-advice-per-tier.md deleted file mode 100644 index 7750f26b92..0000000000 --- a/.changeset/6487-visibility-advice-per-tier.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/react': minor -'@object-ui/app-shell': minor -'@object-ui/components': minor ---- - -**The unresolvable-visibility-predicate report now names the roots of the tier the -predicate was actually evaluated against** (objectui#6487). An app-shell author -whose nav, area or field `visible` faulted was told to check `record` and -`page.` — two roots that tier does not bind at all. - -`formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate` -(both exported from `@object-ui/react`) take a new **optional sixth argument**, a -`PredicateScopeTier` — also exported — selecting the closing advice paragraph. -Everything above that paragraph is unchanged on every surface, and so is every -verdict: this is diagnostics copy only. - -**The published signature grew; nothing existing breaks.** The argument defaults -to `'page-component'`, so a five-argument call keeps printing the bytes it -printed before. All three in-repo call sites pass their tier explicitly rather -than lean on that default. - -Each tier's root set was derived from the code that builds the bag, not from the -prose that described it: - -- **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item - predicates. Both bind `record`, `current_user` and `page.` (the roots - `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is - byte-for-byte what it was. -- **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility` - runs, wired onto this reporter by objectui#6443. Its evaluator is built from - `{ current_user, user, ctx: { user }, os: { user }, app, data, features }`, so - the line now names `current_user` with its three ADR-0068 alias spellings, - `app`, and `features` — the deployment-flag root that provider documents for - exactly this kind of predicate — and states outright that `record` and - `page.` do not exist there. - -**Why not generalise the copy instead.** Dropping the concrete root names would -have made one paragraph true everywhere at the cost of making it useful nowhere: -an author who mistyped a root needs to know which roots exist *at their tier*, -which is the whole reason the paragraph is read. - -`data` is bound at the app-shell tier but is deliberately not advertised there — -every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it, -so naming it would point an author at a root that answers nothing. diff --git a/.changeset/6488-carry-over-field-keys.md b/.changeset/6488-carry-over-field-keys.md deleted file mode 100644 index 619f637a0f..0000000000 --- a/.changeset/6488-carry-over-field-keys.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MetadataService.saveFields` carries the server's per-FIELD keys through a field save -instead of rebuilding every entry from the designer model (objectui#6488). - -The method preserved unknown keys of the OBJECT document by spreading it, but that spread -is object-level and said nothing about keys INSIDE a field. Every entry was rebuilt by -`toFieldPayload`, so every key the server sent inside a field that the designer does not -model was dropped on every field save: `expression` (a formula authored in metadata-admin), -`precision`, `scale`, `system`, `sortable`, and anything a plugin registered. Measured -against the installed `@objectstack/spec` 17.2.0, `FieldSchema` accepts all five — the -designer's model is a subset of what a field may hold, and the difference was being -deleted. - -The loss is not new but was UNREACHABLE. While `fields` went out as an array the whole -body was refused `422 INVALID_METADATA` before persistence, so nothing `saveFields` -dropped ever reached storage; objectui#6240 made the body parse, and a PUT is an upsert, -so from that fix onward the drop lands. - -`toFieldPayload` now merges onto the previous SERVER entry, read from the document -`saveFields` already fetches for the object-level spread — the form -`MetadataFieldsPage.fromDesignerField` has used one writer over all along, and no extra -request. - -Two properties keep the fix from becoming its own mirror image, both pinned in -`MetadataService.fieldKeyCarryOver.test.ts`: - -- **A property the author CLEARED stays cleared.** Every modelled key is still written - unconditionally, so a cleared property arrives as an explicit `undefined` that overrides - the carried value and is dropped by `JSON.stringify` — absent from the body, which on an - upsert is the deletion. A conditional merge would leave the server's old value standing - and fail the author's deletion silently. -- **Retired designer keys do not ride back out.** `indexed`, `referenceTo`, `formula`, - `isSystem` and `sortOrder` are refused BY NAME by `FieldSchema`; a stored document can - still carry them, and echoing one back is a hard 422 that blocks every later save of the - object with no UI path to clear it. Everything else the server sent still survives — the - strip is keyed to those tombstones, not a blanket unknown-key purge. diff --git a/.changeset/6489-designer-fields-map-keying.md b/.changeset/6489-designer-fields-map-keying.md deleted file mode 100644 index 2af37a0ed1..0000000000 --- a/.changeset/6489-designer-fields-map-keying.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-designer': patch ---- - -The Field Designer builds an object's `fields` map by defining own properties and refuses -the three field lists a name-keyed map cannot carry (objectui#6489). `MetadataFieldsPage` -keyed the map by blind assignment — `nextFields[f.name] = fromDesignerField(…)` inside a -bare `for` loop — which failed silently in three directions, all measured on the installed -`@objectstack/spec` 17.2.0: - -- **A field named `__proto__` never reached the wire.** `map['__proto__'] = def` invokes the - prototype setter instead of creating a key, so the field vanished from the serialised PUT - body. `__proto__` matches `ObjectSchema.fields`' key rule `/^[a-z_][a-z0-9_]*$/`, so the - spec stood ready to accept the field the client had thrown away. The map is now built - through `Object.fromEntries`, which defines an own property. -- **A nameless field was stored under the literal key `"undefined"`.** Measured: - `ObjectSchema.safeParse` with `fields: { undefined: … }` returns `success = true`, so the - document parsed, persisted, and had no reader anywhere. It is now refused before the - request. -- **Two fields sharing a name collapsed into one entry.** A designer list carrying two - `amount` fields PUT a single entry, the later silently replacing the earlier. Also refused - before the request. - -Both refusals raise before `client.save`, so a refused list issues no PUT at all, and the -message lands in the page's existing error surface naming the offending index — the caller -is fire-and-forget (`void handleFieldsChange(next)`), so throwing past it would show the -author nothing. - -This is the plugin-designer port of the refusals objectui#6240 landed in the sibling object -writer (app-shell's `MetadataService.toFieldsMap`), down to the wording, so the two writers -of the objectui#5761 parity family cannot drift. `fromDesignerField`'s carry-over semantics -are untouched. diff --git a/.changeset/6493-bind-field-visibility-evaluators.md b/.changeset/6493-bind-field-visibility-evaluators.md deleted file mode 100644 index aa20d7da0e..0000000000 --- a/.changeset/6493-bind-field-visibility-evaluators.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -⚠️ **Behaviour change: `current_user` and `features` gates on an object field's -`visible` that have been doing nothing on the record form page and in the -console's record modal now TAKE EFFECT.** Read this before upgrading if any of -your object metadata gates a field on the session user or on a deployment flag. - -objectui#6010 and objectui#6110 bound the host predicate scope on the form -renderer and on the console form routes. `evaluateVisibility` was still being -reached with a THIRD and FOURTH evaluator that neither of those touched: -`RecordFormPage` and `AppContent` each built a private -`new ExpressionEvaluator({ user, app, data })` for the field-visibility filter, -beside — not from — the `ExpressionProvider` each of them mounts. Those bags -bound `user`, but not the canonical `current_user` nor the ADR-0068 `ctx.user` / -`os.user` spellings of that same object, and not `features` at all. So one -authored predicate meant two different things depending on which evaluator -reached it: `current_user` resolved on a nav item and was unbound on a field. -Both sites now build their scope with the same `buildExpressionScope` the -provider uses, which is the only declaration of what an app-shell predicate may -name. - -**Why nobody noticed, and why the fix is felt as a change.** A field `visible` -predicate fails OPEN: a field on screen is what you get when the predicate says -TRUE, when the root was never bound so the predicate faulted, *and* when the -predicate has a typo. Those worlds are indistinguishable, so an app that -authored a `current_user` gate saw the field render and had no way to tell the -rule was inert. After this change the predicate is evaluated for real, and -**fields that have always been visible will disappear for the users the rule -excludes** — and a `features` gate whose flag is off will hide its field once -`/api/v1/auth/config` resolves. - -`AppContent`'s bag also hand-rolled its user as `{ name, email, role }`, without -`positions`. It now uses the same `buildExpressionUser` normaliser every other -console surface publishes, so `'sales' in current_user.positions` — the gate the -server enforces on write — reaches the same verdict client-side instead of -faulting open. - -**Before upgrading**, audit any `visible` predicate in your object metadata that -names `current_user` (or `user` / `ctx.user` / `os.user`) or `features`, and -confirm each says what you actually want evaluated. Measured on the metadata -shipped in this repo and in the framework at the time of the change: **nothing -in it authors such a gate**, so no shipped surface changes behaviour today — -the audit is for your own object metadata, which this cannot see. - -**The error path is deliberately unchanged.** A predicate that throws still -fails open, exactly as objectui#6443 / objectui#6487 left it. This change is -about which roots are BOUND, not about what happens when evaluation fails. diff --git a/.changeset/6496-toast-button-keys.md b/.changeset/6496-toast-button-keys.md deleted file mode 100644 index a5281eb24d..0000000000 --- a/.changeset/6496-toast-button-keys.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`ToastSchema` now declares the two trigger-button keys the `toast` renderer actually reads -(objectui#6496, triage scope cut 2026-08-26 — the same declare-what-runs family as -objectui#6170). - -`renderers/feedback/toast.tsx` renders a `