diff --git a/.changeset/action-param-dialog-option-visiblewhen-4758.md b/.changeset/action-param-dialog-option-visiblewhen-4758.md deleted file mode 100644 index c0335d9f7..000000000 --- a/.changeset/action-param-dialog-option-visiblewhen-4758.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`ActionParamDialog` (the `custom` barrel's published dialog) now resolves each -`select` param's options through `@object-ui/core`'s shared option evaluator, so a -per-option `visibleWhen` narrows the offered list here exactly as it does on the -app-shell action dialog and in the object form (objectui#4758). - -This surface is the repo's second action-param dialog, and its `select` branch -rendered `param.options?.map(...)` straight into Radix items. A per-option -`visibleWhen` was not evaluated wrongly — it was not evaluated at all, so an option -gated on `record.*` (a sibling param) or on `current_user.*` was offered -unconditionally, while the app-shell dialog filtered the identical field metadata. -Triage ruled the governed side authoritative; the dialog rebinds to -`resolveVisibleOptions`, resolving predicates against the dialog's own in-progress -values (the objectui#3765 Option B ruling) plus the ambient predicate scope. - -Rebind, not removal: the component stays a published export, its props and every -other branch are untouched, and retiring it remains a separate decision. - -A selection the predicate stops offering is now cleared rather than kept as a hidden -value — the same `isValueStillOffered` clear `SelectField` already performs. Without -it, filtering alone would let a picked-then-gated-out option vanish from the trigger -while still riding in the submitted payload. Params whose options declare no -predicate are untouched. diff --git a/.changeset/adapter-meta-type-singular-4940.md b/.changeset/adapter-meta-type-singular-4940.md deleted file mode 100644 index a96ab6451..000000000 --- a/.changeset/adapter-meta-type-singular-4940.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata -types in the singular, matching the other twelve `client.meta.*` call sites in this -file (objectui#4940). - -`getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed -the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued -its singular spelling was deliberate — the plural site was silent. Both plural sites -resolved today only because the server folds plural → singular -(`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`, -confirmed by reading both the mapping and the by-name route handler that calls it), so -this is consistency restoration rather than a behavior change — nothing a user hits was -broken, and nothing a user hits changes. - -`appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended -with two new cases asserting `getApp`/`getPage` pass the singular type to -`client.meta.getItem`, so a future revert to the plural spelling fails a test instead of -depending on the server-side fold staying in place. diff --git a/.changeset/adr-0057-d10-citation-attribution-5202.md b/.changeset/adr-0057-d10-citation-attribution-5202.md deleted file mode 100644 index e30834a07..000000000 --- a/.changeset/adr-0057-d10-citation-attribution-5202.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Traceability only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The change is comment text at seven live source sites; no -executable line moves, and `git diff -U0` carries zero non-comment added or removed lines. - -`server enforces, client is courtesy` was cited at those seven sites as a bare -**`ADR-0057 D10`**. The substantive claim is correct and is unchanged here; what was missing -is the **framework qualifier**. In this repository the bare string resolves to -`docs/adr/0057-console-ai-chat-one-conversation-docked.md` — a document about console AI chat -docking, which contains no `D10` at all and is the one a reader greps first. The intended -anchor is the *framework's* ADR-0057, whose D10 decides *"Setup-nav surfacing follows the -capability (ADR-0029 K2); the object stays open"*. - -Each site now carries the disambiguation already shipped by the two authorities in this -repository — `docs/adr/0036-field-conditional-rules.md:91` and -`packages/core/src/evaluator/fieldRules.ts:38` — rather than a third phrasing: -`the framework's ADR-0057 D10 — framework numbering; this repo's own ADR-0057 is an -unrelated document`. - -Three sites are deliberately left byte-untouched, all three already correct: -`packages/data-objectstack/src/appAccessProbe.test.ts:25` (a verbatim quotation of the -objectstack#8013 ruling, and about the capability/nav gate — the one family D10 really does -decide), plus the two authorities above. diff --git a/.changeset/ai-chat-public-share-base-4482.md b/.changeset/ai-chat-public-share-base-4482.md deleted file mode 100644 index 3ad06d025..000000000 --- a/.changeset/ai-chat-public-share-base-4482.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`AiChatPage`'s public share-link base now resolves through the one console-mount -resolver instead of a private copy of it (objectui#4482). - -The page built `publicShareBase` itself — read the injected ``, take its -pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third -independent implementation of the mount resolution `resolveConsoleUrl` centralizes. -objectui#4472 had just deleted the other two on that rule; this was the surviving -sibling. Its output was correct, so nothing a user hits was broken and nothing a user -hits changes: measured over the base-href matrix, the deleted builder and -`resolveConsoleUrl('s')` return identical URLs for every shape the console is served in -— `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable -builds, nested mounts, and no `` at all. - -The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`, -which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM -it returns `undefined` rather than a URL built from an origin that does not exist, so -`ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument — -the mount is only ever carried by the injected ``, and a resolver with no -other input cannot be pinned by a test that steers something production never reads. - -`resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected -`` element for each deployment shape, plus a structural case asserting no other -app-shell file reads the `` tag — so a fourth copy fails a test rather than -waiting for mount semantics to change under it. diff --git a/.changeset/ai-studio-accessor-5577.md b/.changeset/ai-studio-accessor-5577.md deleted file mode 100644 index dca947932..000000000 --- a/.changeset/ai-studio-accessor-5577.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead -of being spelled inline at two call sites (objectui#5577). - -`features.marketplace` already had a documented accessor whose docblock is where the -fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating -`/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*, -plus the "never infer this from the shape of a failure" warning. `features.aiStudio` -had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false` -and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine -had two spellings and neither reader could cite it. - -The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and -against a runtime-config snapshot whose `features` is absent that read is a TypeError -rather than a fail-open — the exact shape that crashed 29 tests across four suites in -PR #5575 before it was corrected. Measured here: no live path can currently deliver -such a snapshot to `ChatDock` (the module's singleton constructs `features` on every -write and exports no setter, and no suite mounts the dock's default body under a -partial stand-in), so this closes a reachable-by-construction crash rather than a live -one — and it closes it at the source by leaving no inline read to get wrong. - -`isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`: -neither is re-exported from `src/index.ts`, so the package's published `exports` surface -is unchanged. diff --git a/.changeset/alias-closure-relative-miss-5386.md b/.changeset/alias-closure-relative-miss-5386.md deleted file mode 100644 index f67ab4d32..000000000 --- a/.changeset/alias-closure-relative-miss-5386.md +++ /dev/null @@ -1,39 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file changed is -`examples/console-starter/test/vite-alias-closure.test.ts`, and that example is private. - -Makes the alias-closure walker record an unresolvable **relative** import instead of -dropping it. - -`computeClosure()` walks two kinds of specifier. The bare-specifier branch pushed a miss -onto `unresolvable`; the relative branch dropped one with no record at all. So -`expect(closure.unresolvable).toEqual([])` was not a weak assertion for relative imports -— it was a structurally empty one. It could not fail no matter how many relative -specifiers the walk failed to follow, and the `filesWalked` floor was the only signal -that anything had gone wrong. - -That is how two earlier conversions to explicit extensions (objectui#4538, objectui#5214) -each truncated this walk while landing green: the floor had enough slack to absorb both, -and only went red once app-shell — the largest package — converted and took the count -under 500. The direct symptom was being swallowed one branch away the whole time. - -Measured on `main` at the time of this change, with the resolver ablated to its -pre-objectui#5357 behaviour to reproduce that regression class: 275 relative specifiers -dropped, `filesWalked` 1245 to 402, packages reached 29 to 22 — and `unresolvable` still -reporting `[]`. With this change the same ablation fails the suite with all 275 named, -each alongside the file that imports it. - -The miss is recorded only for specifiers that are *meant* to be modules — no extension, -or one of the JS/TS emitted extensions. `ts.preProcessFile` also reports `./styles.css`, -`./data.json` and `./logo.svg`, which `resolveModule` cannot resolve by design, so -recording those identically would fail the suite for a reason that is not a defect. -Assets and Vite resource specifiers (`?raw`, `?url`, `?inline`) are skipped into a -separate `nonModuleSkipped` list — explicitly, and observably, rather than by accident. - -Four fixtures pin the class against the real walker so a future edit cannot silently -re-blind the branch: `./Foo.js` resolving through to the `Foo.tsx` on disk, planted -unresolvable modules of both spellings being named, assets staying out of `unresolvable` -while still being accounted for, and the classifier's boundary cases. diff --git a/.changeset/app-shell-page-header-subtitle-4761.md b/.changeset/app-shell-page-header-subtitle-4761.md deleted file mode 100644 index 01333ab79..000000000 --- a/.changeset/app-shell-page-header-subtitle-4761.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's `` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761). - -This repository has two components named `PageHeader`. `@object-ui/layout`'s is -the renderer for the authored `page:header` / `page-header` node and converged -on `subtitle` in objectui#3789, because `subtitle` is the key -`@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s — -the console's own title row, drawn by `ObjectView` and `ObjectDataPage` — -spelled the very same concept `description` and had no `subtitle` at all. Both -rendered correctly; the defect was one concept carrying two key names one -package apart, the objectstack#4115 shape moved up a layer. An author reading -one component to learn the other was being taught a key the contract does not -have. - -**Not a breaking change, measured rather than assumed.** The convergence is a -plain rename with no alias, because this component is not on the published -surface: - -| gauge | result | -|---|---| -| exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) | -| `exports` map | declares exactly `.` and `./styles.css` | -| Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves | -| in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) | -| emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) | - -No supported specifier reaches the prop, so there was nothing to keep -compatible, and a renderer-side `description` alias would have been exactly the -second dialect AGENTS.md #0.1 forbids — the layout side had just finished -retiring one. Out-of-repo consumers cannot be enumerated from this repository; -what can be, and is, is the set of import paths through which one could have -reached this component, which is empty. - -Rendered output is unchanged: same element, same classes, same position. The -patch tier is a declaration that the tarball moved, not a claim that a consumer -must act. - -`packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the -pin the card asked for. It asserts the subtitle on the DOM a reader gets (a -`

`, in the title block, after the `

`), that `description` now draws -nothing and is rejected by the compiler, and — the assertion that actually goes -red if either side drifts again — that both packages' `PageHeaderComponentProps` -declare `subtitle`. diff --git a/.changeset/approvals-inbox-cell-remount-5348.md b/.changeset/approvals-inbox-cell-remount-5348.md deleted file mode 100644 index 4a11753a5..000000000 --- a/.changeset/approvals-inbox-cell-remount-5348.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Approvals Inbox stops remounting every row on every render. - -`ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions` -inside its own component body. React identifies a component by the identity of -its function, so each render produced three brand-new component *types* and React -unmounted and remounted every row's subtree instead of updating it — and the page -holds its clock in state and ticks it every 60s, so this fired on a timer whether -or not anyone was touching the page (objectui#5348). - -Two consequences were reproduced against `origin/main` before the fix, in -`apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`: - -- **Transient subtree state is discarded.** Focus placed on a row's Approve - button moved to `` on the next clock tick. -- **Input is silently swallowed.** A pointer sequence that spans a re-render — - press, tick, release — left the confirmation dialog unopened: the captured node - had been replaced, so React's delegated listener never saw the click. This is - the failure objectui#5211 hit and worked around at its call site - (`Unable to find role="alertdialog"`). - -The three cells are now at module scope beside `StatusBadge`, which was moved -there for the same reason and already carries the explanation. Everything they -closed over is passed in: `RequestCell` and `InlineActions` take the page's -scoped translator, and `RecordCell` takes `href: string | null` — one prop rather -than two, so the objectui#5211 readable/unreadable decision and the URL cannot be -handed in disagreeing with each other. - -The verification asserts the consequence, not the placement. A test that checks -the three functions now sit at module scope stays green for a refactor that moves -them and introduces a fourth inline component beside them; these cases compare -DOM-node identity for all three cells across a clock tick, which no remount can -pass, and re-drive the swallowed click. - -That guard is load-bearing because lint cannot supply one here. -`react-hooks/static-components` exists for exactly this class and is `error` in -this repo via the plugin's recommended set, yet it reports nothing on this page: -measured on `origin/main`, an arrow-form inner component injected into -`ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same -shape in a ten-line file produced two. The rule's analysis bails out on this -component, which is how three of them shipped. diff --git a/.changeset/approvals-raw-payload-gate-5553.md b/.changeset/approvals-raw-payload-gate-5553.md deleted file mode 100644 index 3cd1ed264..000000000 --- a/.changeset/approvals-raw-payload-gate-5553.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Approvals Inbox no longer shows a business approver the submitted record's raw -row JSON. - -The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no -principal check of any kind — so every approver could expand (and one-click copy) the -complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`, -`organization_id`, bare lookup ids, and **the fields the object's metadata declares -`hidden: true`**. Reported from a live EHR deployment on 17.1.0 -(objectstack-ai/objectstack#10734), where that declaration is a patient-data control. -The app author had no legitimate lever to remove the panel — field `hidden`, view -columns, app navigation, permission sets and env vars are all ineffective against it — -so the remedies available in the field were patching the shipped bundle or injecting -CSS. - -The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's -`studioEntry` module: `studio.access` is a declared platform-scope capability that a -tenant org owner does not hold by design, and it already reaches the browser in -`systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served, -computed or made authorable — no new config key, no new i18n copy, and the panel is -byte-for-byte unchanged for the platform operator it was written for. A business -approver keeps the structured record summary, the approval chain, the activity feed -and the decision actions; only the raw snapshot is gone. - -The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from -`usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a -holder's button while the server still refuses the write is the worse outcome for an -action. This panel has the opposite stake, since the measured defect is a non-holder -seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend -predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200` -with no `systemPermissions` at all, and a reported empty array. A deployment whose -permission layer just failed must not be the one that leaks the snapshot. - -`ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the -acceptance condition is that something does *not* render — which an empty render -reproduces perfectly — every denial case also asserts the drawer it denies inside, and -the `studio.access` case drives the same fixture through the same helper and finds the -panel. `created_by` and `organization_id` are the witnesses: both are in the page's -`PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can -reach the DOM only through the raw panel. Ablating the gate (restoring the bare -`payload != null` condition) turns the three denial cases red on exactly that -assertion and leaves the holder case green. - -Out of scope, tracked separately: trimming the summary by object metadata, and the -server-side residual that sends the unfiltered snapshot to the client at all. diff --git a/.changeset/approvals-step-progress-vertical-5554.md b/.changeset/approvals-step-progress-vertical-5554.md deleted file mode 100644 index 8bccea37e..000000000 --- a/.changeset/approvals-step-progress-vertical-5554.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/app-shell': patch ---- - -The approval step progress bar is a vertical stepper, so long flows stop -clipping their tail steps. - -Both occurrences were a single non-wrapping flex row whose steps were each -`shrink-0`. A flex row's min-content width is the sum of its non-shrinkable -items, so the bar's intrinsic width grew without bound with step count and -label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK -step names measured **1070px inside a 527px container** (objectui#5554). - -The two hosts failed differently, and neither failure was recoverable by the -reader: - -- **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not - scrollable, so the nearest scroller was the drawer *panel*. Reaching steps - 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the - record card, the activity timeline and the action buttons off-screen and left - a near-blank panel. -- **`RecordApprovalsPanel`** (the record page's approvals panel) — this one - carried `overflow-x-auto`, so it scrolled itself rather than its container. - Better, but the tail steps still sat behind a scroll gesture with no visible - affordance. - -In both, readers took the clipped bar for the end of the data; the reporting -customer acceptance tester said so verbatim. Widening the window does not help: -the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. - -Both now render as a column: one row per step, a badge-and-rail gutter, and a -label that may wrap. Width is capped by the container at every step count and -every label length, which also suits both hosts' tall-and-narrow aspect. The -rail segment below each step keeps the tint rule the horizontal connector used -— it is coloured by the step it leads *into*. - -**Always vertical, with no step-count or measured-width threshold**, because -the overflow is driven by intrinsic content width (labels x count), not by -count alone: three 16-character CJK labels already crowd a 527px drawer, so any -count threshold picks a cutoff that is wrong for some real flow, and a measured -one reintroduces a viewport-dependent branch. The card's requirement is a fix -that cannot break at an untested viewport or flow length, and a layout with no -breakpoint and no measurement is the form that satisfies it. Horizontal-with- -scroll was ruled out for both occurrences: it leaves steps behind a gesture. - -Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and -`RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is -green against the broken code too — every step was always in the DOM, and the -clipping was layout — so the suites assert the property the defect names -instead: no row is `shrink-0`, every label is `min-w-0` and none is -`whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no -axis, overflow or width-pinning class carries a breakpoint prefix (so there is -no viewport with untested behaviour). The reported failing regime is exercised -directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins -that the layout classes are byte-identical across all four, so no count -threshold can put some other flow length back on the old path. - -The two steppers are kept identical by hand rather than extracted to a shared -component: they live in different packages, and deduplicating them is a -refactor with its own surface. Filed separately. diff --git a/.changeset/approver-display-name-5414.md b/.changeset/approver-display-name-5414.md deleted file mode 100644 index f44198385..000000000 --- a/.changeset/approver-display-name-5414.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -The approval panel identifies the pending approver by name, not by a truncated raw id. - -A record waiting on a position rendered its approver as `positi…ager` — the -engine reference `position:sales_manager`, 22 characters, past the identity -formatter's 14-character truncation arm and middle-truncated to fit its chip. The -step names beside it were human prose; the one line answering *who is holding -this record* was an internal identifier, and not even a complete one. The same -reference reached the admin-override confirm dialog un-truncated, so a paragraph -of plain governance prose ended `— position:sales_manager` (objectui#5414). - -Both surfaces now resolve the reference before rendering, in three tiers, most -authoritative first. The server's own `pending_approver_names` wins whenever it -answers, and a backend that resolves its own slate costs the record page no extra -request. Otherwise the console reads the directory row the spec's approver -binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, -for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no -adapter and no row, the machine name still prettifies into prose rather than -truncating. The raw reference stays on hover, which is where an internal -identifier belongs. - -An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` -is actionable where `positi…ager` is not, and it is the motivating rescue case -for the admin-override path. Staffing is deliberately tri-state — a -`sys_user_position` read the viewer is not permitted to make leaves the seat's -staffing UNKNOWN and says nothing, because "I could not look" is a different -claim from "nobody holds it" and only one of them is safe to print on a -governance surface. - -Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` -and `approvalsInbox.approverNameSeparator`. The separator is a translated -punctuation key rather than `Intl.ListFormat`, which was measured on this tree -joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with -no separator, reading as one person's name. - -The directory-backed kinds and their value columns are read from -`@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new -approver type is covered the day the spec publishes it. Id-valued kinds -(`user` / `team` / `department`) keep the existing middle-truncation: a row id -has no prose to recover, and that arm is objectui#3461's answer, not this card's -defect. diff --git a/.changeset/approver-identities-read-positions-5424.md b/.changeset/approver-identities-read-positions-5424.md deleted file mode 100644 index 2137b2b8c..000000000 --- a/.changeset/approver-identities-read-positions-5424.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) - -Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no -deprecation window, and the protocol-17 session face emits no `roles` key at -all. Three client sites still read it: - -- **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's - Approvals tab and Home's To-do card. It read nothing else, so it sent **no - `role:` identity at all**: an approval addressed to a position rather than to - a person matched nothing and vanished from all three surfaces, silently. -- **`approvalsApi.buildApproverIdentities`** — "My Pending" and the - Approve/Reject enablement. It also splits the scalar `user.role`, so it - degraded rather than dying: it still yielded `role:user` while dropping every - business position name (`manager`, `finance_approver`, …). -- **`AppContent`'s expression user** — forwarded a `roles` key that was always - `undefined` into every CEL predicate context. Removed; `positions` and - `isPlatformAdmin` were already forwarded correctly beside it. - -The retired spelling is **not** kept as a fallback — pairing the two is what -ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the -declaration. - -`AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately -untouched: it is a semantics decision, not a rename, and is deferred to a -maintainer ruling. diff --git a/.changeset/calendar-explain-network-escape-5438.md b/.changeset/calendar-explain-network-escape-5438.md deleted file mode 100644 index 18359f993..000000000 --- a/.changeset/calendar-explain-network-escape-5438.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file changed is -`packages/plugin-calendar/src/object-calendar-renderer.propsContract.test.tsx`. - -Stops `object-calendar-renderer.propsContract.test.tsx` from making four real outbound -connections to `127.0.0.1:3000` while it stays green. - -`happy-dom`'s default document origin is `http://localhost:3000/`, so a relative-URL -`fetch` that nothing intercepts resolves against that origin and leaves the process for -real. The escaping request is `@object-ui/plugin-detail`'s record-level explain probe -(`useRecordEditable`, `POST /api/v1/security/explain`): the file's one DEFAULT-navigation -`onEventClick` case opens `ObjectCalendar`'s overlay drawer, which renders -`RecordDetailDrawer` → `DetailView`, which gates its Edit/Delete CTAs on a per-record -write verdict. With no `apiFetch` on the `SchemaRendererProvider` this file wraps every -case in, the hook's `apiFetch ?? fetch` fallback reaches the bare global `fetch` — and the -hook fails open on the resulting connection failure, so the escape was invisible to every -existing assertion. - -Same defect class as objectui#3339 (`plugin-detail`, closed by PR #4105), objectui#5225 -(`plugin-report`) and objectui#5280 (`plugin-dashboard` `DatasetWidget`) — a new consumer -of an already-diagnosed hook, not a new root cause. Fixed the way #4105 settled it: -`installExplainDouble()` answers the probe from a recording double instead of the -network, installed for every case in the file (not only the one that reaches it, since -the probe is `DetailView`'s own wiring and invisible from this file's schema authoring). -`visible: true` reproduces the pre-fix fail-open behaviour exactly, so no existing -assertion changes meaning. - -A new pinned test — the file's counter-probe — opens the DEFAULT-navigation drawer and -asserts the double is actually reached, twice (`update` and `delete`), with the expected -URL and request body. Confirmed load-bearing by reverse verification: with the double -temporarily disabled, the same case brings back all four `ECONNREFUSED` and fails that -new assertion (`explainCalls` stays empty instead of reaching length 2). diff --git a/.changeset/calendar-readme-schema-keys-5045.md b/.changeset/calendar-readme-schema-keys-5045.md deleted file mode 100644 index eaf0ed083..000000000 --- a/.changeset/calendar-readme-schema-keys-5045.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -`README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema` -that does not exist. Measured against the interface itself -(`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's -only required key besides `type` — was published as `events?`, so a reader -following the README omits it and TypeScript rejects the node; `defaultDate` was -`string` where the schema says `string | Date`; and `onDateClick` was listed as a -schema key when it is a `CalendarViewProps` **component** prop, sending readers -to a different package's surface for a key `calendar-view` does not have (the -schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys -with nothing saying it was a summary (objectui#5045). - -The block now carries the requiredness the schema declares, names itself a -partial summary of `CalendarViewSchema`, and adds the author-facing -`defaultView` / `view` / `views` / `editable` / `date`. It also states plainly -what the registered `calendar-view` renderer actually reads — it builds events -from the node's `data` array and drops an authored `events` key (objectui#4433) — -so the corrected requiredness does not itself become a new wrong instruction. - -This is a documentation fix to a file `plugin-calendar` publishes to npm, which -is why it carries a version: the npm landing page only picks up the correction -on a release. No behaviour, export, type, or `dist` byte changes. The pin test -added alongside it publishes nothing. diff --git a/.changeset/catalog-layout-props-sweep-4891-4890.md b/.changeset/catalog-layout-props-sweep-4891-4890.md deleted file mode 100644 index 92f13f187..000000000 --- a/.changeset/catalog-layout-props-sweep-4891-4890.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/components': minor ---- - -`stack` now reads its spacing from `gap` and nothing else — the undeclared -`spacing` key it also accepted is gone (objectui#4890). - -`StackSchema extends Omit`, whose spacing key is `gap`. -`spacing` was declared by nothing: not the TypeScript interface, not the zod -mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway, -as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole -story, since it existed to get past the type system saying the key was not there. -A lenient consumer leg does not stay in the consumer: it becomes a second -de-facto contract that producers write to, and 135 nodes across 39 files of the -shipped schema catalog did exactly that. Every one of them rendered correctly, so -nothing ever pointed at it, while the examples went on teaching the key to every -author who copied them. - -The trap it was one edit away from springing: `flex` — semantically a `stack` -with a `direction` — never read `spacing`, so re-typing any of those nodes would -have dropped the spacing to the default silently. Fixed at the producer -(AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the -alias is deleted rather than legalised into `StackSchema`, where it would only -have been a second name for `gap`. - -**If you author `spacing` on a `stack`**, rename it to `gap`; the value and the -rendering are unchanged. A `stack` still carrying `spacing` now renders the -default gap, exactly as a `flex` always did. - -Also in the same sweep, and visible only in the published example catalog rather -than in any package API: 140 catalog nodes that were already `flex` / `stack` / -`container` stopped hand-writing their own declared props in `className` -(`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`, -`flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all -(objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in -`className`, because the props are not responsive. Both facts are ratcheted in -`examples/schema-catalog/test/layout-props-conversion.test.tsx`. diff --git a/.changeset/chat-transport-memo-4187.md b/.changeset/chat-transport-memo-4187.md deleted file mode 100644 index 93978f70c..000000000 --- a/.changeset/chat-transport-memo-4187.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -`useObjectChat` no longer rebuilds its `DefaultChatTransport` on every render -(objectui#4187). - -The transport `useMemo` listed the caller's `body` and `headers` in its dep list. -Both are object props and every caller passes a fresh literal each render — the AI -page's chat pane builds its `body.context` inline — so the memo never hit and a -transport was constructed on every render of every chat surface, which during a -streaming turn is once per token batch. - -`body` and `headers` are now read through refs inside -`prepareSendMessagesRequest`, the idiom this hook already uses for the live model -(`modelRef`) and the handoff conversation id (`parentConvRef`), and they are gone -from the dep list. Unlike memoizing at each call site, a future caller cannot -undo it. - -No user-visible behaviour changes: `@ai-sdk/react` keeps the transport in a ref -and re-keys its `Chat` only on `chat`/`id` (verified against the installed -4.0.68), which `useObjectChat` passes neither of, so the message thread was never -at risk — the rebuild was pure waste. The one real difference is *when* the two -values are sampled: a send now reads them at send time, so it observes the values -of the most recent render instead of those of the last render that happened to -rebuild the transport. That is never staler than before, and it is pinned by -`useObjectChat.transportIdentity.test.tsx`. diff --git a/.changeset/check-schema-positive-marker-and-skip-count.md b/.changeset/check-schema-positive-marker-and-skip-count.md deleted file mode 100644 index 84f3ac9f6..000000000 --- a/.changeset/check-schema-positive-marker-and-skip-count.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/cli': minor ---- - -`objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge. - -A root `type` was treated as a component key wherever it appeared. `type` heads at -least seven unrelated JSON vocabularies, and the most common of them is -`package.json`'s `"type": "module"` — so the first line a user saw running -`objectui check` in their own project was a warning about their own package -manifest. Measured at this repository's root: 46 warnings, 45 of them -`package.json` (objectui#5127). - -A file now enters type judgement only when its root carries a structural key -declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`, -`style`, the `visible`/`hidden`/`disabled` predicate family, `testId`, -`ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an -`.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply -never judged. The key set is read out of the node contract rather than invented, -and it is closed: it grows only when `BaseSchema` grows. - -A list of filenames to exclude was the alternative and was rejected: it is a -second hand-maintained list of the shape objectui#5115 had just finished -deleting, and it can only ever enumerate the foreign vocabularies someone already -thought of. This is a positive marker instead. - -Because the marker narrows what is checked, the command now also reports the -count of files that had a root `type` and no marker, together with the marker -keys that opt one back in. That number is the coverage this gate gives up until -schema files are recognisable, and printing it is what keeps the loss visible -rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was -never type-judged, before this change or after it. Exit codes are untouched: a -JSON parse failure remains the only thing that fails the run. - -No public `$schema` URL is introduced. An earlier revision also admitted a file -whose root `$schema` had an `objectui.org` host; the maintainer ruled against -minting that identifier (2026-08-20, objectui#5127), so the structural key is the -only marker. Because the matching was host-based rather than literal, that arm -can be added later without invalidating a single file. diff --git a/.changeset/cli-validate-field-widget-namespace-5449.md b/.changeset/cli-validate-field-widget-namespace-5449.md deleted file mode 100644 index 2984a2ef4..000000000 --- a/.changeset/cli-validate-field-widget-namespace-5449.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/cli': patch ---- - -`objectui validate` now refuses a form field whose widget id names a namespace -other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` -has given since objectui#5375 (objectui#5449). - -The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema -declared `type` and `widget` as bare optional strings — so a field typed -`ui:password` validated clean while the runtime validator rejected the same -document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an -author actually runs before shipping, so it was the one handing out the false -green: an author did exactly the diligence objectui#5375 asks for and still -shipped metadata that renders a secret into a plain text box. - -A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's -precedence (`widget` before `type`), the key it blames, its error code and its -message verbatim, so the two entry points cannot describe one defect two ways. - -**This rejects documents that previously validated.** Only colon-qualified -field widget ids outside the `field:` namespace are affected — `field:`-prefixed -ids and bare names such as `password` still pass, registered or not. A field -carrying, say, `type: 'ui:password'` must be rewritten as `password` or -`field:password`; it never rendered as a password box in any case. - -Which of the repo's authoring-time validators is canonical remains open -(objectui#4631) — this states the rule on the zod side rather than unifying -them. diff --git a/.changeset/cloud-connection-bind-failure-i18n-5054.md b/.changeset/cloud-connection-bind-failure-i18n-5054.md deleted file mode 100644 index 68a88aac2..000000000 --- a/.changeset/cloud-connection-bind-failure-i18n-5054.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -A cloud-connection bind failure now reads in the user's language whichever clock -noticed it (objectui#5054). - -One abandoned device approval could be noticed by either of two clocks, and the -Cloud Connection panel had a different answer for each. When the panel's own -`expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — -translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered -HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` -threw a bare `Error` carrying only that sentence, and the catch rendered it -verbatim. Same user, same failure, two languages, decided by which clock got -there first — visible on a zh console as the same abandoned approval reading -Chinese or English depending on whether the tab sat open past `expires_in`. - -`getJson` now carries the envelope's `declaredCode` and `code` across its throw, -and a single closed map turns the two RFC 8628 outcomes a user can actually cause -into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, -`access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten -locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream -spelling there — `code` is `DEVICE_CODE_FAILED` for both. - -Every other code is unchanged: `invalid_grant`, and anything upstream invents -next, still render the wire `message`, which stays the single source of truth for -failures this console has no copy for. No API, export or resolver was widened. diff --git a/.changeset/clusterradius-unit-doc-5020.md b/.changeset/clusterradius-unit-doc-5020.md deleted file mode 100644 index 00f5731ef..000000000 --- a/.changeset/clusterradius-unit-doc-5020.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-map': patch ---- - -`ObjectMapProps.clusterRadius`'s JSDoc said "in pixels"; `clusterMarkers` has -always used it as a coordinate-degree grid cell edge (`radius / 2 ** zoom`, -divided into the marker's `[lng, lat]` degrees), not a screen-space radius — -a host tuning clustering granularity by the documented unit would get a -completely different result than intended (objectui#5020). - -No behavior, default, or name changes: clustering, the >100-visible-marker -auto-threshold, and tap-through zoom are unaffected, and `clusterRadius` has -no call sites outside `plugin-map/src` today (re-confirmed repo-wide, -including `apps/`, `examples/`, and the `objectstack` spec/server repo — the -default of `50` is what runs everywhere). This is a doc-comment correction -only, bringing the JSDoc in line with the README's already-correct wording -(post objectui#5002). - -The latitude-anisotropy trade-off (a degree grid distorts east-west as -latitude rises) is a known design trade-off, not part of this fix. diff --git a/.changeset/componentinput-reexport-4972.md b/.changeset/componentinput-reexport-4972.md deleted file mode 100644 index 910e0a734..000000000 --- a/.changeset/componentinput-reexport-4972.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/types': minor ---- - -`ComponentInput` is now declared once and re-exported, instead of restated in three -places (objectui#4972). - -`@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' -plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) -were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now -re-exports of that one declaration, which is the disposition objectui#4580 ruled for the -identical shape — *a structural copy would reproduce the defect the moment either side -moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. - -Either side had already moved. `base.ts` declared thirteen keys; both copies declared -nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every -component registration actually imports**. Those four keys were unwritable at any real -registration — a plain TypeScript error at the call site — while `ComponentInputSchema` -(the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face -advertised four keys the authoring face rejected. Measured over the repository, no -registration had tried to write one yet, so nothing a user hits was broken today; what -changes is that the four keys become writable, and there is no longer a second -declaration for the next widening to miss. - -`ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single -declaration imported by all three sites (objectui#3832); this converges the rest of the -interface. - -Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is -byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported -here only with its control — a probe that added a *required* key to `ComponentInput` left -the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of -`export *` lines that names `ComponentInput` zero times. The gauge that can actually fail -is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does -`@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted -declarations that change in either package. - -`WidgetInput`'s union-arm capability is deliberately untouched — a different gate path -and a separate judgment. diff --git a/.changeset/componentrendererprops-reexport-4594.md b/.changeset/componentrendererprops-reexport-4594.md deleted file mode 100644 index 52cad5745..000000000 --- a/.changeset/componentrendererprops-reexport-4594.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/core': minor ---- - -`ComponentRendererProps` is now declared once and re-exported, instead of -hand-declared a second time in `@object-ui/core` (objectui#4594). - -`@object-ui/core`'s `ComponentRendererProps` (`src/types/index.ts`) was a -non-generic interface typing `schema` as `SchemaNode`, while -`@object-ui/types`' declaration of the same name is generic — -`ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` with -`schema: TSchema`. Same name, both exported from their package entry, from two -packages the same consumers import together: which declaration a call site got -depended on which package it reached for, and the two disagree about whether a -primitive node is admissible. Core's is now a re-export of types', which is the -disposition objectui#4580 ruled for `SchemaNode` two lines above it in the same -file, and objectui#4972 for `ComponentInput` — *a structural copy would -reproduce the defect the moment either side moved*. - -**Published-surface effect, and the reason it is not neutral.** Resolved -through the TypeScript checker from `core/dist/index.d.ts` over a clean rebuild -of both legs, `ComponentRendererProps` as reached through `@object-ui/core` -moves from non-generic with -`schema: BaseSchema | string | number | boolean | null | undefined` to -`ComponentRendererProps` with `schema: TSchema`, defaulting to -`BaseSchema`. `schema` therefore **narrows** back to the object form — core's -copy had silently widened when objectui#4608 made core's `SchemaNode` a -re-export of types' union — and the type gains a parameter. **Nothing imported -it**, on either side, re-verified repo-wide on the merged ref, so no call site -can observe either move; the narrowing is recorded here because it is a change -to a published type, not because a consumer is affected. - -A compile-time pin now holds the reconciliation from -`@object-ui/react` — the only position that resolves both packages through -`node_modules` — alongside the existing `SchemaNode` one. It is a test-only -addition and emits nothing, so `@object-ui/react` takes no bump of its own. diff --git a/.changeset/console-action-dispatch-envelope-5611.md b/.changeset/console-action-dispatch-envelope-5611.md deleted file mode 100644 index a7e8555f2..000000000 --- a/.changeset/console-action-dispatch-envelope-5611.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Internal: the console's admin-override notice now travels on a declared dispatch -type instead of a cast (objectui#5611). No published API and no runtime -behaviour changes, so this changeset declares "no release" rather than a bump — -`@object-ui/app-shell` source moved, nothing it exports did. - -`overrideNotice` is the safety copy shown once, ahead of a privileged admin -override that finalises an approval step over approvers who have not acted. Its -producer (`DeclaredActionsBar`) reached its reader (`useConsoleActionRuntime`'s -param-collection dialog) through a `dispatch as ActionDef` cast on one side and -`action?: any` on the other, so nothing declared the key anywhere and the two -could drift apart in silence — rename it on either side and the notice stops -appearing with every test still green, because each side's suite spells the key -itself. - -Both ends now share one declaration: `ConsoleActionDispatch` -(`ActionDef & { overrideNotice?: string }`), a HOST-composed envelope that lives -at the seam, in the one package where producer and reader both live. The cast is -gone and both param-collection handlers narrow off `any` — which is what puts -those functions under the compiler at all. - -The published `ActionDef` deliberately does NOT declare the key (maintainer -ruling 2026-08-22): it is the authored-metadata mirror, and `overrideNotice` is -the first key no author supplies, so declaring it there would make an unenforced -key legally writable in metadata. That prohibition still holds exactly as -written — `ActionDef` and `ACTION_DEF_KEYS` are unchanged. - -`@object-ui/core` is NOT untouched, and the reason is a separate declaration: -the same ruling's item 4 adds `HOST_DISPATCH_ACTION_KEYS` to the key inventory -so the dev-mode warning stops calling the host-composed key unknown. That change -carries its own changeset (`host-dispatch-action-keys-5611.md`) and its own -patch bump; this one remains the app-shell half, which publishes nothing. diff --git a/.changeset/console-boot-request-dedup-5544.md b/.changeset/console-boot-request-dedup-5544.md deleted file mode 100644 index 44270d6e8..000000000 --- a/.changeset/console-boot-request-dedup-5544.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The console's cold load no longer asks `/api/v1/runtime/config` or -`/auth/me/localization` twice (objectui#5544). - -Two pairs of boot callers were racing each other for the same URL, with no shared -provider between them, so no guard inside either component could see the other: - -- `GET /api/v1/runtime/config` — the pre-React branding script inlined in - `apps/console/index.html` (it runs during HTML parse so the tab title and - favicon are the operator's before the bundle is fetched) and - `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the - expensive one: the console `await`s `initRuntimeConfig()` before - `createRoot().render()`, so the duplicate sat on the critical path to first - paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed - boot concurrency further past the server's pool knee. -- `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true - first visit and `LocalizationFetchProvider` on every boot. The seed keeps - running past its 500 ms race by design and the provider mounts the moment that - race resolves, so on a first visit the two overlap. Measured ×2 on staging. - -`@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET -while one is already in flight join that request instead of starting another. It -shares the in-flight promise and nothing else — the entry is deleted the instant -the request settles, so there is no cache, no TTL and no stale window, and a -caller arriving after settle fetches fresh exactly as before. Rejections fan out -to every sharer with the status intact (`LocalizationFetchProvider`'s retry -policy still sees its own 503), each caller receives its own copy of the parsed -body, and only GETs are eligible — a non-GET is refused rather than quietly -rewritten. - -Requests that differ in credentials mode or headers keep separate identities, so -the console's two deliberate `auth/get-session` calls — one Bearer-only with the -cookie omitted to detect a stale token, then one through the cookie — stay two -requests. Collapsing those would have destroyed the signal the first one exists -to read. - -No component receives anything different: same payloads, same errors, one fewer -round trip. diff --git a/.changeset/console-form-container-specs-one-declaration-5596.md b/.changeset/console-form-container-specs-one-declaration-5596.md deleted file mode 100644 index 6f199b8c8..000000000 --- a/.changeset/console-form-container-specs-one-declaration-5596.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The two form CONTAINER contracts now have ONE declaration each, derived from -`@objectstack/spec`, and the console reads them instead of its own copies. - -objectui#5542 converged the LEAF of this contract — the field spec — and left the -two containers above it untouched, because converging them was a bigger call than a -mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared -twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once -in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean -subset — these two had **already drifted, in both directions**, so neither copy was a -subset of the other and there were two live answers to "what may an author write": - -- `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; - the console declared none of them. The console's `columns` admitted the string arm - (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. -- `FormViewSpec` — the console declared `label` / `groups` / `sharing` / - `submitBehavior`; app-shell stopped at `type` plus `sections`. - -The drift is decided by asking the **contract**, not by picking a side. `columns` -does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` -with the four numeric literals, folded to a number by its own transform), so -app-shell's numbers-only declaration was rejecting metadata the platform accepts — -objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is -the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured -against the installed `@objectstack/spec` 17.0.0), because a form config is titled, -not labelled. The value that read actually finds is the VIEW's identity label, which -arrives on the `ExpandedViewItem` envelope or beside the config on a flattened -runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to -the body it unwraps, rather than smuggled onto the form contract. - -Both types are therefore **derived from the spec's own `FormSection` / `FormView` -with named narrowings** — the repo's sanctioned form for a spec-shaped local type -(`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the -two layers agree on comes from the spec and cannot fall behind it; the four positions -where this layer is deliberately narrower are each named in an `Omit` list and -restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving -it would silently re-open #5542), and `label` / `description` / `visibleWhen` / -`visibleOn` keep the shapes this repo's renderers and evaluators actually consume -rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s -`submitBehavior` union — previously hand-written under the comment "Mirrors the spec -FormView.submitBehavior union" — is now read back off the shared type, making the -mirror structural. `@object-ui/app-shell` re-exports both names from its package root -(type-only, erased at build — nothing is added to the bundle), because a type that -cannot be imported is a type that gets retyped. - -The pins are what make future drift loud, and each half is pinned on both sides. -`form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the -non-narrowed half of each type against the spec's own symbol, so re-hand-writing -either declaration fails `type-check` the day the spec moves rather than years later -when someone reads two files side by side — and the console's pins read both types -back out of the **exported** `buildSections` signature rather than naming them, so a -re-inlined local copy fails even if it agrees on every key on the day it is written. -Their liveness controls are what stop them being phantom checks: the removed copies -are pinned NOT equal to the shared types (proving the `Equal` helper still -discriminates), the renderer's honoured `RenderableSection` is pinned not equal -either (so the authored-document and honoured-row types cannot be collapsed again), -and an undeclared key is still rejected (so the derivation smuggled in no index -signature or `any`). Every narrowing carries a matching negative pin, so "derived" -cannot quietly become "widened to whatever the spec says". - -Behaviour is unchanged — the runtime always accepted these keys. The vitest halves -prove it: a section spelling its column count as the string `'3'` lays out identically -to the numeric `3` on both sides, and a section carrying the keys only one side used -to declare builds the same rows. diff --git a/.changeset/console-form-field-spec-one-declaration-5542.md b/.changeset/console-form-field-spec-one-declaration-5542.md deleted file mode 100644 index 59b1a9fdc..000000000 --- a/.changeset/console-form-field-spec-one-declaration-5542.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -The form-field authoring contract now has ONE declaration, and the console reads it -instead of its own copy. - -objectui#5040 was not a missing key. It was that **two hand-written descriptions of -one contract drifted**, and nothing could notice, because each was only ever checked -against itself. PR #5537 converged the two app-shell descriptions into -`views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: -`FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same -name, in a different package — so the same failure mode stayed fully available. - -Measured key by key before choosing a route, because the two honest outcomes are -"same contract, import it" and "genuinely narrower layer, rename it and pin the -subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, -every one identical in type, none console-only — and it sat in a position that -describes an **authored document**: `FormSectionSpec.fields`, read straight off the -`/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both -files even spell the same six-member `type` union and call the element type -`FormFieldSpec`). The narrow, renderer-honoured shape is a different type that -already exists in that file, `RenderableField`. So this was one contract described -twice, and the console's description was wrong about the document: legal metadata — -`visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, -and ten more keys — was undeclared there. That is #5040's own symptom, "the type -rejects the configuration the runtime accepts", which no runtime test can see. - -`@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root -(type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` -imports it and deletes the local declaration. Reachability is the load-bearing half: -a type that cannot be imported is a type that gets retyped, and retyped copies drift. -`form-spec.ts` itself is untouched. - -`FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the -field-spec type back out of the **exported** `buildSections` signature rather than -naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even -if the copy agrees on every key on the day it is written — which is exactly what did -not happen to the copy this change removes. Its liveness controls are what stop it -being a phantom check: the removed nine-key shape is pinned NOT equal to the shared -type (so the `Equal` helper is proven to still discriminate), `RenderableField` is -pinned not equal to it either (so the honoured-row and authored-document types cannot -be collapsed again), and an undeclared key is still rejected (so the import did not -smuggle in an index signature). Behaviour is unchanged: the runtime always accepted -these keys, and the vitest half proves the same rows are built. diff --git a/.changeset/console-formpage-visible-predicates-5594.md b/.changeset/console-formpage-visible-predicates-5594.md deleted file mode 100644 index 60c95e1a1..000000000 --- a/.changeset/console-formpage-visible-predicates-5594.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's standalone form renderer now evaluates conditional field visibility. - -`apps/console/src/components/FormPage.tsx` is a **second, independent form renderer** -— its own `buildSections`, its own JSX — and it serves both the public -`/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of -the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` / -`visibleOn` *read* inside that file returned zero. So a field an author conditioned on -`record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec` -normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both -authors and honours — rendered unconditionally on both routes. Fail-open and silent: -the author saw the field always, with no diagnostic. - -objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different -chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form` -`sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx` -is on that chain at no point, and #2212's regression pin lives with the chain it fixed, -so nothing in the suite could see this copy. One contract, two implementations, each -only ever checked against itself. - -The wiring is **#2212's ruling applied verbatim** rather than a second predicate -semantics invented for this renderer, because two form renderers disagreeing about what -`visibleWhen` *means* would be a worse defect than one renderer ignoring it. The -predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`, -`evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and -`{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*` -= the stored record an edit form started from), and the fail-open-but-loud behaviour on -an unevaluable predicate are the shared ones by construction. Resolution is -canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers: -`sectionFields.ts` and app-shell's `readVisibility`. - -Two things deliberately did **not** change. A field hidden by its predicate still -submits its value — conditional visibility is a rendering rule in both renderers, and -making it a submit-payload rule would be a new contract decided once for both, not -invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain: -the second-renderer question is real, but it belongs with the #5596 convergence track, -not with a predicate that is dead today. - -`FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this* -renderer on purpose — a pin that cannot see the second copy is how the first gap -survived. With the fix reverted and the pin in place the suite reports -`11 failed | 1 passed (12)`; the one green is the control that has to be green (a field -with no predicate still renders), without which every "the field is absent" assertion -would be equally satisfied by a renderer that draws nothing at all. diff --git a/.changeset/console-lazy-docs-portal-5467.md b/.changeset/console-lazy-docs-portal-5467.md deleted file mode 100644 index 9b44325e8..000000000 --- a/.changeset/console-lazy-docs-portal-5467.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to. - -`AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the -app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same -three statically for the platform portal at `/docs` (ADR-0046 section 6), so all -of them sat in the eager graph regardless and the `import()` moved nothing — -three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build` -(objectui#5467). A static import on either side silently defeats the split for -both, and the only signal is a build warning that fails nothing. - -`App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`, -matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even -though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot, -so nothing imported `DocsIndex` dynamically, but left static it alone would keep -`DocShell`, `use-book-data` and `book-nav` eager and the portal would only -half-leave the closure. - -Measured on this branch with the `dist/eager-closure.json` gauge added by -objectui#5324, both builds exiting 0: - -| | before | after | -|---|---|---| -| `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 | -| eager closure, gzipped | 3,881,609 B | 3,870,058 B | -| eager chunks | 58 | 52 | - -Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz), -`CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B), -`componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown -folds into the entry chunk rather than dropping — which is why the entry chunk -grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by -11,551 B. The entry stays far under that budget's 350 KB line, and the eager -closure is the number a page load actually pays. - -What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this -looked like a bigger win than it is. Three eager chunks import it statically, -and only one of them was this portal: `plugin-chatbot` reaches it directly, and -`packages/fields`' `MarkdownContent` — lazy in source — is folded into the -eagerly imported `ui-components` chunk by the `advancedChunks` group that claims -every `packages/fields` module. That is objectui#5325's mechanism, not this -card's, and it is why the saving here is 0.30% rather than 4%. diff --git a/.changeset/console-studio-builder-decorative-lazy-5486.md b/.changeset/console-studio-builder-decorative-lazy-5486.md deleted file mode 100644 index e557f79f0..000000000 --- a/.changeset/console-studio-builder-decorative-lazy-5486.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing. - -The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a -`Suspense` fallback — naming the same barrel the line above it imports -statically for `registerAppComponent`, and the same barrel `App.tsx` pulls -`BuilderLanding` from to render the standalone `/studio` landing full-screen. -Either reason alone makes the `import()` unable to move a module into another -chunk (objectui#5486). - -**This moves no modules and is not a bundle improvement.** `BuilderLanding` was -already in the eager graph via `App.tsx` and still is. Measured on -`dist/eager-closure.json`, both builds exiting 0: the eager closure holds the -same 52 chunks with the same names, and the only difference is 130 B gzipped -(413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback -text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure, -because nothing could. - -What it does fix is honesty. The old code told every reader the builder was -deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every -console build — the console's count of those drops from 44 to 43, with the 43 -remaining ones all belonging to the `packages/fields` barrel (objectui#5325). -A permanent warning that fails nothing is how a team learns to skim past build -warnings, and a decorative `lazy()` is how the next reader learns something -false about the chunk graph. - -The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx` -lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in -statically, and is genuinely deferred; it is untouched. Making the *builder* -genuinely lazy would mean taking `App.tsx` off the static import too, changing -how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind -the barrel — a separate measured card, not folded in here. diff --git a/.changeset/console-user-preference-request-budget.md b/.changeset/console-user-preference-request-budget.md deleted file mode 100644 index a629873f7..000000000 --- a/.changeset/console-user-preference-request-budget.md +++ /dev/null @@ -1,6 +0,0 @@ ---- ---- - -Tests only: pin the console `sys_user_preference` request budget across one -mount (objectui#5544). Three distinct preference keys, one read each, and every -consumer still receives its own row. No runtime code changes, nothing to release. diff --git a/.changeset/current-user-default-prefill-5683.md b/.changeset/current-user-default-prefill-5683.md deleted file mode 100644 index b519b0fb6..000000000 --- a/.changeset/current-user-default-prefill-5683.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/permissions': minor -'@object-ui/plugin-form': minor ---- - -Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. diff --git a/.changeset/dashboard-self-contained-unreachable-arm-4620.md b/.changeset/dashboard-self-contained-unreachable-arm-4620.md deleted file mode 100644 index 7d8e70c29..000000000 --- a/.changeset/dashboard-self-contained-unreachable-arm-4620.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained -(card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally -(objectui#4620). - -`isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the -`isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The -`datasetBound` side of that inner fork could never execute: reaching it required -`isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged — -the removed arm never ran, and the reachable fork in the Card branch (the one that gives -a dataset-bound metric its title and border chrome) is untouched. - -The cost was to readers, not to users: the shape read as "both branches handle -dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto -`DashboardGridLayout` had to pay for the reachability argument before it could decline to -copy the dead limb. A comment now names the invariant in place so the arm is not re-added. diff --git a/.changeset/dashboard-stable-empty-rows-4629.md b/.changeset/dashboard-stable-empty-rows-4629.md deleted file mode 100644 index 9fc980654..000000000 --- a/.changeset/dashboard-stable-empty-rows-4629.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for -"no rows yet" instead of a fresh array literal per render (objectui#4629). - -Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so -whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind` -path that resolves to an object — the fallback produced a NEW array identity on -every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so -every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the -`isSystemField` pass, the `fieldLabel` lookups) and then discarded by the -`finalData.length === 0` early return. In `ObjectPivotTable` the value is handed -straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot -rebuilt its row/column sets, bucket map and totals on every render over no rows -at all. - -Nothing rendered wrong before or after; this is wasted work in the empty window, -plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is -the same module-scope frozen empty `data-table.tsx` adopted for its own -`EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings. diff --git a/.changeset/dashboard-table-header-one-spelling-5425.md b/.changeset/dashboard-table-header-one-spelling-5425.md deleted file mode 100644 index 8a0de721b..000000000 --- a/.changeset/dashboard-table-header-one-spelling-5425.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does. - -`ObjectDataTable` derives headers on two paths — from the author's declared -`columns`, and from the object schema when no columns were declared. The -declared path (and the static `data-table` half of the same widget family) -already used `humanizeFieldKey`, whose docstring names it "the single home for -the convention, because both halves of the `table` widget family need it and -they must agree". The auto-derived path carried a third, inline spelling that -split camelCase but never turned `_` into a space, so it left a raw underscore -on screen. Measured over the same object's columns: - -``` -path close_date needs_analysis -object-bound, AUTO-DERIVED (before) Close_date Needs_analysis -object-bound, AUTO-DERIVED (after) Close Date Needs Analysis -object-bound, DECLARED columns Close Date Needs Analysis -static `data-table`, no columns Close Date Needs Analysis -``` - -One dashboard can hold all three widgets over one object, so a single field key -rendered under two spellings — the defect class objectui#5425 rules out. The odd -path adopts the shared convention rather than the convention gaining a fourth -dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and -after — the coincidence that kept the snake_case divergence unnoticed), and a -translated header still wins: only the fallback handed to `fieldLabel` changed. - -Dimension MEMBER labels are untouched by this. The same card reported dashboard -members rendering a prettified enum instead of the picklist's translated label, -measured on 17.1.0; re-measured on this branch it no longer reproduces — the -analytics label net shipped in 17.5.0 routes every non-metric dataset dimension -through the field's declared options and the locale bundle. That behaviour had -no test stated in the card's terms and now has one, over the four dashboards the -card measured, including the property that a bar axis and a pivot header cannot -disagree about one stored value. diff --git a/.changeset/dashboard-title-locale-writeback-5428.md b/.changeset/dashboard-title-locale-writeback-5428.md deleted file mode 100644 index 849b85b70..000000000 --- a/.changeset/dashboard-title-locale-writeback-5428.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-designer': patch -'@object-ui/app-shell': patch ---- - -A widget title stored as an inline per-locale map is editable again in both dashboard -authoring surfaces, and a save writes back only the active locale's entry -(objectui#5428). - -`@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at -17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both -authoring panels edit a title in ONE single-line input. Writing the input's value back -as the whole value would collapse every other locale on the first keystroke, so both -surfaces took the same conservative branch: show a map-valued title resolved, and make -it READ-ONLY. - -That branch could not lose data, but it rested on a premise the spec had already -invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was -plain `string` through rc.5" — stated sixty lines below a comment in the same file -documenting the rc.6 widening that makes a stored map reachable. Both could not hold. -The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward -was not protect an unreachable path: it denied an author the ability to edit a widget -title in their own locale. - -objectui#5301's maintainer ruling settled the write rule for the sibling surface — a -save replaces only the active locale's entry and preserves the others — and -`@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because -the read and the write have to agree. Both panels now adopt it: - -- `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; -- `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. - -A plain-string title keeps saving as a plain string, so the common path is unchanged. -An edit made in a locale the stored map does not carry ADDS an entry under that locale -rather than overwriting the entry the display fell back to. - -The pins are preservation pins, not "the input is editable" pins: at both surfaces a -keystroke on a map-valued title must leave every other locale's entry byte-identical. -Reverse-verified by mutating each write back to the flattening form and confirming those -assertions go red at both surfaces. - -Not a multi-locale editor: an author still reaches only the entry for the locale they -are in. Authoring every locale from one panel remains an open product question. The -stale deferrals both comments carried pointed at objectui#4163, which closed as -completed on 2026-08-15 while the placeholders were still in the tree; they are replaced -with the rule that is actually in force rather than re-pointed at another tracker. diff --git a/.changeset/dashboard-widget-dataset-stale-cast-5067.md b/.changeset/dashboard-widget-dataset-stale-cast-5067.md deleted file mode 100644 index ba87f15ec..000000000 --- a/.changeset/dashboard-widget-dataset-stale-cast-5067.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Internal only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`DashboardRenderer.tsx`'s dataset-bound check (`const datasetBound = !!(widget as -any).dataset`) drops its `as any` (objectui#5067). The comment's stated reason — -"the bundled DashboardWidget type gains `dataset` only after objectui bumps -`@objectstack/spec`" — is stale: the repo already carries `@objectstack/spec@17.0.0`, -whose `DashboardWidget` declares `dataset`, and `packages/types/src/complex.ts`'s -`DashboardWidgetSchema` inherits it through its `extends Omit, -…>` (`dataset` is not in the `Omit` list). `widget.dataset` type-checks directly; the cast -was pure redundant widening, and a harmful one — after `as any`, `.dataset` reads as -`any`, so a future spec change to that key's shape would not go red here. - -**No runtime behaviour changes.** `!!(widget as any).dataset` and `!!widget.dataset` -evaluate identically for every input; `as any` is a compile-time-only annotation. Proven -with a reverse check: temporarily typo'ing the property to `widget.datasetTypoXYZ` turns -`pnpm --filter @object-ui/plugin-dashboard type-check` red with `TS2339: Property -'datasetTypoXYZ' does not exist on type 'DashboardWidgetSchema'`, confirming the removed -cast was suppressing real type coverage rather than papering over a genuine gap. - -The ADR-0021 point the original comment made — a dataset-bound widget renders through -the governed `queryDataset` path (`DatasetWidget`) instead of the inline object-aggregate -schema — is unchanged and kept; only the now-false justification for the cast is rewritten. diff --git a/.changeset/data-table-header-declared-key-5351.md b/.changeset/data-table-header-declared-key-5351.md deleted file mode 100644 index c0b154de2..000000000 --- a/.changeset/data-table-header-declared-key-5351.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/components': minor -'@object-ui/plugin-dashboard': minor -'@object-ui/plugin-detail': minor ---- - -`data-table` reads the declared `header`; the producers translate `label` into it. - -`TableColumn` declares `header: string` and does not declare `label`. The -renderer's column normalization nonetheless read `header: col.header || col.label`, -so the same key had one spelling the type admits and one only the runtime did. -That alias is gone (objectui#5351), and the translation it used to perform happens -once at each producer instead: metadata vocabulary in, adapter vocabulary out. - -**This narrows what `data-table` accepts, so read this if you author `data-table` -nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a -directly authored `data-table` now renders a **headerless** column over live -cells. Spell it `header` — the key `TableColumn` has always declared. Columns -reaching `data-table` through `object-data-table`, `object-grid` or a related -list are unaffected: those producers resolve `header` for you from the spec's -`ListColumnSchema.label`, so every spelling they accepted before they still -accept. - -`@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader -producers use to cross that boundary. It is adapter-first (`header` wins over -`label`), so an author who addressed the table directly is never overwritten. - -`object-data-table` also gains a fix from the same move: a column carrying a -`label` used to render a **blank** header there even while the alias existed, -because the widget's field-meta enrichment overwrote the authored `label` before -the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". - -The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here -and still resolves. Retiring it is objectui#5120's remaining step, which is -gated on two published skill guides that teach that spelling. diff --git a/.changeset/date-range-default-range-binding-4984.md b/.changeset/date-range-default-range-binding-4984.md deleted file mode 100644 index 60e729853..000000000 --- a/.changeset/date-range-default-range-binding-4984.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`DashboardComponentSchema.dateRange.defaultRange` is now bound to -`DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a -hand-written 14-member union (objectui#4984). - -The union was byte-faithful to the spec — all 14 members, same order — so nothing -a user hits changes today. What was missing is the tie that keeps it faithful: -`resolveDashboardFilterDefs` takes `Pick`, so this union is what typechecks every TS-constructed dashboard, and -a preset the spec ADDS would have been a legal document that objectui's own types -said could not exist — the "narrower than the contract it implements" shape whose -consequence in objectui#4163 was that the bad reads were invisible to `tsc`. - -No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union -on an interface member has no symbol to collide with, while rule 2's claim heuristic -was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes -the file's existing "Aligned with @objectstack/spec" comment structural rather than -prose. - -The emitted `.d.ts` collapses the inline union to the imported alias; the published -type surface is unchanged — measured with the TypeScript checker over the emitted -declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, -and `defaultRange` resolving to the same 14 string-literal members before and after). diff --git a/.changeset/dead-refresh-callback-objectview.md b/.changeset/dead-refresh-callback-objectview.md deleted file mode 100644 index 6f077fa36..000000000 --- a/.changeset/dead-refresh-callback-objectview.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Internal only, no released behaviour change: `plugin-view`'s `ObjectView` no -longer declares a `handleRefresh` callback that nothing referenced. - -The callback bumped `refreshKey` but was never passed to the toolbar, exposed on -a handle, or wired to any control — it advertised a refresh entry point the -component does not have, which cost objectui#4549 a detour to rule out. The -reachable refresh paths are unchanged (`onMutation` auto-subscribe, delete, bulk -delete, form success), and the real toolbar Refresh button continues to live in -`plugin-list`'s `ListView`, reached through `renderListView`. diff --git a/.changeset/default-children-retired-5051.md b/.changeset/default-children-retired-5051.md deleted file mode 100644 index 0605f03d1..000000000 --- a/.changeset/default-children-retired-5051.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor -'@object-ui/components': minor ---- - -The register-meta key `defaultChildren` is retired (objectui#5051). - -It was declared in four places, produced in eleven, and read in **none**. The designer's -drop path builds a new node from its twin key only — `PageDesigner.tsx`, -`props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item -that declared `defaultChildren` dropped an **empty** node and the declared children never -materialised. Nothing rendered the wrong thing; an entire declaration surface was simply -inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer -ruling of 2026-08-19, the key is removed rather than wired up; if designer -default-children UX is ever product-wanted it returns as its own designed card. - -**If you author plugins against the published register-meta table, drop the key.** It is -gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A -meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, -and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the -key is silently dropped from the parse output instead of failing validation. TypeScript -authors get the loud signal instead: all three `ComponentMeta` declarations -(`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no -longer offer it, so re-declaring it is now a compile error. - -**No runtime behaviour changes in either direction.** No code path read the key before -this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were -feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types -into the designer produces exactly the node it produced yesterday. - -Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key -is absent from the parse output, with a surviving sibling asserted present through the -same parse as the control) plus the two TS twins with `@ts-expect-error`, and -`packages/core` pins the registration surface the eleven producers were written against. -Both are compile-time-enforced through each package's chained `tsconfig.test.json`. diff --git a/.changeset/delete-dead-release-workflow-5405.md b/.changeset/delete-dead-release-workflow-5405.md deleted file mode 100644 index b1c00f27a..000000000 --- a/.changeset/delete-dead-release-workflow-5405.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -CI/docs only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -Deletes `.github/workflows/release.yml`, a workflow that never ran once in this -repository's history, and removes the two places on -`content/docs/guide/ci-cd-pipeline.md` that documented it as a release path. - -It triggered on `push: tags: ['v*']`. No tag matching that glob has ever existed here: -of the 2896 tags on the remote, every one is a Changesets per-package tag -(`@object-ui/@`) and not one begins with `v`. Nothing creates tags except -the Changesets action, so the trigger had no way to fire. - -It is obsolete rather than misconfigured, which matters because the two have opposite -fixes. The workflow parsed and registered fine (GitHub lists it `active`) and would fire -if a `v*` tag were pushed — the job it would do, "Create GitHub Release", is simply -already being done by `changeset-release.yml`, whose Changesets action publishes GitHub -Releases tagged `@object-ui/@`. Its own npm publish step was still -commented out under "Uncomment the following steps when ready to publish to npm" while -the repo has been publishing to npm through Changesets for months, and its one-version -model (a single `v` for the whole repo, pointing at the root `CHANGELOG.md`) -never matched the 39-package fixed group this repo actually releases. - -The doc edit is not optional housekeeping: `scripts/__tests__/ci-cd-pipeline-doc.test.ts` -pins that page to `.github/workflows/` in both directions, so a page still naming -`release.yml` after the file is gone fails "never names a workflow file that does not -exist". - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/dom-leak-sweep-components-5574.md b/.changeset/dom-leak-sweep-components-5574.md deleted file mode 100644 index ce389acf8..000000000 --- a/.changeset/dom-leak-sweep-components-5574.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The one file changed is -`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`, which -`packages/app-shell/tsconfig.json` excludes from the build program -(`"exclude": [… "**/*.test.tsx"]`). - -Measured rather than asserted, because emitted-output behaviour varies per package: -`packages/app-shell/dist` was built twice from a cleared `dist/` **and** a cleared -`tsconfig.tsbuildinfo` — once at the `origin/main` version of that file, once at this -branch's version — and sha256-compared. 862 emitted files, 431 of them `.d.ts`, every -hash equal. (The first attempt at this measurement read as an empty `dist/`: `tsc` is -`composite`, so with the build info left in place it skipped the emit entirely and -produced no output to compare. Clearing the build info is part of the measurement, -not a detail.) - -Widens the objectui#3291 DOM-leak canary sweep to `packages/components/src/renderers/**`. -That family — 158 registry-reachable types across five namespaces — was outside the -gate's discovery entirely, which is why `ui:grid`'s leak had to be found by hand. The -first run records a ledger: 119 of 158 targets leak, in eight measured shapes, every -renderer named. Nothing is skipped or allow-listed; the per-target assertion stays exact -set equality in both directions, so a renderer fix cannot go green until its ledger row -is deleted in the same change. diff --git a/.changeset/dts-build-exit-code-5370.md b/.changeset/dts-build-exit-code-5370.md deleted file mode 100644 index b6fe0bc6d..000000000 --- a/.changeset/dts-build-exit-code-5370.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Build tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched: `@object-ui/layout`'s `vite build` -now exits non-zero when the declaration step reports type errors, instead of printing them -and exiting 0 (objectui#5370). The typings and the JavaScript it emits are unchanged. diff --git a/.changeset/dts-explicit-extensions-5365.md b/.changeset/dts-explicit-extensions-5365.md deleted file mode 100644 index 73de8c9d3..000000000 --- a/.changeset/dts-explicit-extensions-5365.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/layout': patch ---- - -The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. - -`vite-plugin-dts` emits one declaration file per source file, and TypeScript -copies a module specifier into the declaration verbatim. `export * from './ui'` -therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in -`@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted -trees. Node16/NodeNext resolution does not extension-search a relative -specifier, so the compiler could follow none of the hops and every symbol they -carried read as absent from the package: - -``` -error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. -``` - -Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls -in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from -`layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. -On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. - -The emitted `.js` never had the defect — rolldown resolves the same specifier -away — which is why `pnpm check:esm-specifiers`, whose verdict is about -specifier-preserving `.js` builds, correctly never scanned either package. The -fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, -shared by both `vite.config.ts` files), not in the sources: the same source line -produces a clean `.js` and a broken `.d.ts`, so no source edit can express the -difference. The rewriter resolves each specifier against the source tree the -output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — -throws on anything it cannot resolve, and after the build re-parses the emitted -declarations to assert every relative specifier both carries an extension and -names a file the build really emitted. - -`packages/fields` takes the `nodenext` pin as a result — the same two lines -`packages/react` has carried since objectui#4538 — so the property is enforced by -the compiler on the consumer side rather than by review. `packages/app-shell` -does not: it type-checks clean without the pin and still shows 23 errors with it, -none of them from these two packages. That residue is filed separately. diff --git a/.changeset/eager-closure-budget-5324.md b/.changeset/eager-closure-budget-5324.md deleted file mode 100644 index 230d59670..000000000 --- a/.changeset/eager-closure-budget-5324.md +++ /dev/null @@ -1,45 +0,0 @@ ---- ---- - -CI and build tooling only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. No package `src/` is touched. - -The console "performance budget" now weighs the **eager closure** instead of one chunk. - -`.github/workflows/performance-budget.yml` gzipped `apps/console/dist/assets/index-*.js` -and compared it against a 350 KB line. Measured on `77f846a8b`, that chunk is 25,910 bytes -gzipped, while the closure it statically pulls in — every chunk the browser must fetch and -parse before the app renders — is 3,881,609 bytes across 58 of 507 chunks. The gate passed -on 0.67% of the payload it claimed to govern, and `advancedChunks` routes vendor and -workspace code into named chunks on purpose, so most regressions land outside the entry -chunk. objectui#5266 is the worked example: 89 KiB gzipped added to every page load, landing -in `vendor-objectstack-*.js`, structurally invisible here (objectui#5324). - -`emitEagerClosureReport` in `apps/console/vite.config.ts` walks rolldown's own -`chunk.imports` from the entry chunks — static edges only, because the dynamic edge is the -lazy boundary — gzips the bytes actually written to disk, and writes -`dist/eager-closure.json`. `scripts/check-eager-closure-budget.mjs` applies the ceiling. -The split is deliberate: a size ceiling enforced inside `vite build` would fail every -Vercel preview and every local build, which is how a budget gets switched off rather than -fixed. Exit codes are distinct — `1` over budget (a verdict about the bundle), `2` no -trustworthy measurement (a verdict about the gauge) — so a broken gauge is never reported -as a clean bundle, and vice versa. - -Every check in that path is a counter-probe, because this gate's failure mode is silent: a -walk that finds too little, a stale report, an absent field read as zero all produce a -SMALL number, and a budget reads a small number as good news. So the build refuses to -publish a figure unless `react-dom` is inside the closure and at least one chunk is outside -it, and the checker refuses a report whose totals disagree with its own chunk list, whose -version it does not recognise, or that has collapsed to its entry chunk — that last one -being precisely the gauge this replaces. - -The ceiling is 3,960,000 gzipped bytes: today's measurement plus 78,391 bytes of headroom. -It passes on current `main`, and the headroom is deliberately narrower than the 89 KiB -regression the gate exists to catch, so a repeat of objectui#5266 fails it (verified: the -baseline plus 89 KiB comes out 12.4 KB over). Both constraints are asserted in -`scripts/__tests__/check-eager-closure-budget.test.ts`, not merely argued in a comment. - -This is a truthful current-state ceiling, not a target. 3.79 MB gzipped before first render -is a bad payload and the honest long-term line is far below it; lowering it is a separate -decision with its own work behind it. The entry-chunk budget and its 350 KB line are -unchanged — replacing a blind gauge is not licence to drop the check that was already there. diff --git a/.changeset/esm-load-comment-mask-5382.md b/.changeset/esm-load-comment-mask-5382.md deleted file mode 100644 index 70abee6d0..000000000 --- a/.changeset/esm-load-comment-mask-5382.md +++ /dev/null @@ -1,41 +0,0 @@ ---- ---- - -CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched. - -Fixes the comment mask in `scripts/check-node-esm-load.mjs` (objectui#5382). Leg 1 of -the gate blanked comments out of every source with two ordered regexes and then matched -specifiers in the result. The block-comment pass ran first and had no notion of already -being inside a `//` line, so a slash-star sequence occurring in ordinary line-comment -prose — a package glob, a path pattern, a wildcard import, all of them ordinary here — -opened a comment that ran to the next closing delimiter anywhere in the file and blanked -every line between, live code included. - -Re-measured on `main` at 478ec54ce over the 805 emitted sources of the 13 -specifier-preserving packages: the mask found 2132 relative specifiers and the TypeScript -parser found 2133. The one it could not see is a real `import` in -`packages/app-shell/src/preview/DraftChangesPanel.tsx`, hidden by a line comment naming -the `@objectstack` chunk group by glob two lines above it. - -That direction is the bad one. Since `SPECIFIER_DEBT` emptied, leg 1 is a hard -requirement rather than a ratchet, so a blind spot in it is somewhere a regression can -sit permanently while the run reports clean and only the nightly load leg can see the -consequence. - -Leg 1 now reads each file exactly as written and takes its module edges from -`check-phantom-dependencies.mjs`'s shared TypeScript scanner — the same one -`check-package-self-import.mjs` uses, so three gates cannot drift apart on what a module -edge is. Comments, strings, template literals and regex literals stop being questions -this gate has an opinion about. Reported line numbers stay the compiler's: the specifier -literal is located inside the statement the parser already identified, which matters -because `tsc` reports this class at the specifier and the statement opens on a different -line for 255 of 2066 relative specifiers here. - -The gate's verdict is unchanged — 0 findings before, 0 findings after — because the -newly visible import already carries its `.js` extension. What changed is that it is now -visible. The whole cheap leg went from 0.71s to 2.43s. - -`readTsconfig()` in the same script strips comments with the same kind of -context-unaware regex and is a separate live instance of this class. It is deliberately -untouched here and remains open as objectui#5367. diff --git a/.changeset/evaluateexpression-jsdoc-links-5580.md b/.changeset/evaluateexpression-jsdoc-links-5580.md deleted file mode 100644 index d8167b195..000000000 --- a/.changeset/evaluateexpression-jsdoc-links-5580.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction` -JSDoc are now qualified, so each resolves to the entity it means (objectui#5580). - -`ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method -on `ExpressionEvaluator` (bare expression, throws) and the module-level export -(context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block -referred to both under the one spelling, four lines apart. - -The prose link was not merely ambiguous, it was bound wrong. Measured with -`checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}` -resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the -sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}` -binds to the method, but only because no module-level `evaluate` exists to outrank -it, so the rule "an unqualified link resolves to the enclosing class's member" does -not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`, -which the checker resolves to the `MethodDeclaration`. - -The `@example`'s final line is the module-level export — its second parameter is a -context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat -two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover -carries no import to disambiguate. It now names the module-level export and shows the -import it needs. - -This is prose only: the diff is confined to a block comment and no declaration moves. -It is scored `patch` rather than the empty-frontmatter form because the block is -emitted into what npm ships — measured, this edit moves both -`dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js` -(this package builds with a bare `tsc`, which preserves comments in the JS emit), and -the ten changed lines in that JS are all comment lines. - -`registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather -than asserting it in prose, since a `{@link}` that binds to the wrong entity is -indistinguishable in source from one that binds right. diff --git a/.changeset/filecell-error-slot-5431.md b/.changeset/filecell-error-slot-5431.md deleted file mode 100644 index c6a890b5e..000000000 --- a/.changeset/filecell-error-slot-5431.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/fields': minor ---- - -**API addition (public-surface widening):** `FileCell` — the compact upload -control `@object-ui/fields` exports for line-item grid cells — gains the -published optional `error?: string` slot, mirroring `LookupField` and -`FileField`: the same validation slot `@objectstack/spec/ui`'s -`FieldWidgetPropsSchema` declares and `FieldWidgetComponentProps` names -(objectui#3222). When set, `FileCell` puts `aria-invalid` on its own focusable -picker button; the message text stays with the host (objectui#5431). - -`GridField` now passes that slot for a required-but-empty `file` cell — the one -cell type objectui#3318's per-cell `aria-invalid` delivery left out. Before -this, a required `file` cell flagged only the visual ring and `title` on the -`td`; no element in the cell subtree announced the state, so assistive tech was -told nothing (a wrapper-only mark is exactly what objectui#5223 forbids). Text, -number, select, and lookup cells were wired in PR #5429; `file` cells now -behave identically. diff --git a/.changeset/flow-resume-result-5417.md b/.changeset/flow-resume-result-5417.md deleted file mode 100644 index 5de280435..000000000 --- a/.changeset/flow-resume-result-5417.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -A screen flow's resume result reaches the user — on both outcomes (objectui#5417). - -A dogfood walkthrough reported that a refused `resume` and a successful one -"render identically: the dialog closes and the page is unchanged", leaving no -gesture that distinguishes "created" from "rejected". Re-measured against `main` -before any change, one half of that was already fixed — `interpretFlowResponse` -reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its -prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors -after the version the report was measured on. There was no interpreter bug and -no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were -real, and they are what changed: - -- **A terminal failure no longer closes the dialog.** The reason it closed is - unchanged and is not reversed: on a `FLOW_FAILED` the engine has already - consumed the suspension, so a resubmit can only reach "No suspended run" and - must not be offered. Closing was one way to withhold that dead retry and the - expensive one — the user had just typed a form they could no longer see, and - the engine's sentence names a value that left the screen with it. The dialog - now stays open with the submit affordance withdrawn: the flat footer swaps - Submit for Close, and an `object-form` step drops its Save (which also stops a - second click from duplicating the record it had already persisted). -- **The refusal has a second, non-expiring carrier.** The toast stays — it is - viewport-fixed, so it still reaches a user scrolled past a tall step's header - — and an inline destructive `Alert` (`role="alert"`) now holds the same - sentence inside the dialog, beside the values that produced it. A retryable - refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, - and its banner clears as soon as the user starts editing. -- **A successful run invalidates what the flow WROTE, not just what the user is - looking at.** Both hosts answered `onComplete` with - `notifyDataChanged({ objectName: })`, so a flow that - created a quote from an Opportunity page never told the related list that - would now contain it — the record did not appear until a manual reload. The - runner cannot know which objects a flow touched, so it emits - `{ objectName: '*' }`: the same scope, for the same stated reason, that the - record page's manual ⟳ already uses. Everything mounted refetches in place - over the invalidation bus, with no remount. - -The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded -English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, -`nextStep`, `completed`) in all ten packs, plus reuse of -`common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The -server's own refusal sentence is still passed through untranslated — it is prose -the automation engine composed for a human, not copy with a key. diff --git a/.changeset/form-designer-namespaced-field-type-4838.md b/.changeset/form-designer-namespaced-field-type-4838.md deleted file mode 100644 index 88b03f1e6..000000000 --- a/.changeset/form-designer-namespaced-field-type-4838.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's form designer canvas now emits the namespaced `FormField.type` spelling -instead of passing `objectDef.fields[x].type` through raw (objectui#4838). - -Per the maintainer ruling on that card, a bare spec-type name (`markdown`, -`html`, `richtext`, …) is **not** a legal `FormField.type` — one widget, one -legal spelling, the namespaced widget id. `ObjectFormDesigner` was the measured -producer of the bare spelling: it handed a raw object-metadata type straight to -`isWideFieldType`, a helper whose vocabulary is `FormField.type`. It now -normalizes through `mapFieldTypeToFormType`, the one place that widget decision -is made. - -User-visible effect: a `repeater` field is finally laid out full-row on the -canvas, matching the runtime form. `repeater` is a spec `FieldType` that -resolves to the wide `field:grid` widget, but bare `repeater` is not one of -`WIDE_FIELD_TYPES`' bare members, so the raw pass matched nothing — the canvas -showed it at normal width while the real form spanned it. Fields whose spec name -doubles as a widget id (`textarea`, `markdown`, `html`, `richtext`, `grid`) are -unaffected; they matched under both spellings. - -The two tolerant consumers this makes look redundant are deliberately left -alone, each scheduled under its own follow-up with deprecation care: the -`field:`-prefix fallback in `renderFieldComponent`, and `WIDE_FIELD_TYPES`' dual -spellings. Removing a tolerance is the consumer-tightening half, and other -producers have not been normalized yet. diff --git a/.changeset/form-reset-notification-channels-5235.md b/.changeset/form-reset-notification-channels-5235.md deleted file mode 100644 index 6a2d2c3f1..000000000 --- a/.changeset/form-reset-notification-channels-5235.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The form renderer now keeps a `defaultValues` reset off `onChange` and off the -`form_change` `onAction` for **every** caller — including one that memoizes the -callback (objectui#5235). - -"A record landing is not a user edit" was already this file's documented, -pinned behaviour, but two of the three channels delivered it by accident of -React's effect ordering: every layout DESTROY runs before any layout CREATE, so -a caller passing a fresh callback each render had its value subscription torn -down before the reset and re-established after. The guarantee was therefore -delivered by the callback's *identity changing*. Wrap the same callback in -`React.useCallback` — taught everywhere as a semantically neutral performance -optimization — and the identity stays put, the effect never re-runs, the -subscription survives the reset, and the whole loaded record comes back to the -host as if the user had typed it: the false "the user edited this" signal -objectui#2968 was filed about, in a form no type, doc or call site warned about. - -The reset now states what those two channels report, the way `onDirtyChange` -already did (it computes its payload against the freshly installed baseline and -calls the host outright). Callers passing inline arrows see byte-identical -behaviour; callers who memoize stop receiving a phantom edit. - -Not a contract change: whether a value channel *should* report a programmatic -reset stays open in objectui#5235. This only removes the answer's dependence on -caller identity. diff --git a/.changeset/formatpercent-comment-citations-4596.md b/.changeset/formatpercent-comment-citations-4596.md deleted file mode 100644 index 6e68a460d..000000000 --- a/.changeset/formatpercent-comment-citations-4596.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Comments only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared (objectui#4596). - -Why the empty form is the right declaration here: both changed comments are **internal -reasoning**, not consumer-visible API documentation. One sits inside `formatMeasure`'s -body; the other is a test-file header. No released behaviour changes, and no declaration -changes — `dist/utils/dataset-format.d.ts` is byte-identical across this change -(measured: sha `be5f5938…`, 10,580 bytes on both sides), so nothing a consumer types -against or reads on hover moves. - -Stated honestly, because an earlier draft of this note got it wrong: the shipped -JavaScript **does** change. `tsconfig.base.json` sets `removeComments: false` -deliberately and `@object-ui/core` builds with a plain `tsc`, so a body comment is -emitted — `dist/utils/dataset-format.js` goes from 14,716 to 15,457 bytes, and it is the -only one of the package's 180 dist files that moves. The test file is excluded from the -build program (`src/**/__tests__/**`) and never reaches `dist` at all. Bytes moving is -not the criterion; released behaviour and consumer-visible surface are, and neither does. - -Three comments cited `formatPercent` as the live example of the divide-by-100 percent -route. Each was accurate when written and stopped being true when objectui#4590 landed: -`formatPercent` renders through `style: 'percentPoints'` with no division. The comments -now argue the route on its own merits without the expired citation, and record what -replaced it — no caller in this repo takes the divide-by-100 route today. `formatPercent` -was the last one; the two remaining `style: 'percent'` sites hand `Intl` a FRACTION, which -is that style's own contract, and the route otherwise survives only where a test builds it -in order to show it disagreeing. - -The measured argument underneath is unchanged, and the tie / extreme-magnitude pins that -keep the percentage-points route honest are untouched. The `27,581 of 1,200,013` figure -now names the grid it came from — objectui#4576's tie-dense grid, 0.005 steps to 2,000, -precisions 0/1/2, on `formatMeasure`'s call shape — so it stops reading as a discrepancy -against objectui#4590's `27,577 of 1,200,003`, which is the same grid re-measured through -`formatPercent`. diff --git a/.changeset/formfieldspec-dependson-5040.md b/.changeset/formfieldspec-dependson-5040.md deleted file mode 100644 index 183a8fc65..000000000 --- a/.changeset/formfieldspec-dependson-5040.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -metadata-admin: `FormFieldSpec` declares `dependsOn`, and the widget half reads -the same declaration instead of its own copy of it (objectui#5040) - -`FormFieldSpec` — the authoring type for a metadata-admin form layout, the -element type of `FormSectionSpec.fields[]` — did not declare `dependsOn`. -`widgets.tsx` held a second, inline description of the same object as -`WidgetProps.fieldSpec`, and that one did, because two registered widgets read -it as their primary configuration: `field-selector` resolves -`dependsOn || reference || 'objectName'` to decide whose field catalog to -offer, and `dynamic-config` uses it to pick a sub-schema out of -`WidgetContext.dynamicSchemas`. One value travelling down one channel, -described twice, disagreeing on the one key that decides what those widgets -show — so - -```ts -{ field: 'fields', widget: 'field-selector', dependsOn: 'objectName' } -``` - -the only configuration that makes `field-selector` work, was a `TS2353` for -anyone who typed their spec. It survived because in-repo specs reach the form -through `as any` / loose types, so the authoring type was never asked. - -No runtime behaviour changes: `MetadataField` already handed `dependsOn` -through and both widgets already read it. What changes is the type face — it -now admits what the runtime has always accepted. The two descriptions are one -declaration, extracted to a leaf module -(`views/metadata-admin/form-spec.ts`) that both halves import, because -`SchemaForm.tsx` imports `./widgets.js` and a back-edge would close a cycle. -`SchemaForm.tsx` re-exports `FormFieldSpec` and `VisibilityPredicate`, so every -existing importer is unaffected. - -`dependsOn` is `string | string[]` here, deliberately **not** -`@object-ui/types`' wider canonical `DependsOnInput`, which also admits -`{ field, param }` objects: both readers index `[0]` and use the result as a -field name, so the wider shape would be a type that lies. Converging the two is -its own decision, pinned as a refusal rather than taken silently. diff --git a/.changeset/formpage-maxlength-override-5595.md b/.changeset/formpage-maxlength-override-5595.md deleted file mode 100644 index 337dd53ad..000000000 --- a/.changeset/formpage-maxlength-override-5595.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`buildSections` now honours a FormView field's `maxLength` override instead of always -taking the object's ceiling (objectui#5595). - -The function merges a form's field overrides with the target object's field definitions, -and its own docstring states the rule: *"Field-level FormField overrides take precedence -over object defaults."* Every key in the loop is built that way — `override.label ?? -def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder` -— except one, which read `def.maxLength` unconditionally. So an author who set a tighter -per-form limit (a short public intake form over a column whose object-level ceiling is -generous) got the generous one. - -The failure was silent in the worst direction: no diagnostic, no warning, and the form -still submits, so the symptom is a value the author believed the input refused being -accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two -`maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"` -arm. - -`override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys, -so an explicitly declared `0` stays a value the author wrote rather than falling through -to the column's ceiling. This narrows only what the input allows; the object's storage -ceiling still decides at submit time, so nothing that was accepted before is now rejected -anywhere but at the keyboard. - -Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all -until objectui#5542, so no one typing a spec in this app could write the override in the -first place, and the inert merge branch was never exercised. #5542 converged that type -onto the shared app-shell declaration, which does declare the key — making the gap -expressible, and therefore findable. - -The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in -`FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than -assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered -evidence for this fix, and it is what made the gap findable in the first place, so it -keeps its place and names the honoured answer. diff --git a/.changeset/formpage-record-id-param-comment-4319.md b/.changeset/formpage-record-id-param-comment-4319.md deleted file mode 100644 index d20c6fa82..000000000 --- a/.changeset/formpage-record-id-param-comment-4319.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Comment-only repair in `apps/console`'s `FormPage`: the `FORM_RECORD_ID_PARAM` -docblock no longer borrows `createdRecordPath.ts` as a second witness for -`@object-ui/app-shell`'s root-barrel unreachability. That sibling case was -resolved when the host-app resolver was published from the package root, so a -reader following the cross-reference found the opposite of what it promised. -The surviving justification is unchanged and still true — the root barrel does -not re-export `./urlParams` — and now states that fact directly. No published -behaviour changes. diff --git a/.changeset/governed-surface-human-merge-5149.md b/.changeset/governed-surface-human-merge-5149.md deleted file mode 100644 index 7d63234c4..000000000 --- a/.changeset/governed-surface-human-merge-5149.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Instruction files only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. `AGENTS.md` records the governed surface -(`AGENTS.md`, `CLAUDE.md`, `.claude/**`, `docs/adr/**`) and the rule that agent seats -leave a PR touching it in draft for a human merge. diff --git a/.changeset/grid-column-spelling-docs-5352.md b/.changeset/grid-column-spelling-docs-5352.md deleted file mode 100644 index d80cdfb61..000000000 --- a/.changeset/grid-column-spelling-docs-5352.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Docs and skills only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. No package `src/` is touched, so no -`@object-ui/*` package changes behaviour and there is nothing here for a consumer -to upgrade to. - -Corrects the two published corpora that taught `object-grid` columns in a `name` -spelling `ObjectGrid` does not read. `ListColumnSchema` (`@objectstack/spec/ui`) is a -strict object whose column-identity key is `field`; `{ "name": ... }` is refused by -name (`unrecognized_keys: ["name"]`) and, at runtime, contributes no column. - -- `content/docs/api/schema-reference.md` — the `ObjectGridSchema` example authored a - MIXED array (four bare strings followed by one column object). Two defects in one - array: the object entry spelled `name`, and mixing forms is itself unsupported — - `normalizeColumns` dispatches the whole array on `columns[0]`, so a column object - standing behind a bare string is dropped whatever it spells. Renaming the key alone - does not fix it; the example is now uniformly `ListColumn` objects. The `columns` - row of the property table now names `field` and states the no-mixing rule. -- `skills/objectui/guides/page-builder.md` — the grid example's three columns were - all-object in the `name` spelling, so the grid rendered its row-number column and no - data columns at all. Now spelled `field`. A note was added between the grid and form - examples, which sit adjacent and mean the OPPOSITE thing by the same pair of words: - `ListColumn.field` names the object field a column shows, while `FormField.name` - names the field a form input writes. That adjacency is the documented cause of this - defect family (`packages/core/src/utils/column-identity.ts`). - -The adjacent `object-form` example is unchanged and was never wrong — `FormField.name` -is that layer's real key. diff --git a/.changeset/grid-default-filters-lowering-4082.md b/.changeset/grid-default-filters-lowering-4082.md deleted file mode 100644 index 1fa03310a..000000000 --- a/.changeset/grid-default-filters-lowering-4082.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@object-ui/plugin-grid": patch ---- - -`ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of -byte-copying it onto `$filter` (objectui#4082). - -The query assembly already lowered the canonical `filter` key through `toFilterNode` — -the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch -beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the -one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter` -and `plugin-view`'s non-grid fetch both already route the same value through -`toFilterNode` / `mergeFilterNodes`. - -Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is -declared `Record` (the MongoDB-style shape) and `isFilterAST` is false for a -plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one -answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. - -`toFilterNode` handles both without new logic: objects route through -`convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot -passes through untouched, so nothing is lowered twice. It also folds an absent or empty -source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` -used to send `$filter: {}`, asking the server a question with no content in a shape it -refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. - -**Grade — this is less dormant than the card assumed.** objectui#4082 was filed -observation-class on "no measured producer", reasoning that `defaultFilters` is not in -`object-grid`'s registered `inputs` so an author writing it only draws a save-gate -warning. That reasoning covers authors, but not the framework: `plugin-view`'s -`ObjectView` writes the slot itself, forwarding an active named view's `filter` as -`defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — -and `plugin-view`'s own README documents `listViews..filter` as -`[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as -`400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no -`renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, -and `ListView`'s lowering does not cover it. So a schema-registration host — the -documented authoring path — reached the raw assignment whenever an active named view -carried a filter. Not asserted here: a failing request captured against a running -deployment. `app-shell` is unaffected either way; it supplies `renderListView` and -delegates to `ListView`, which lowers. - -Not in scope, and deliberately not done: retiring `defaultFilters`. This is -consumer-side only — the key the schema admits is unchanged, and its precedence behind -the canonical `filter` is unchanged. - -The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the -measurements. diff --git a/.changeset/grid-dom-attribute-whitelist-4787.md b/.changeset/grid-dom-attribute-whitelist-4787.md deleted file mode 100644 index 1dc2d3506..000000000 --- a/.changeset/grid-dom-attribute-whitelist-4787.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer -land on the rendered `
` as invalid HTML attributes (objectui#4787). - -`grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and -`style`, so everything else `SchemaRenderer` hands a registered component reached the -element. Measured on a canary node, eight attributes leaked: -`columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, -`props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an -unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as -`columns="[object Object]"`. Layout was unaffected, so every catalog grid example -rendered with them — the reason this went unnoticed. - -The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist -objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the -SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, -`role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the -designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be -forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly -any more, including keys `GridSchema` does not have yet; enumerating today's keys to -strip would have re-rotted on the next schema addition. - -No authored input changes and no layout changes: the grid's own vocabulary was always -read off `schema`, never off these props. diff --git a/.changeset/grid-unresolved-column-diagnostic-5349.md b/.changeset/grid-unresolved-column-diagnostic-5349.md deleted file mode 100644 index 43d1c5a7d..000000000 --- a/.changeset/grid-unresolved-column-diagnostic-5349.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -`ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence. - -objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so -`ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer -reads. That was right — the spec refuses `accessorKey` and `header` by name, and the -census found zero authored usages. But it relocated a failure mode instead of removing -it: a column authored in a spelling the renderer does not read contributed nothing, and -nothing said so. No error, no warning, no empty state — the author got a grid with its -row-number column and no data columns, which is a success receipt for a disagreement -between the renderer and the author. - -An authored column that can never resolve now emits one `console.warn` naming the -address rather than the symptom: which block (`object-grid` or the `view:grid` alias), -which object and label, which `columns[i]`, the keys that entry actually carries, and the -rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }` -the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped -it" (the export-format warning), rather than adding a second differently-shaped one. - -Rendering is unchanged in every case: this is additive. The diagnostic reads the -`columns` input and nothing else — it never asks whether the grid found rows, because -`object-grid` legitimately draws them from five different places (a bare `data` array, -`data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and -passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView` -does). All five are pinned by test, in both directions. A `hidden: true` column is -authored intent and is never reported, and so are the arms that legitimately produce no -columns of their own: no `columns` key, an empty `columns` array, and the `string[]` -spelling. - -A throw was rejected: a grid that renders nothing today would become a page that renders -nothing. diff --git a/.changeset/grid-user-actions-collision-5240.md b/.changeset/grid-user-actions-collision-5240.md deleted file mode 100644 index 8f7c79a35..000000000 --- a/.changeset/grid-user-actions-collision-5240.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -`object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. - -`userActions` names two different blocks. On a **view** it is toolbar policy — -the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, -`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by -name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / -`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is -the only shape `listViewPredicates` can read, since its loop skips every -non-object value. - -`ObjectGrid` read the key view-first when building the `$select` projection -(`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a -perfectly legal toolbar block therefore shadowed the object's CRUD predicates, -the harvest found none, and the predicate's operand left the projection. CEL then -faults on the absent key, fails closed, and the row Edit/Delete button disappears -for everyone with nothing pointing at the projection — objectui#3501's failure, -reached with a success receipt at every step. - -The view-level block is not hypothetical: `SpecBridge.transformListView` copies -it onto the `object-grid` node the renderer receives, and `app-shell`'s -`ObjectView` builds one unconditionally. - -The harvest now reads the resolved object block only. Both `userActions` read -sites carry a comment naming the collision, and -`__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes, -the producer that writes the view one, the harvest's blindness to it, and the -projection that must keep the object's operand with a toolbar block present. - -Toolbar policy itself is untouched — it was never read through this path. diff --git a/.changeset/grid-widget-aria-invalid-3318.md b/.changeset/grid-widget-aria-invalid-3318.md deleted file mode 100644 index e644f0adf..000000000 --- a/.changeset/grid-widget-aria-invalid-3318.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -Grid field widget: announce a form-level validation failure to assistive tech. - -A required `grid` submitted while still empty rendered its "is required" message -but marked nothing — every row was a ghost row, and ghost rows were skipped by -the widget's per-cell validity channel. A sighted user saw the red message; a -screen-reader user was told nothing at all. - -The host failure now drives the per-cell channel the widget already owns: when -the `error` slot is set on an empty grid, the ghost entry row's required cells -flag, and the mark sits on each cell's own control rather than on the `td` -wrapper (a `td` is not focusable, and assistive tech reads validity from the -control). Populated grids are unaffected — they already marked their own empty -required cells inline. diff --git a/.changeset/home-ai-studio-flag-5521.md b/.changeset/home-ai-studio-flag-5521.md deleted file mode 100644 index 411aa0a99..000000000 --- a/.changeset/home-ai-studio-flag-5521.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Console Home stops offering the metadata-authoring front door on a deployment -whose own runtime config says authoring is not offered there (objectui#5521). - -The "Build an app" cover card is now withheld when -`GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed -hosted-SaaS shape that card led a plain tenant into the full authoring flow -behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry -holds zero authoring handlers — the entry was offered and the refusal arrived at -submit. The lockdown criterion for that shape is two-part, UI entry hidden AND -API refused; only the backend half was green. - -- The card is **hidden, not dimmed**, because that is the flag's own declared - meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when - false, the SPA hides the AI authoring affordances", and the serving plugin - documents "set false to force-hide the authoring UI". -- `features.marketplace` keeps the different presentation objectui#5504 gave it - — a dimmed card plus a visible localized reason. That flag means a route is - reachable; this one means force-hide. "Start with a template" is untouched: - installing a marketplace package is not AI metadata authoring and answers to - its own flags. -- No reason line is rendered in its place. `home.build.noCapability` says the - *account* lacks "Manage Metadata"; on a runtime with no authoring at all the - surface is absent for everyone, and pointing a viewer at a permission that - would not help them is the misdirection objectui#5557 is about. -- Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()` - already encodes: a runtime predating the flag, or one whose config fetch - failed, keeps the card exactly as visible as before. - -No new authorable config key, no new server surface, and no new copy — the flag -was already being served and already reaches the browser. diff --git a/.changeset/host-dispatch-action-keys-5611.md b/.changeset/host-dispatch-action-keys-5611.md deleted file mode 100644 index 1f43f6e85..000000000 --- a/.changeset/host-dispatch-action-keys-5611.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/core': patch ---- - -The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's -privileged-override safety copy (objectui#5611). - -`ActionRunner.execute` classifies the object it was HANDED, and a console host -hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes -`overrideNotice` on that dispatch and two param-collection handlers read it — -yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a -key two files read as one "no reader recognizes", and prescribed promoting it to -an explicit field on `ActionDef`. That prescription is the one shape the -2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic -walked an author into a rejected design. A false warning on the product's own -privileged path — the branch that finalises an approval over approvers who have -not acted — is how a dev console gets muted. - -Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to -`actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the -fourth input to that set and the first one that is not an authored-surface -mirror. Measured before and after on the exact dispatch the bar composes: the -warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS` -grew by exactly one member, and an action carrying a real typo alongside it -still warns — naming `targt` only. - -The authored surface does not move. `overrideNotice` is still NOT declared on -`ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal -remains a compile error, and the AST-derived pin over the interface is unchanged. -Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what -an author may write — `actionKeys.pin.test.ts` now pins both halves, including -the new list's exact contents so a second member cannot arrive quietly. diff --git a/.changeset/host-only-viewtypes-tree-icon-5321.md b/.changeset/host-only-viewtypes-tree-icon-5321.md deleted file mode 100644 index 1c922a8be..000000000 --- a/.changeset/host-only-viewtypes-tree-icon-5321.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s -view switcher instead of the grid one, and the `tree` / `chart` view types are -recorded as host-composition-only surfaces (objectui#5321). - -`viewSwitcherSchema`'s `iconMap` carried an entry for every view type except -`tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn -with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by -adding a single key — nothing recorded that the map had to be COMPLETE, so the -next missing member went unnoticed. The map is now typed -`Record`, which is how `ViewSwitcher`'s own -`DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: -a future `ViewType` member fails `type-check` rather than silently rendering as -a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph -`DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback -stays for host props that carry an unrecognised type. Reached in practice by -the console, whose `CreateViewDialog` offers `tree` among the view types a user -can create. - -No authoring surface changes. `generateViewSchema` renders eight view types -while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of -them, so `tree` and `chart` are selectable only through the component's `views` -prop. The maintainer ruled on 2026-08-20 that both stay recorded as -host-composition-only rather than being added to those unions, following the -objectui#5097 precedent; the record now lives beside that one, with the branch -set derived from a source fence, the authored unions pinned at the type level, -and host reachability measured. diff --git a/.changeset/humanize-label-single-home-5444.md b/.changeset/humanize-label-single-home-5444.md deleted file mode 100644 index 1e4cfdd0e..000000000 --- a/.changeset/humanize-label-single-home-5444.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch -'@object-ui/plugin-charts': patch ---- - -The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. - -`humanizeLabel` turns a stored value into a display string when nothing else -resolves it — an option with no declared label, an object name, a chart axis -member. It existed twice, byte for byte: once in `@object-ui/fields` (read by -`plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own -renderers) and once as a deliberate local copy in `plugin-charts`' -`ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on -`@object-ui/fields`". - -Two copies of one convention is a live hazard rather than tidiness: one -dashboard can hold a chart and a grid over the same stored value, so a change -landing on one copy alone would put that value on screen under two spellings at -once. The single implementation now lives in `@object-ui/core` — the shared -ancestor both packages already depend on, so the dependency the copy existed to -avoid is still avoided and no new edge is created, and core takes no React -(objectui#4389: core-canonical logic, plugins consume). Both former sites -re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps -working unchanged. - -**Nothing rendered changes.** The surviving implementation is byte-identical to -both deleted copies, and each former call site is pinned by identity against the -core function — not by a copied output table that someone would have to remember -to edit in two places. - -The core module also writes down, for the first time, why this convention stays -distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), -which additionally splits camelCase: - -``` -input humanizeFieldKey humanizeLabel -needs_analysis Needs Analysis Needs Analysis -NeedsAnalysis Needs Analysis NeedsAnalysis <- differ -unitPrice Unit Price UnitPrice <- differ -BestCase Best Case BestCase <- differ -lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ -``` - -A field KEY is authored in the codebase and carries a machine spelling, so -splitting camelCase recovers words its author meant. A stored VALUE is arbitrary -tenant data, where a mid-token capital is not reliably a word boundary and -splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two -conventions also do not nest — on the last row each leaves alone the separator -the other rewrites. Whether they should ever converge is a separate decision -that would move rendered output in four packages at once; it is deliberately not -made here. diff --git a/.changeset/i18n-guide-label-rule-5081.md b/.changeset/i18n-guide-label-rule-5081.md deleted file mode 100644 index 16ec799ad..000000000 --- a/.changeset/i18n-guide-label-rule-5081.md +++ /dev/null @@ -1,30 +0,0 @@ ---- ---- - -Docs + gate ledger only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. - -`skills/objectui/guides/i18n.md` attributed its label rule to `@objectstack/spec` v4 in two -places (`:117`, `:162`) while every manifest here declares `^17.0.0` and `node_modules` -carries `17.0.0` — thirteen majors, on the surface an AGENT reads before it writes a user's -project. The version number was the reported defect; measurement against the installed -package found the rule it was backing to be wrong as well, which is why neither arm of the -original fork (renumber to v17, or drop the qualifier and keep the sentence) was writable: -both would have laundered a v4-era false statement into a current one. Maintainer ruling -2026-08-20, option A: restate the rule per the installed spec, with no version qualifier. - -Measured against `@objectstack/spec` 17.0.0's published dist, `I18nLabelSchema` is a union -of a plain string and an inline locale map whose keys must match -`/^(default|[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*)$/` — a BCP-47 tag, or `default`. The guide now -states both forms, shows an inline-map example on the two keys this repo was measured to -resolve (`card.title` and `button.label`, both through `pickLocalized`), and keeps its -"don't use `{key, defaultValue}`" advice with the real reason: that key-reference vocabulary -was retired in objectstack#5055, the spec rejects the object with its own message, and if one -reaches a renderer anyway `pickLocalized` falls through to the first string value and paints -the raw translation key on screen. - -The `KNOWN_CLAIMS` entry that inventoried the fossil as `stale` is deleted in the same commit -— the downward ratchet in `scripts/__tests__/doc-version-claims.test.ts` ("no entry may -outlive the claim it excuses") turns red otherwise — and that file's header prose, which -restated the now-falsified two-branch fork, is corrected to record what the fork actually -turned out to be. diff --git a/.changeset/i18nlabel-comment-4611.md b/.changeset/i18nlabel-comment-4611.md deleted file mode 100644 index 3a243e944..000000000 --- a/.changeset/i18nlabel-comment-4611.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a -plain string (objectui#4611). - -The paragraph explaining why `label` / `options[].label` are inherited rather than -locally overridden justified itself with a claim about `@objectstack/spec` that was -never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects -were dropped in favour of translation files". Measured against the installed GA pin -`@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of -a string and a string-to-string record, and the schema's own doc block states two -authorized forms with "Both are real; neither is deprecated by this schema". Executed -against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted, -`{ key, defaultValue }` rejected. A reader who believed the comment would have taken a -widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one -seat having nearly done exactly that. - -The replacement describes what `I18nLabel` admits and cites the spec's own doc block -rather than restating a zod expression; where today's spelling is named it is scoped as -a measurement against 17.0.0 with its file and line, so it ages as a reading rather than -as a standing fact. The decision itself is unchanged and never depended on the false -premise — `label` flows in by reference through the spec's schema, and a local -`string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds. - -Documentation only, and the release-visible surface is the declaration file: measured -with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`), -108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every -other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha), -because the comment documents an `interface`, which is erased at emit along with its -leading comment. No behaviour changes; hover text and the shipped `.d.ts` do. diff --git a/.changeset/i18nlabel-inverted-pin-5612.md b/.changeset/i18nlabel-inverted-pin-5612.md deleted file mode 100644 index ffa052d0b..000000000 --- a/.changeset/i18nlabel-inverted-pin-5612.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/types': patch ---- - -The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and -`ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613). - -Both are residue of the same removed local `label` / `options[].label` override. - -The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` -that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion, -`const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed -`I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain -string is assignable under the narrow shape *and* under the wide one, so that assignment -could only ever fail if the plain-string form were removed — the opposite of the event it -was written to catch. The widening had already landed: `@objectstack/spec@17.0.0` -declares `I18nLabelSchema` as a union of a string and a string-to-string record -(`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection -it did not provide, and asserted a false premise in its own name. - -It is retargeted at what actually holds the decision up — not which single form the spec -has, but that **both** authorized forms stay assignable, on the spec type and on the -inherited `ActionParam['label']` and `options[].label`. It now fails when either form is -withdrawn, and deliberately does not fail on a further widening, since inheriting by -reference is exactly what stays correct as the authorized set moves. The comment is -rewritten against the schema's own doc block (two authorized forms, "Both are real; -neither is deprecated by this schema") instead of the false premise. Verified by -construction: against a locally built narrow `type I18nLabel = string` the new assertions -fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both -shapes. - -`ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since -the override was removed, and nothing re-exported it — and the doc paragraph that -recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard. - -No behaviour changes; the release-visible surface is the declaration file. Measured with -the package's real `tsc` build, both legs building from a cleared `dist/` and cleared -composite build info: 108 emitted files on both sides, exactly one differing — -`dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed. -Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged -sha), because the comment documents an `interface`, which is erased at emit along with -its leading comment. The deleted type import contributes no emitted delta at all, and the -rewritten test file is not part of the build. diff --git a/.changeset/inbox-actor-name-retired-5203.md b/.changeset/inbox-actor-name-retired-5203.md deleted file mode 100644 index ebb7ec8a9..000000000 --- a/.changeset/inbox-actor-name-retired-5203.md +++ /dev/null @@ -1,28 +0,0 @@ ---- ---- - -Internal only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -Retires `InboxNotification.actor_name` (`packages/app-shell/src/layout/inboxGrouping.ts`), -which was dead at both ends. `mergeInboxRows` -(`packages/app-shell/src/hooks/sharedUserFeeds.ts`) is the single producer of every row -the bell and Home's action centre render and never mapped it; neither consumer read it; -and `sys_inbox_message` declares no actor column for it to have been mapped FROM. It was -the last declared-but-unfilled member of that interface after objectui#5190 removed the -sibling `source_object` / `source_id` pair. - -**No published type surface changes.** `InboxNotification` is not reachable from -`@object-ui/app-shell`'s public entry: neither `src/index.ts` nor `src/layout/index.ts` -re-exports it, the built `dist/index.d.ts` does not name it, and the package `exports` -map offers only `.` and `./styles.css` — no deep subpath an external consumer could -import it through. The type is internal to the package, so removing an optional member -of it is not an externally observable narrowing and nothing user-visible ships. Runtime -behaviour is unchanged in both directions: no code path produced the field and no code -path read it. - -Two pins keep it retired, in opposite directions — a TYPE PIN in -`layout/__tests__/inboxGrouping.test.ts` that fails if the field is re-declared, and a -runtime key-set pin in `hooks/__tests__/sharedInboxFeed.rowShape.test.tsx` that fails if -the producer is ever changed to spread raw `sys_inbox_message` columns through instead -of mapping them field by field. diff --git a/.changeset/issue-5389-workspace-admin-positions.md b/.changeset/issue-5389-workspace-admin-positions.md deleted file mode 100644 index 2c12bc2f6..000000000 --- a/.changeset/issue-5389-workspace-admin-positions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -Restore platform-admin detection for permission-set-derived administrators. - -`useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace -gating and the "Build an app" CTAs. Its third source read `user.roles`, a key -the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it -to `positions`). An administrator whose adminship comes from the -`admin_full_access` permission set — the single-tenant deployment shape, where -there is no organization member row and the server deliberately no longer -overwrites `user.role` — matched none of the three sources and read as **not an -administrator**: Setup and Studio simply disappeared for them. - -The hook now reads `user.positions[]`, the one spelling the session publishes. -Detection is restored for that path and unchanged everywhere else: an active -member row with an admin role, a stored `user.role` admin scalar, and -preview/no-auth mode all behave exactly as before, and nobody who was not an -administrator becomes one — pinned by four negative cases alongside the -positive one. - -Also corrects the now-stale documentation that described the removed spelling: -the hook's own docblock, the `roles?: string[]` declaration on the client -`AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and -two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the -comment corrections. diff --git a/.changeset/list-user-actions-collision-5398.md b/.changeset/list-user-actions-collision-5398.md deleted file mode 100644 index 62a0d3c77..000000000 --- a/.changeset/list-user-actions-collision-5398.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. - -`userActions` names two different blocks. On a **view** it is toolbar policy — -the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, -`rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by -name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / -`create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is -the only shape `listViewPredicates` can read, since its loop skips every -non-object value. - -`ListView` read the key view-first when building the `$select` projection -(`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view -carrying a perfectly legal toolbar block therefore shadowed the object's CRUD -predicates, the harvest found none, and the predicate's operand left the -projection. CEL then faults on the absent key, fails closed, and the row -Edit/Delete button disappears for everyone with nothing pointing at the -projection — objectui#3501's failure, reached with a success receipt at every -step. - -This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and -it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level -`userActions` it hands down as an object literal of two spreads, so the left -operand was `{}` at worst — never nullish. The `??` never fell through, and the -object's CRUD predicates were never consumed at all on that path, whether or -not an author wrote any toolbar policy. - -The harvest now reads the object block only. Both `userActions` read sites in -`ListView.tsx` carry a comment naming the collision, and -`__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the -two shapes, a producer that manufactures the view one, the harvest's blindness -to it, and the projection that must keep the object's operand with a toolbar -block — or an empty block — present on the view. - -Toolbar policy itself is untouched — it was never read through this path. diff --git a/.changeset/list-view-nested-aria-label-i18n-5134.md b/.changeset/list-view-nested-aria-label-i18n-5134.md deleted file mode 100644 index f330b77a6..000000000 --- a/.changeset/list-view-nested-aria-label-i18n-5134.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -`ListView` now resolves the nested `aria.ariaLabel` against the audience's locale -instead of casting it to a string (objectui#5134). - -`@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain -string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only -read site in this repo spread it into the DOM as -`{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a -conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which -a screen reader announces as the list view's accessible name — in every locale. The -read now goes through the spec's own `resolveI18nLabel` (the resolver four other -in-repo read sites already use) against `useDisplayLocale()`. - -Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a -plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label -predates rc.6, and no measured author writes one today — but map values are legitimate -and arrive via API/import, so an imported list view carrying -`aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong -accessible name. This is the map form working as declared, not a defect users are -currently hitting. - -Behaviour on the string arm is byte-identical, including `''` (falsy before and after, -so no attribute). One edge changes for the better: a map that matches no locale used to -render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an -unnamed region beats a garbage-named one. - -The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different -vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by -`SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's -shape. diff --git a/.changeset/listview-comment-pair-4559.md b/.changeset/listview-comment-pair-4559.md deleted file mode 100644 index 2f012823c..000000000 --- a/.changeset/listview-comment-pair-4559.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime -behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does -change, which is why this is a `patch` rather than an empty frontmatter. - -**objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment -block above the `sortFields` memo still called a formula field "the supported -alternative (… which sorts like any text column)". Since objectui#4294 the -`list.sortRelationalHint` string in this same file says the opposite ("Not a formula -field: it is virtual, so no column is stored for it and the server refuses to sort by -one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the -server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical -now names the remedy the hint, the server's refusal and the README already share — a -stored field that denormalizes the name onto this object, written when the source -changes. This was the last copy of the retired advice in the repo. - -**objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.** -It had drifted two declarations up, so the exported `parseSortConfig` carried two -stacked leading comments and the helper carried none. This one was not cosmetic: because -`parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into -`dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort -parser with a sentence about action labels. Moving the block removes it from the `.d.ts`; -`formatActionLabel` is module-private, so its now-correct docblock does not appear there. -It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the -comment block *attached* to a declaration — a misattributed docblock is the mechanism by -which a claim gets scored against the wrong symbol. This block carries no spec-alignment -phrase, so nothing fired today. - -No tests accompany this change and none could: both edits are comment-only, and there is -no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real -`vite build` before and after, not asserted. diff --git a/.changeset/lookup-dropdown-cell-renderer-5492.md b/.changeset/lookup-dropdown-cell-renderer-5492.md deleted file mode 100644 index d43f0e19a..000000000 --- a/.changeset/lookup-dropdown-cell-renderer-5492.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -A lookup's inline dropdown renders its columns through the same cell renderer the browse-all picker uses, so one `lookup_columns` declaration cannot produce two answers. - -A form's lookup field offers two ways to pick a related record, and both read -the same declaration: the inline dropdown under the field, and the -"browse all records" picker behind it. The picker resolved every cell through -the type-aware cell renderer. The dropdown did not — it printed -`record[descriptionField]` verbatim into the option subtitle and concatenated -`label: String(rawValue)` into the row's `title` attribute. Measured on the -same declaration, on a real 17.1.0 deployment: - -``` -column inline dropdown (before) browse-all picker -lookup T5MsMCuwP4t_yUHq (bare FK id) the related record's name -date 2026-08-20T00:00:00.000Z (ISO) a formatted date -select pending (enum code) the authored option label -``` - -Both surfaces now call one shared module — `widgets/lookupColumnDisplay.tsx`, -which owns column normalisation, the field-descriptor enrichment from the -referenced object's schema, and the render itself. The picker's own -`renderCellContent` and `columnFieldDescriptors` are now thin calls into it, so -there is a single renderer left to drift from. The dropdown's extra columns are -rendered into the option row itself; the row's `title` keeps the full option -label, which is what a truncated label needs, instead of a raw-value dump. - -No query changed and no contract widened. `lookupColumns` entries stay bare -field names — no dot paths, no populate/expand semantics — because neither -surface's request carries populate to begin with: the picker resolves a -foreign-key id to a name client-side, in the lookup cell renderer, and the -dropdown now inherits exactly that. An unresolved reference therefore renders -what the picker renders for it, and keeps its column: a slot is dropped only -when the record holds no value for the field, decided on the raw value and -never on what the renderer makes of it, so an unresolved id can never degrade -into a silently empty column. diff --git a/.changeset/loud-lamps-shake.md b/.changeset/loud-lamps-shake.md deleted file mode 100644 index eae0d1622..000000000 --- a/.changeset/loud-lamps-shake.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@object-ui/react": minor -"@object-ui/plugin-detail": minor -"@object-ui/plugin-grid": minor -"@object-ui/plugin-form": minor ---- - -`object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so - -The three object-bound blocks disagreed about how the data-source adapter reached -them. `object-grid` and `object-form` were registered through wrappers that read -it from `SchemaRendererProvider` context; `detail-view` was registered as the raw -component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads -only context, so the two wirings were mutually exclusive: measured with correct -keys in every cell, provider wiring gave the grid `find` 1 and the detail view -`findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. - -All three now resolve the adapter through one rule — an explicit `dataSource` -prop first, the provider context second. This is additive: `detail-view` keeps -its prop form (and direct `` callers are untouched), -`object-form` gains a prop form it did not have, and `object-grid` no longer -throws `useSchemaContext must be used within a SchemaRendererProvider` when a -page has no provider. - -And the silence is over. A block in this family that resolves no adapter renders -a **No data source resolved** panel naming the block, the object it was about to -read, and the ancestor that injects the adapter — instead of a header-only grid, -a field-less form card, or nothing at all. The check is opt-in per block, so a -placement with inline rows, inline `customFields`, an inline record or an `api` -endpoint is untouched. - -New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, -`noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. diff --git a/.changeset/marketplace-catalog-runtime-before-admin-5557.md b/.changeset/marketplace-catalog-runtime-before-admin-5557.md deleted file mode 100644 index 69a66da58..000000000 --- a/.changeset/marketplace-catalog-runtime-before-admin-5557.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace **catalog** page now tells a non-admin that the runtime has no -marketplace, instead of telling them they lack permission (objectui#5557). - -`MarketplacePage` ordered its two early returns admin-first, so on a runtime that -mounts no marketplace at all (`features.marketplace: false` — an `OS_CLOUD_URL=off` -deployment, the EE deploy template's factory default) an unprivileged member got -"access denied" for a surface that exists for nobody. That answer sends them to -ask an administrator for a grant that would not help them, and it left the -informational disabled state built in objectui#5504 unreachable for every -non-admin. The runtime check now answers first, because "this deployment has no -marketplace" is true regardless of who is asking. - -This restores the sibling-page invariant for the one class of viewer it still -failed for: `MarketplacePackagePage` was reordered the same way in objectui#5533, -so on a marketplace-off runtime the catalog page and the package detail page now -give a non-admin the same kind of answer. - -Scope, deliberately narrow: - -- **Admin-first ordering stays correct where a marketplace exists.** On a runtime - with `features.marketplace: true`, a non-admin still gets `MarketplaceAccessDenied` - — the catalog is an install surface, and a member who cannot install has nothing - to do with it. That boundary is pinned by an explicit test, not left to prose: - without it, a change that simply dropped the admin check would look correct. -- Nothing an admin sees changes, on either kind of runtime. -- No new i18n keys, and no change to `MarketplaceAccessDenied` or - `MarketplaceDisabled` themselves — only which of the two the page reaches for, - and in which order it decides. -- The disabled state is still the server's own answer (`features.marketplace`), - never inferred from a failed request, and it still fails open. diff --git a/.changeset/marketplace-detail-admin-guard-order-5583.md b/.changeset/marketplace-detail-admin-guard-order-5583.md deleted file mode 100644 index b50d52248..000000000 --- a/.changeset/marketplace-detail-admin-guard-order-5583.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace package detail page decides "you are not an admin" before it fetches, -instead of after the load has already failed. - -`MarketplacePackagePage` ordered its early returns with the `!isAdmin` guard *after* -both the loading branch and the `error || !data` branch, and gated its two fetch -effects on `features.marketplace` alone. On a runtime that mounts a marketplace, a -non-admin who opened a package URL was therefore walked through the fetch and the -skeleton, and — when the load failed — was handed the destructive "Failed to load -package" card carrying the server's own error message. Whether that viewer was -refused or handed a diagnosis about a surface they are not allowed to use came down -to whether an unrelated request happened to succeed. - -The guard now sits ahead of both branches, and `getMarketplacePackage` and -`getCloudInstallationInfo` are gated on `isAdmin` as well, so the page stops issuing -requests on behalf of a viewer it has already decided to turn away. That is the -discipline objectui#5533 established on this same page for `features.marketplace`, -applied to the other predicate that decides the same thing. It is also the ordering -`MarketplacePage` carries after objectui#5557, so the two sibling pages now answer one -runtime the same way for every viewer. The server remains the authority on what a -non-admin may fetch; this only stops the client doing work it would discard. - -Unchanged for an admin, deliberately and under test: a failing load still produces the -destructive card with the server's message intact, and a successful one still renders -the package. A "fix" that hoisted the refusal unconditionally, or that deleted the -failure branch, would satisfy every non-admin assertion and fail those two. - -`loading` stays seeded from `marketplaceEnabled` alone rather than from -`marketplaceEnabled && isAdmin`. `isAdmin` reads `activeMember`, which `AuthProvider` -resolves asynchronously *after* the session settles, so an admin whose role comes from -the org member row renders once as a non-admin before the flag flips. Seeding `false` -there would leave that first admin render with `loading: false` and no data — the -destructive card, painted for a frame before the effect could raise the flag again. -`MarketplacePackagePage.guardOrder.test.tsx` pins the flip case for that reason, along -with the ordering, the skipped requests, and the marketplace-off boundary the guard -must not jump above. diff --git a/.changeset/marketplace-detail-disabled-state-5533.md b/.changeset/marketplace-detail-disabled-state-5533.md deleted file mode 100644 index 672d7f993..000000000 --- a/.changeset/marketplace-detail-disabled-state-5533.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The marketplace **package detail** page now states that the marketplace is turned -off, instead of red-erroring, on a runtime that has none (objectui#5533). - -`MarketplacePackagePage` takes the same `isMarketplaceEnabled()` early return its -sibling catalog page took in objectui#5504, rendering the informational -`MarketplaceDisabled` state when the server reports `features.marketplace: false` -(an `OS_CLOUD_URL=off` deployment — the EE template's factory default). Until now -the same runtime answered a bookmarked or pasted package URL with a destructive -"Failed to load package / Not found." card, so the two sibling pages reached -opposite conclusions about one runtime: the catalog called it configuration, the -detail page called it a failure. - -Both requests the page fires for its own view are skipped in that state — the -package fetch and the cloud-installation probe — rather than fired and discarded: -a discarded request still reaches the server and can race the destructive card -onto the screen before the disabled state settles. - -Scope, deliberately narrow: - -- **Not** a "swallow all errors" change. With `features.marketplace: true` the page - behaves exactly as before, and a package that genuinely is not there still - renders the destructive card carrying the server's own message. The flag is the - runtime's own answer, never inferred from the shape of a failure, and it fails - open — a runtime that answers nothing keeps its detail page. -- The `installLocal` surfaces are untouched. That is a different capability flag, - and an air-gapped `OS_CLOUD_URL=off` runtime still has a working install-local - path. -- No new i18n keys: `marketplace.disabled.*` and `marketplace.action.backHome` - already ship in all ten locale packs. diff --git a/.changeset/marketplace-disabled-state-5504.md b/.changeset/marketplace-disabled-state-5504.md deleted file mode 100644 index 4462a0913..000000000 --- a/.changeset/marketplace-disabled-state-5504.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a -first-class disabled state, and the load-failure hint describes the control plane -the runtime was actually pointed at (objectui#5504). - -`apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory -default, so a stock self-hosted stack has no marketplace at all. The Console still -recommended one: Home led with "Start with a template" and "Browse App -Marketplace", and the click landed on a red **Failed to load marketplace / Not -found** card whose hint claimed this runtime "points at the public ObjectStack -cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for -exactly the deployment reading them — the operator had not left the default, and -the advice pointed back at the template that told them to set `off`. "Marketplace -disabled by configuration" is a configuration conclusion, not a load failure. - -- `isMarketplaceEnabled()` (`runtime-config`) reads the server's own - `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the - serving app's route table (objectstack#8356). It is never inferred from the shape - of a failed request: a control plane that is merely DOWN leaves the flag `true`, - so an outage still renders as an outage. Unknown fails OPEN. -- The marketplace page renders an informational "App Marketplace is turned off" - state — muted, not `destructive` — and issues no request it knows will 404. -- Home's "Start with a template" cover greys out with a visible localized reason, - and the "Browse App Marketplace" shortcut is withheld, exactly as they already - are for the `manage_metadata` capability gate. -- `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the - configured control plane) and `failedHintSameOrigin`. The "points at the public - cloud by default" sentence is gone: it was rendered unconditionally, including on - every runtime whose operator had overridden `OS_CLOUD_URL`. - -All ten locale packs carry the new keys. diff --git a/.changeset/metadata-client-actor-retired-4834.md b/.changeset/metadata-client-actor-retired-4834.md deleted file mode 100644 index 2aa8de93c..000000000 --- a/.changeset/metadata-client-actor-retired-4834.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/data-objectstack': minor ---- - -**Breaking (published surface):** remove `options.actor` from `MetadataClient`'s -`save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor` -request header. - -The server stopped honouring that header. objectstack#7941 ruled that the -recorded actor is the identity the request was authorized as, and removed the -header limb from the `/meta` write resolver — attribution cannot drift from -authorization. The option therefore typed cleanly, sent a header, and could not -influence the audit or history row it appeared to address: a false affordance -that promised attribution and silently failed to deliver it. - -Three declarations go: `MetadataClientSaveOptions.actor` (inherited by -`MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`), -and the inline `{ actor?: string }` on each of `publish` and `rollback`. -`MetadataAuditEntry.actor` is unaffected — that is the server's read-back of -who acted, and it remains the way to see attribution. - -Marked `minor` rather than `major` per this repo's version-alignment policy -(the fixed group's major tracks `@objectstack`, and `major` in a changeset -would drag all 39 packages off that cadence). - -No caller in this repo passed `actor`; the census found the only in-repo -occurrence was the client's own unit test. Callers outside this repo that still -pass it are unaffected at runtime beyond losing a header the server already -ignored — the property is dropped rather than forwarded, pinned by -`metadata-actor-retired-4834.pin.test.ts`. diff --git a/.changeset/metadata-lock-state-5024.md b/.changeset/metadata-lock-state-5024.md deleted file mode 100644 index 785458cbf..000000000 --- a/.changeset/metadata-lock-state-5024.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/data-objectstack': patch -'@object-ui/app-shell': patch ---- - -The metadata lock banner can no longer render an amber, padlocked box with no -title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three -times (objectui#5024). - -`MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four -states out by hand, 42 lines apart in one file, compared by no gate. They are now -one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s -`z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies -were restating a schema rather than filling a gap. - -The user-visible half is the banner. Its title was three independent `&&` branches -with no fallback, while the switch that opens the banner is true for any non-`none` -value — so a lock state outside the four opened the box and left the headline -empty. That is reachable without a fifth state ever being added here: -`MetadataClient.layered()` casts the wire value through unchecked, so a newer -server reaches this banner as-is. Measured, not assumed — feeding `no-publish` -through the page rendered the padlock, the border and an empty title. The title is -now a keyed lookup with a loud fallback that names the unrecognised token, so a -fifth state fails `type-check` here and, if one arrives from a server anyway, the -operator reads a sentence instead of a blank box. diff --git a/.changeset/objectchart-fieldoptionlabel-ref-5587.md b/.changeset/objectchart-fieldoptionlabel-ref-5587.md deleted file mode 100644 index 4b61e3f15..000000000 --- a/.changeset/objectchart-fieldoptionlabel-ref-5587.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -`ObjectChart` now depends on the `fieldOptionLabel` resolver directly instead of -holding it behind a ref, so a chart re-resolves its groupBy option labels when -the resolver genuinely changes (objectui#5587). - -The ref existed for a reason that no longer holds. `useSafeFieldLabel()` returned -a fresh object on every render outside an i18next provider, so a direct -dependency made `fetchData`'s `useCallback` identity fresh on every render, and -the effect that depends on `fetchData` refetched on every render — an unbounded -loop. `ObjectChart` worked around that locally with `fieldOptionLabelRef` plus a -`useEffect` keeping it current. `useObjectLabel`'s memo now holds with or without -an i18next instance bound (objectui#5564), so the resolver's identity is stable -on both paths and the indirection buys nothing. - -It did cost something, and that is the user-visible half: a ref-hidden dependency -meant `fetchData` did NOT re-run when the resolver changed. A chart mounted -before its `I18nProvider`, or rendered across a language switch, kept serving -groupBy labels resolved by the old resolver until some unrelated dependency -(object name, filter, aggregate) happened to move. It now refetches once on that -transition and shows labels in the active language. - -Pinned by `ObjectChart.fieldOptionLabelRefetch.test.tsx`, which counts fetches -across forced re-renders both outside and inside a provider. Reverting -`useObjectLabel.ts` to its pre-objectui#5564 state turns the no-provider case red -(2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so -the removal is pinned to the fix that unlocked it rather than to a comment. diff --git a/.changeset/objectmap-schema-data-shorthand-5305.md b/.changeset/objectmap-schema-data-shorthand-5305.md deleted file mode 100644 index c21345fce..000000000 --- a/.changeset/objectmap-schema-data-shorthand-5305.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-map': patch ---- - -`ObjectMap` reads `schema.data` in one place again, so an array-shorthand map stops -making a metadata request it never uses. - -The fetch effect carried a second short-circuit beside the `props.data` one -objectui#5003 fixed: it read `schema.data` directly and tested whether that value was -itself an array. eslint reported it as `missing dependency: schema.data` — the last -`react-hooks/exhaustive-deps` warning on that effect. - -The dependency was never actually missing. `getDataConfig(schema)` already returns -`schema.data` verbatim, and the result is memoized on `JSON.stringify(rawDataConfig)` -into `dataConfig`, which **is** one of the effect's declared dependencies. The authored -rows therefore reached the effect before this change; the direct read was a duplicate of -an already-threaded value, which is why neither adding a dependency nor deleting the -branch was right. - -The array handling moved into `getDataConfig`, where `ObjectGrid`'s own `getDataConfig` -already pins the same normalization (`"Check if data is an array (shorthand format)"`). -Same rows render, and the effect now reads only `dataConfig`. - -One behavioural consequence, and it is the point: an array under `data` now yields -`provider: 'value'`, so `hasInlineData` is true and the sibling effect no longer calls -`dataSource.getObjectSchema()` for it. That request's only read site is -`buildExpandFields()` inside the object-provider fetch branch, which an inline schema -never reaches — so the call was pure waste, and the shorthand now behaves exactly like -the declared `{ provider: 'value', items }` form it is shorthand for. - -Deleting the branch instead was measured, not assumed: with no producer-side handling, -an array-shorthand map renders `Error: DataSource required for object/api providers` -rather than its markers. The shorthand is a live convention in six sibling blocks -(`ObjectGrid`, `ListView`, `ObjectTree`, `ObjectChart`, `ObjectDataTable`, -`calendar-view-renderer`), so `object-map` would have become the one block in the family -that answers it with an error box. diff --git a/.changeset/objectview-overlay-pair-completeness-5025.md b/.changeset/objectview-overlay-pair-completeness-5025.md deleted file mode 100644 index 85273d19a..000000000 --- a/.changeset/objectview-overlay-pair-completeness-5025.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`sanitizeViewOverride` now strips a half-filled `between` from a stored view -overlay, instead of handing it back to the merge (objectui#5025, -objectstack#8815). - -The overlay recovery pass asked "is this filter row filled in?" with the -shape-blind predicate objectstack#8815 retired — `value == null || value === '' -|| (Array.isArray(value) && value.length === 0)`. That is correct for `scalar` -and `list` and blind to `pair`: a `between` carrying one bound is -`['2024-01-01', '']`, an array of length 2, so the pass read it as a real -condition and kept it. The two write paths (`plugin-list`'s `ListView` and -app-shell's `viewFilterFold`) were converted to the builder's arity-aware -`isFilterValueComplete`; this read path was the third verbatim copy, and the one -whose job was to clean up exactly the rows the other two used to write. Until -now a stored half-range survived the pass, reached the query, and the server -refused the whole view (`400 INVALID_FILTER`) for every user on every later -read. - -The pass now delegates to the same `isFilterValueComplete` the write paths use, -on both of the at-rest shapes it handles (the spec `ViewFilterRule` object and -the legacy runtime triple). A complete range — bounds of `0` and `false` -included — is untouched, and the `scalar` and `list` families read exactly as -before, since the retired predicate was already right for them. One shape is -newly stripped beyond the half-filled array: a `between` whose value is a bare -scalar, which `ViewFilterRuleSchema` itself refuses. - -Which operators want no value at all is unchanged and still answered by -app-shell's `VALUELESS_FILTER_OPERATORS` — that set is already derived from the -builder's `VALUELESS_FILTER_BUILDER_OPERATORS` rather than being a private copy, -and its parity is pinned. diff --git a/.changeset/olive-donkeys-smile.md b/.changeset/olive-donkeys-smile.md deleted file mode 100644 index e643a315e..000000000 --- a/.changeset/olive-donkeys-smile.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@object-ui/plugin-charts": minor ---- - -Dashboard chart widgets no longer render as a blank area when their height class -resolves to `auto`. - -`ChartContainer`'s min-size fallback was applied only to the wrapper `div`. -Recharts measures its own `width:100%;height:100%` size-detector element, and a -percentage height never resolves against an ancestor's `min-height`, so the -wrapper obediently grew to 280px while the measured element stayed at 0 — and -Recharts renders no children at all for a non-positive box. The result was a -widget card with its title over an empty chart area: no marks, no refusal, no -empty state, and permanent, because a box that never changes fires no resize. -The floor is now applied to the measured element as well, under the same -condition, so an author's explicit height still wins. diff --git a/.changeset/olive-pugs-sing.md b/.changeset/olive-pugs-sing.md deleted file mode 100644 index 0ebe36886..000000000 --- a/.changeset/olive-pugs-sing.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -Dashboard record fields: percent columns now render through the one percent -scaling decision instead of a second, drifted copy of it. - -`renderFieldValue`'s `%`-format branch normalised the value itself before -calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`, -then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`, -which `@object-ui/core` documents as the single source of truth for percent -display scaling, so the branch was re-deciding what core owns — and its copy had -drifted from it in three measured ways: - -- `(value / 100) * 100` is not value-preserving in binary floating point, - re-introducing one call frame upstream the round trip that was removed from - inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000 - values change bit pattern and 1,108 rendered strings move, every one a - last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is - `1.61%`. -- A stored fraction below `0.01` was scaled twice — the local `* 100` put it - back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered - `50.00%`. -- The local test was `value > 1` rather than core's symmetric `|value| < 1`, so - a negative already in percentage points took the fraction arm: `-5` rendered - `-500.00%`. - -The branch now hands the raw stored value to `formatPercent` — the identical -call the list-view percent cell already makes for an ordinary percent column — -so a percent reads the same as a record field, as a grid cell and as a dashboard -measure. Output moves where it was wrong: values at or above 1 whose round trip -lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly -`1`, which is one percentage point by core's convention and now renders -`1.00%` at two decimals, where the local `value > 1` test had made it -`100.00%`. diff --git a/.changeset/owd-width-single-home-5477.md b/.changeset/owd-width-single-home-5477.md deleted file mode 100644 index 10aa33c86..000000000 --- a/.changeset/owd-width-single-home-5477.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ObjectSettingsPanel` now calls `isExternalWider` from `owd-sharing.ts` instead of -re-declaring `OWD_WIDTH` and the ADR-0090 D11 width comparison inline (objectui#5477). - -`owd-sharing.ts` states its own purpose — it is "the single home" for the pieces the -per-object Settings tab and the package-level OWD overview must agree on, the D11 -"external ≤ internal" comparison among them — and the Settings tab was the one surface -not calling it. The two implementations were verified equivalent before the swap, over -the full domain of both dials plus `undefined`, the rejected legacy aliases and -prototype-chain keys: 144 input pairs, zero disagreements. The module's extra `!!` -truthiness guards are redundant at this call site, which already normalizes both dials -to `''`, and `'' in OWD_WIDTH` is false regardless. So no author-visible verdict -changes — the same three pairs warn, and the same twenty-two stay calm. - -The substantive half is the pin that keeps it that way. `ObjectSettingsPanel.owdAgreement.test.tsx` -drives BOTH surfaces over the full 5×5 cross-product of the values their dials offer and -requires all three legs — the Settings tab's rendered warning, the overview's per-row -error, and `isExternalWider` itself — to agree on every pair, with the violating pairs -pinned by name so the sweep cannot pass vacuously. Re-inlining a drifted copy into either -surface, swapping the `(internal, external)` argument order, or refining D11 in only one -place now fails a test instead of silently leaving the authoring surface enforcing the -old rule. diff --git a/.changeset/page-source-tailwind-framing-5461.md b/.changeset/page-source-tailwind-framing-5461.md deleted file mode 100644 index 476267511..000000000 --- a/.changeset/page-source-tailwind-framing-5461.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/types': patch ---- - -`PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted. - -This is a published type surface: the TSDoc ships in `@object-ui/types`'s built -`.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'` -page is "constrained JSX/HTML + Tailwind" — and it links -`content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to -say the opposite. Shipped type documentation was contradicting the guide it points -readers to. - -ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that -framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind -is compiled at build time by scanning the console's own `src`, and there is no -safelist — so an authored utility class produces CSS only by coincidence, when -objectui already ships that exact class, and otherwise produces nothing with no -error anywhere. That is the ADR-0065 failure mode verbatim ("works only by -coincidence"), and it is how a modal's `bg-black/50` backdrop reached production -fully transparent. - -The tiers themselves are unchanged, and every load-bearing claim in the TSDoc -survives verbatim — parse-never-execute and untrusted-author safety for `html`, -the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the -`react-pages` host capability for `react`, the ADR-0080 citation and the guide -link. Only the styling conclusion changes, to the primitive each tier actually has: - -| `kind` | Style with | -|---|---| -| `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows -light/dark and whatever theme the deployment installs. The TSDoc now also names the -rule that reports a violation — `page-source-className-tailwind`, shipped in -`@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by -`os validate` as a warning on both tiers. - -No behaviour change, and the accepted `kind` set is untouched. - -`packages/components/src/renderers/layout/react-page.tsx` carries the same -correction on its two source comments (the injected-scope note and -`buildComponentScope`), and gains the styling note the file was missing. Those are -internal comments — they do not project into any `.d.ts` and change no export — so -they get no entry of their own; there is nothing an `@object-ui/components` -consumer could read in a CHANGELOG and act on. diff --git a/.changeset/page-source-tailwind-prose-retraction-5469.md b/.changeset/page-source-tailwind-prose-retraction-5469.md deleted file mode 100644 index f125f3894..000000000 --- a/.changeset/page-source-tailwind-prose-retraction-5469.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/react-runtime': patch -'@object-ui/sdui-parser': patch -'@object-ui/components': patch ---- - -Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's -`source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, -Accepted) retracted. objectui#5461 corrected three sites; a multiline census -found eight more, in three spellings a line-oriented grep could not see. - -A page's `source` is *runtime metadata*. The console's Tailwind is compiled at -build time by scanning the console's own `src`, and there is no safelist, so it -never sees your page: an authored utility class produces CSS only by coincidence -(when objectui already ships that exact class) and otherwise produces nothing, -with no error anywhere. That is the ADR-0065 "works only by coincidence" failure -mode, and it is how a modal's `bg-black/50` backdrop reached production fully -transparent. `os validate` reports it as `page-source-className-tailwind`, a -warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. - -The tiers themselves are unchanged and every load-bearing claim survives — -parse-never-execute, the untrusted-author safety argument for `html`, and the -deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording -`content/docs/guide/react-pages.md` §Styling already uses: - -| `kind` | Style with | -|---|---| -| `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`. - -Why each package has an entry — each was measured against its built artefact, not -assumed: - -- **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes - `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is - the paragraph that routes untrusted-author work to the `html` tier, and it - carried the retracted framing line-wrapped across `:17-18`. It also gains the - §Styling section it was missing — the absence is why the framing survived here. -- **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects - verbatim into the published `dist/types.d.ts`. -- **`@object-ui/components`**: the corrected header of - `src/renderers/basic/html-elements.tsx` projects verbatim into the published - `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm - comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a - function body) and is included here only because the same package already owes - an entry. - -No behaviour change: this is prose only. `CHANGELOG.md` occurrences are -deliberately untouched — immutable release history. diff --git a/.changeset/param-dialog-title-fallback-4282.md b/.changeset/param-dialog-title-fallback-4282.md deleted file mode 100644 index 41c71b681..000000000 --- a/.changeset/param-dialog-title-fallback-4282.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's param-collection dialog now titles itself from `action.label` alone — -the unreachable `|| action?.title` fallback beside it is removed (objectui#4282). - -`title` is declared on no action surface in the ecosystem: it is absent from -`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from -`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` -inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` -`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, -`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand -side of that `||` could not be reached by authored metadata: a fallback that cannot -fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. -Nothing a user hits changes; the line now reads exactly one key, matching the -`description` line directly below it. - -`useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader so the alias cannot -be reinstated silently: an action carrying `title` and no `label` must open an untitled -dialog rather than a dialog named by a key no producer sets. diff --git a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md b/.changeset/paramtofield-reference-rule-derives-from-core-5312.md deleted file mode 100644 index a3aa770c3..000000000 --- a/.changeset/paramtofield-reference-rule-derives-from-core-5312.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`ActionParamDialog`'s "this param carries a reference target" rule now derives from the shared reference-field family instead of the last private copy of it. - -`packages/app-shell/src/utils/paramToField.ts` restated the rule inline as -`LOOKUP_WIDGET_TYPES.has(type) || type === 'user'` — the fourth and last -hand-maintained answer to one question ("does this widget resolve a foreign key, -so hand it `reference_to` / `display_field` / the rest of the picker config"). -The other three converged on `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` in -objectui#4770 / #4790 / #4815; this face is now the fourth. - -No reachable behaviour change. The shared set is one member wider (`tree`), and -that member can never be a widget key on this surface: it is absent from -`fields`' widget map and `mapFieldTypeToFormType` sends it to `field:lookup`, so -every key the rule tests arrives as `lookup`. Both halves are pinned, so -registering a real `tree` widget surfaces the change instead of shipping it -silently. - -The module's second rule — which widget keys degrade to a text input for want of -a declared `referenceTo` — is a different set over overlapping types (`user` -defaults its target to `sys_user` and must never degrade) and was deliberately -left un-merged, matching the same split the plugin-grid twin keeps. - -Also retires a comment that claimed the disjunction "moves in lockstep with -plugin-grid's `bulkParamToField` twin — the two param faces are never split". -Measured on the tip before this change, that was false in both senses: the twin -had read core's Set since objectui#4815 while this line read a private literal, -so the two shared nothing and no gate could report a split; and the two member -sets already differed, by `tree`. Lockstep now holds mechanically — the pin is -on object identity (a spy on core's `has`), so a member-identical private copy -fails where a value check would pass. diff --git a/.changeset/pending-drafts-bar-locale-parity-5705.md b/.changeset/pending-drafts-bar-locale-parity-5705.md deleted file mode 100644 index 6bafb8435..000000000 --- a/.changeset/pending-drafts-bar-locale-parity-5705.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings — -now exists in all ten locale packs. It previously existed only in `en` and `zh`, so -`ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and -`all-locales-key-parity` failed on `main` (objectui#5705). - -The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was -titled for the locale packs but reached only `zh`, so eight packs × five keys stayed -missing and the parity assertion — which carries no allowlist — was red on `main` and -on every PR whose diff touched source. Source-free diffs skip the shard that runs it, -which is why the breakage survived several merges. - -Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`, -`ru` puts the number last (`…: {{count}}`) as it already does for the sibling -`home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}` -because that value is a runtime message rather than a single token — both choices -carry an in-pack note. Terminology is taken from each pack's existing publish-bar -vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read -alike. - -Both interpolations survive verbatim in every pack — `{{count}}` in `count` and -`{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated -packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a -different node and is untouched. diff --git a/.changeset/pending-drafts-i18n-keys.md b/.changeset/pending-drafts-i18n-keys.md deleted file mode 100644 index 84dab23b4..000000000 --- a/.changeset/pending-drafts-i18n-keys.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/app-shell': patch ---- - -Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. diff --git a/.changeset/pivot-table-stable-empty-rows-5562.md b/.changeset/pivot-table-stable-empty-rows-5562.md deleted file mode 100644 index d7423a5b5..000000000 --- a/.changeset/pivot-table-stable-empty-rows-5562.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`PivotTable` no longer re-runs its cross-tabulation memo on every render when it -has no rows (objectui#5562). - -The component spelled the empty array twice — as the destructuring default for -`schema.data` and as the `Array.isArray` fallback that keeps a provider-config -object out of iteration — so a schema declaring no `data` key, or one whose -`data` is a provider config rather than rows, produced a fresh array identity on -every render. That value is the first entry of the memo's dependency list, so -the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the -aggregated matrix and the row/column/grand totals on every render, over nothing. -Both spellings now resolve to one module-scope frozen empty, so "no rows" is a -stable value and the memo holds. - -Wasted work only: the churn feeds a memo rather than a `setState`, and -`PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no -render loop was possible. The identical fix landed for `data-table` in -objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the -direct-use path those two did not cover, where `DashboardRenderer` and -`DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the -chain. diff --git a/.changeset/plugin-form-readme-classname-quantifier-5131.md b/.changeset/plugin-form-readme-classname-quantifier-5131.md deleted file mode 100644 index 112411d63..000000000 --- a/.changeset/plugin-form-readme-classname-quantifier-5131.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`README.md`'s "Not a `FormField` key" table said a field-level `className` is -"read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier -holds only for the renderer's *explicit* read — `className={fp.className}` on -the `section-divider` branch of -`packages/components/src/renderers/form/form.tsx`. The same renderer forwards -every key it did not destructure, and `className` is not among the names taken -off the field config, not among the ones `stripRendererOnlyProps` removes, and -so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input` -branch spreads it onto ``. A field-level `className` therefore lands -visibly on ordinary built-in controls, and a reader taking "exactly one" -literally concludes the opposite of what the code does (objectui#5131). - -The cell now describes the contract rather than the reader count: an undeclared -key still rides the props spread down to whichever component the field resolves -to, nothing in the contract promises that, and a registered widget honours it -only if it happens to spread its leftover props — the wording the docs site -already ships, so the two sources agree again. The advice in the row is -unchanged and was never wrong (`span` / `colSpan` for width, -`FormSchema.fieldContainerClass` for the grid), and the explicit -`section-divider` read is kept, now named as explicit. - -This is a documentation fix to a file `plugin-form` publishes to npm, which is -why it carries a version: the npm landing page only picks up the correction on a -release. No behaviour, export, type, or `dist` byte changes. diff --git a/.changeset/quick-reference-release-sync-5394.md b/.changeset/quick-reference-release-sync-5394.md deleted file mode 100644 index d88e63d9e..000000000 --- a/.changeset/quick-reference-release-sync-5394.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Release tooling and repo docs only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. - -`changeset:version` now runs `scripts/sync-quick-reference-release.mjs` after bumping the -manifests, so `QUICK_REFERENCE.md`'s "Current Release" block moves in the same commit as -the versions it quotes. Before this, the release path had no human in it and nothing -updated the doc, so the block fossilised once per release and the anti-fossil gate -`scripts/__tests__/quick-reference-current-release-4143.test.ts` reddened `main` on the -push build every time (objectui#4642, objectui#4977, objectui#5394). The gate is -unchanged — it is the judge; this is the thing that keeps the doc true. - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/react-pages-guide-source-contradictions-5413.md b/.changeset/react-pages-guide-source-contradictions-5413.md deleted file mode 100644 index dbc67fae9..000000000 --- a/.changeset/react-pages-guide-source-contradictions-5413.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Docs only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -`content/docs/guide/react-pages.md` contradicted the framework's sources on four points. -The guide is live material (`@object-ui/react-runtime`'s README links it twice), so each -one was reachable teaching. All four were re-measured against `objectstack@f094214b3` and -resolved toward the source: - -- **Tailwind styling.** The guide taught Tailwind `className` as the react tier's styling - primitive, which ADR-0080's 2026-06-30 amendment retracted under ADR-0065 (Accepted): a - page's `source` is runtime metadata the console's build-time Tailwind never scans, so an - authored utility class silently produces no CSS. Replaced with the per-tier primitive the - shipped `page-source-className-tailwind` rule names — inline `style={{ … }}` with - `hsl(var(--token))` on the react tier, structured props plus a JSON `style` object on the - html tier — and added a Styling section giving the mechanism. -- **`record:*` scope.** The family was offered as the illustrative in-scope example while - `os validate` rejects it (`react-block-needs-record-context`, severity error, matched by - type). The tag-derivation rule is kept — it is accurate — and the exclusion, the real - error text, and the per-block alternatives are now stated beside it. -- **`adapter.find` options.** The `Live data` sample passed `filters:`, which is not a - `QueryParams` key; `convertQueryParams` reads only `$`-prefixed keys, so the sample - returned the object's records unfiltered with no error. Corrected to `$filter`. The same - sample also treated the result as an array — `find` resolves to a `QueryResult`, so - `.map` on it throws; corrected to `res.data` alongside it. -- **Block inventory.** The guide conflated the runtime scope (every public non-container - block) with the authored contract (`REACT_BLOCKS`: `ObjectForm`, `ListView`, - `ObjectChart`, `Block`). Both are now stated as two tiers, with the generated - `react-blocks.md` named as the prop authority, and the flat-props example moved off two - deprecated `ObjectGrid` spellings (`pageSize`, `fields`) onto the canonical - `pagination` / `fields` on `ListView`. diff --git a/.changeset/readonly-richtext-raw-markup-5498.md b/.changeset/readonly-richtext-raw-markup-5498.md deleted file mode 100644 index c14d4ec2e..000000000 --- a/.changeset/readonly-richtext-raw-markup-5498.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -A readonly `markdown` / `html` / `richtext` form field now renders its content -FORMATTED instead of showing the user its markup source (objectui#5498). - -`RichTextField`'s readonly early return rendered `{value}` as a React text child, -so a readonly field of any of those three types displayed the stored markup as -literal characters — a markdown field's asterisks and hashes, a richtext field's -tags. The `prose` classes on that wrapper were the tell: they style rendered rich -content, and there was none to style. Every other read surface — grid, kanban -card, gallery, related list, dashboard record panel and the record detail page's -read mode — dispatches through `getCellRenderer` and rendered the same stored -bytes formatted, so one field disagreed with itself depending on which surface it -was read on. - -The readonly branch now renders through the same components `getCellRenderer` -resolves: `markdown` through the GFM renderer, `html` and `richtext` through the -sanitizing HTML renderer. The two renderers moved out of the package barrel into -`widgets/richTextDisplay.tsx` so the widget can reach them without importing the -barrel back, and both sides now read one shared type-to-renderer table rather -than two that can drift apart. - -The editor header's format label is fixed with it: it was computed as -`field.format || 'markdown'`, and `format` is declared on `date` / `datetime` / -`time` / `phone` / `auto_number` and on no rich-content type — so it read -`undefined` for every real field and labelled an `html` field "Format: markdown". -The label is now derived from the field type's display pipeline, so it names the -syntax the value is actually stored in. diff --git a/.changeset/record-alert-cta-label-i18n-4998.md b/.changeset/record-alert-cta-label-i18n-4998.md deleted file mode 100644 index c42fa2e98..000000000 --- a/.changeset/record-alert-cta-label-i18n-4998.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is -widened to `string | I18nLabel` in both copies (`properties.*` and the flat -compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. - -The renderer already resolves `action.label` through the same inline-locale-map -`pickLocalized` call as `title` / `body` (`const ctaLabel = -pickLocalized(props.action?.label, language)`), so a bare `string` declaration -was narrower than the renderer's own runtime behavior — the same -declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for -`title` / `body` one level up in the same interface (objectui#4998). - -Type-only: the block's published authoring surface still declares `action` as -a bare `object` with the member shape in prose only -(`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — -that half stays parked on the `ComponentInput` member-shape question (PR -#3795) and is out of scope here. diff --git a/.changeset/record-alert-row-binding-4807.md b/.changeset/record-alert-row-binding-4807.md deleted file mode 100644 index ecca389da..000000000 --- a/.changeset/record-alert-row-binding-4807.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/plugin-detail': minor ---- - -`record:alert` binds the row through `usePredicateRecordContext`, so an -author-declared `properties.visible` is actually consulted. - -`renderers/record-alert.tsx` was the last predicate face in the repo still -handing `useCondition` a root-only `{ record }` bag. Every other row-scoped -predicate — the four generic action renderers (objectui#4075) and app-shell's -`DeclaredActionsBar` (objectui#4077) — binds the row through the shared -`usePredicateRecordContext(record)` helper, which resolves the three spellings -objectui#5330 ruled on: canonical `record.status`, the deprecated row-action -shorthand `status`, and deprecated legacy `data.status`. - -Under the root-only bag only the canonical spelling worked, and the two others -failed in **opposite** directions — both of them silently, because this call -site is fail-soft: - -- **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator - threw. The legacy `${…}` path answers a throw with its own source text, a - non-empty and therefore truthy string, so the verdict was **SHOWN on every - row**. A banner the author had gated was permanently on screen. -- **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's - ambient predicate scope (`providers/ExpressionProvider.tsx`) carries - `data: {}`, so the predicate read that object instead of the row, compared - `undefined`, and the verdict was a constant false — **never shown**. - -**Behaviour change, stated plainly:** a shipped `record:alert` whose `visible` -was written in either deprecated spelling was inert and is now live. A banner -that was permanently visible may begin to hide, and one that never appeared may -begin to show — that is the point of the fix, but it is a verdict change rather -than a no-op. Canonical `record.*` predicates are unaffected in verdict: they -resolved before and resolve now, pinned on both polarities. An in-tree census -found no `record:alert` `visible` predicate outside this package's own tests. - -A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`, -with its own deliberate bindings (`data` is the data-source adapter there, not -the row). This change does not touch it; the two still compose as AND. - -The renderer's header comment described the shared-scope behaviour it did not -have. It now describes what the file does, including the fail-soft policy and -the two-gate composition. diff --git a/.changeset/record-detail-param-dialog-title-5610.md b/.changeset/record-detail-param-dialog-title-5610.md deleted file mode 100644 index f8c16e205..000000000 --- a/.changeset/record-detail-param-dialog-title-5610.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`RecordDetailView`'s param-collection dialog now titles itself from `action.label` -alone — the unreachable `|| action?.title` fallback beside it is removed -(objectui#5610). - -This was the second copy of the limb objectui#4282 removed from -`useConsoleActionRuntime`. `RecordDetailView` builds its own action runtime rather -than routing through that hook, so the two near-identical `paramCollectionHandler`s -have drifted as a pair and the first fix could not reach this one. - -`title` is declared on no action surface in the ecosystem: it is absent from -`@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from -`@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` -inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` -`ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, -`action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand -side of that `||` could not be reached by authored metadata: a fallback that cannot -fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. -Nothing a user hits changes; the line now reads exactly one key, matching the -`description` line directly below it. - -`RecordDetailView.paramDialogTitle.test.tsx` pins the reader so the alias cannot be -reinstated silently: an action carrying `title` and no `label` must open an untitled -dialog rather than a dialog named by a key no producer sets. A pin per reader is the -only shape that covers both handlers, since the hook's own pin cannot see this site. diff --git a/.changeset/record-picker-empty-text-i18n-5590.md b/.changeset/record-picker-empty-text-i18n-5590.md deleted file mode 100644 index 8c53ac4a9..000000000 --- a/.changeset/record-picker-empty-text-i18n-5590.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `emptyText` now resolves the inline per-locale map its -contract has admitted since rc.6, and its published declaration says so -(objectui#5590). - -`@objectstack/spec` widened this key to the `I18nLabel` union -(`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA -still carries it — measured, not assumed: -`ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })` -succeeds. The renderer honoured only the string arm, handing the map straight to a -text node. React refuses a plain object in a child position rather than stringifying -it, so an author writing the map form the contract accepts did not get a mis-rendered -empty state — the whole picker subtree threw -`Objects are not valid as a React child (found: object with keys {en, zh-CN})`. - -The read site now resolves through `pickLocalized`, the objectui-side helper the -sibling text-node sites already read through (`element:text.content`, -`element:button.label`, `page:card.title`), which spells a miss as `''` rather than -the spec resolver's `undefined`. The default is applied before resolution, so -`emptyText` absent still means "No records" and an authored empty string still -renders empty. - -The `ComponentMeta` entry, which held a single `'string'` arm precisely because the -renderer dropped the other one, now declares `['string', 'object']`. That narrowing -was correct for exactly as long as it was true: with the map arm reaching the screen -resolved, withholding it would be the false declaration in the other direction — the -manifest gate reporting `type-mismatch` on a legal write the same input's own -`description` teaches the author to make. The `apps/console` specimen that pinned the -narrow arm named this release condition in its own words ("keeps its single `'string'` -arm until the render site catches up") and is flipped here, keeping its controls. - -Three comments in the renderer deferred this gap to objectui#4163, which closed as -completed on 2026-08-15 while the gap was still open; the file now carries no -reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very -key as its worked example of an arm deliberately withheld, and is corrected in the -same change so the example stays true. diff --git a/.changeset/registerfunction-casefold-doc-5363.md b/.changeset/registerfunction-casefold-doc-5363.md deleted file mode 100644 index def190682..000000000 --- a/.changeset/registerfunction-casefold-doc-5363.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`ExpressionEvaluator.registerFunction` now documents the case-fold it has always -performed: the name is stored — and must be called — in UPPER CASE -(objectui#5363). - -`registerFunction('formatCurrency', fn)` registers `FORMATCURRENCY`, because the -method delegates to `FormulaFunctions.register`, which stores under -`name.toUpperCase()`. That fold is correct for the spreadsheet-style built-in -vocabulary (`SUM`, `IF`, `UPPER`) and is unchanged here — but nothing declared -it on the public method, and two things keep it from being self-evident at the -call site. The registry API stays case-insensitive, so `getFormulas().has()` and -`.get()` both answer to the original spelling and never reveal the fold; only -expressions see the stored key, because the evaluation scope is built from -`FormulaFunctions.toObject()`, a plain object whose identifiers are matched -case-sensitively. And a wrong-case call site does not raise: `evaluate()` -catches, warns, and returns `defaultValue ?? expression`, so the template -renders its own `${...}` source as literal text on screen rather than erroring. - -Behavior is untouched — this is the declaration catching up with what the code -enforces. It ships as a patch rather than as an empty changeset because the -JSDoc is emitted into the published `dist/evaluator/ExpressionEvaluator.d.ts`, -so it is what consumers see on hover. - -`ExpressionEvaluator.test.ts` gains three cases pinning the half that was -uncovered — that the given spelling does *not* resolve in an expression, that -the failure renders the raw template source instead of throwing, and that the -registry API stays case-insensitive underneath — so making registration -case-preserving fails a test instead of silently invalidating the new JSDoc. diff --git a/.changeset/release-lane-tests-and-concurrency-5404.md b/.changeset/release-lane-tests-and-concurrency-5404.md deleted file mode 100644 index 31f50fe34..000000000 --- a/.changeset/release-lane-tests-and-concurrency-5404.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -Two changes to `.github/workflows/changeset-release.yml`, the workflow whose last step -publishes to npm. - -**The duplicate `pnpm test` is gone.** It re-tested a commit already on `main`, so it -could never keep anything out — it could only stop the release afterwards, and it did: -runs #3606 and #3901, 2 of the last 27 release-PR merges, failed there and skipped the -changesets step entirely. It cost 31m19s of a 33m22s job (medians over the 106 runs of -#3712-#3911 that executed; install 6s, build 35s, the changesets action 24s). The -backstop for `main` is `ci.yml`'s push lane, which runs the whole suite under coverage -across four shards and enforces the thresholds on the merged report. - -**The concurrency group is now keyed by commit rather than by branch.** A group shared by -every push to `main` does not queue: GitHub holds one pending run per group and cancels -the rest, so 93 of those same 200 runs were `cancelled` with an empty jobs array — not -one step ever ran. On a workflow that publishes, a discarded run is a discarded publish. -Ordering moves into a fail-open wait step that holds a run until every older release run -has finished, so runs are still serialised, but a contended lane now delays a release -instead of dropping it. diff --git a/.changeset/report-view-datasource-object-key-5116.md b/.changeset/report-view-datasource-object-key-5116.md deleted file mode 100644 index 2d3435113..000000000 --- a/.changeset/report-view-datasource-object-key-5116.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -`ReportView` reads a report's data binding through the one key the contract declares — `dataSource.object`. - -The view accepted `resource` as a second spelling of `object`, in two places, and -named that spelling in a warning the user could read: - -``` -:171 liveReport?.objectName || liveReport?.dataSource?.object - || liveReport?.dataSource?.resource -:273 dataFetchSource.dataSource.object || dataFetchSource.dataSource.resource -:275 console.warn('ReportView: dataSource missing object/resource property') -``` - -`resource` is not on this binding. `ElementDataSourceConfig` declares `object`, -`view?`, `filter?`, `sort?` and `limit?`; its `@objectstack/spec` twin -`ElementDataSourceSchema` is a strict object, so an extra `resource` key is -*rejected* there rather than ignored; and the binding's own predicate -`isElementDataSourceConfig` decides on `object`. A `resource`-only binding -therefore was never a binding on any other renderer in the system — it rendered -here and silently produced nothing anywhere else, with neither end reporting a -problem. That divergence is what a consumer-side alias buys: one renderer -answering a question the contract says has no answer. - -`resource` is a real key on other surfaces — `CRUDSchema.resource`, the -`DataSource` adapter's first parameter, `LiveExportOptions.resource` — and all -three are untouched. None of them is this one. - -Behaviour, measured by rendering each input shape before and after. Only the -`resource`-only shape moves: - -| binding | before | after | -| --- | --- | --- | -| `object` only | queries that object | unchanged | -| `resource` only | queries it as if declared | not queried; named warning, no rows, fallback field list | -| both | queries `object` | unchanged | -| neither | not queried; warning | unchanged | - -So off-spec report metadata that used to render now fails loudly instead of -appearing to work. A producer census found nothing that would notice: no site in -this repository, and none in the `objectstack` framework repository, writes -`resource` onto a report `dataSource`. The limb was speculative in the commit -that introduced it, and per AGENTS.md #0.1 an off-spec spelling is corrected at -the producer, never taught a second dialect by the renderer. - -The `:275` wording now names only `object`. A diagnostic that lists a key the -contract does not declare is not a small thing: it is the system telling an -author — increasingly, an author's code generator — that the wrong spelling is -supported. diff --git a/.changeset/retire-theme-component-schema-5489.md b/.changeset/retire-theme-component-schema-5489.md deleted file mode 100644 index 121ce3a10..000000000 --- a/.changeset/retire-theme-component-schema-5489.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer -implemented (objectui#5489). - -`packages/types/src/theme.ts` declared a theme-manager **component** carrying -`themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and -`storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching -Zod object as a member of `ThemeUnionSchema` and therefore of -`AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no -`ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`, -and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS` -(`packages/components/src/renderers/placeholders.tsx`), so it did not even -resolve to a placeholder — a page declaring one got the registry's "Unknown -component type" panel (OBJUI-001) instead of a theme manager. Declared-but- -unenforced, removed under the maintainer ruling of 2026-08-21 on -objectstack#10485 (option B). - -Removed from the published surface: the `ThemeComponentSchema` type -(`@object-ui/types`), the `ThemeComponentSchema` Zod object -(`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the -`'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling -`type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than -accepted and then rendered as an error panel, which is pinned by a test. - -**The theme system is unchanged.** `Theme` (the spec's authoring theme -document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine` -(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and -untouched — the same ruling retains them explicitly. Author a theme as a -document handed to `ThemeProvider`; that path never went through the removed -component kind. diff --git a/.changeset/retire-usebranding-hook-5368.md b/.changeset/retire-usebranding-hook-5368.md deleted file mode 100644 index bf5f6e24d..000000000 --- a/.changeset/retire-usebranding-hook-5368.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Delete `apps/console/src/hooks/useBranding.ts`, a `@deprecated` wrapper with zero callers. - -Nothing published changes. The console's npm tarball ships only `dist`, `plugin.*` and -`README.md` (`files` in `apps/console/package.json`) — `src` is never in it — and the -package's single `exports` entry resolves to `plugin.js`, which is compiled from -`plugin.ts` alone and imports nothing from `src`. With no importer anywhere in the repo, -the hook was also absent from the built SPA bundle. Empty frontmatter is therefore the -accurate declaration: a source deletion under a released package that releases nothing. diff --git a/.changeset/retired-field-type-gate-4914.md b/.changeset/retired-field-type-gate-4914.md deleted file mode 100644 index ca91ef84f..000000000 --- a/.changeset/retired-field-type-gate-4914.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch -'@object-ui/components': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-view': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-list': patch ---- - -A RETIRED field-type spelling is now refused — out loud, once — by every -field-type predicate in the renderer, not just by the widget road -(objectui#4914, maintainer ruling B of 2026-08-18). - -`@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs -ahead of six predicate faces that previously granted a retired spelling -first-class treatment: the filter builder's operator buckets and its value -control (`@object-ui/components`), the detail page's highlight-strip picker -(`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), -the dashboard's `$expand` whitelist and `isLookupType` -(`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter -control (`@object-ui/plugin-list`). Each one now fires the migration -prescription on the console — once per spelling across all of them, never once -per predicate — and then answers as it would for a spelling it does not -recognise. - -This closes the whole CLASS rather than one word: the gate is quantified over -`RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the -day it lands. It is the shape objectui#4932 and objectui#4942 already -established for the form and inline-edit roads. - -Measured before the change, and the reason the fix is a gate rather than a -deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` -equalled the `user` bucket item for item, `computeLookupExpand` actively -requested `$expand` for it, `isLookupType('owner')` was `true` alongside -`reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as -`picklist` does. Deleting the members alone would have traded a visible -contradiction for a SILENT degradation — a filter picker collapsing to a bare id -box, `$expand` quietly stopping so cells show raw foreign-key ids — which is -verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. -The gate keeps that fallback and adds the half that was missing: the author is -told. - -The boundary question is answered on record: `owner` arriving through a -backend-vocabulary normalizer is an authoring error to refuse loudly, not -legitimate foreign input to tolerate. The open backend vocabulary those -normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, -`datetime_tz` and the rest are equally absent from the spec's closed `FieldType` -and are equally unretired, so they classify exactly as before. - -`RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` -move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that -package's published surface is unchanged apart from the newly ruled gate. -`@object-ui/components` is a consumer of the gate and `@object-ui/fields` -depends on it, so a single shared table could not live in `fields` — and a -second copy would have meant a second dedupe set and two console lines for one -spelling. No package gained a new dependency. - -A retired spelling never loses a stored value: `retypeFilterValue` is -deliberately not gated, and the refused filter row stays operable rather than -drawing a blank operator trigger. diff --git a/.changeset/retired-lucide-spellings-5622.md b/.changeset/retired-lucide-spellings-5622.md deleted file mode 100644 index aa246fa96..000000000 --- a/.changeset/retired-lucide-spellings-5622.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/components': patch -'@object-ui/plugin-list': patch ---- - -Repair five retired lucide icon spellings that reach a record-reading resolver, and pin -the names against the runtime `icons` record so the next lucide bump goes red instead of -silently blanking a glyph (objectui#5622). - -lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it -as a deprecated named export. A retired name therefore still imports, still type-checks, -and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it -is used as a STRING, because every string lookup here reads that record. Nothing goes red -either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at -implementation time. - -What a user sees change: - -- `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon - again. Its items become an `action:bar` schema whose renderers resolve `icon` through - `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been - drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. -- The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in - both the registration `icon` and the `name` input's `defaultValue`) resolves again: the - designer palette entry's glyph was blank, and an `icon` dropped from that palette - rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette - looks exactly as it did. -- `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` - (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The - gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher - landed in objectui#5586, so one view type no longer draws two different icons depending - on which switcher is on screen. - -Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and -alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather -than resolvability, because every retired spelling repaired here is the SAME component -object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, -`Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, -`XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the -export, would pass on the broken name. That is the blindness that let this ship. diff --git a/.changeset/richtext-cell-renderer-5452.md b/.changeset/richtext-cell-renderer-5452.md deleted file mode 100644 index 8b1448c2d..000000000 --- a/.changeset/richtext-cell-renderer-5452.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/fields": patch ---- - -A populated `richtext` field no longer renders as a blank cell (objectui#5452). - -`richtext` stores HTML — the spec documents the type as "Formatted content with -HTML/WYSIWYG", the showcase seed's own specimen is `

Rich text

`, -and this repo's designer bridge already maps `richtext` onto its `html` type. The -display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose -sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops -raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the -cell body came out empty — with no error, no fallback and no console warning, so a -populated field read as an empty field and anyone auditing data through a grid -concluded the records were blank. Measured on the same stored bytes, a neighbouring -`html`-typed column rendered them correctly, which is what ruled out "the value never -arrived". - -`richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` -(script/style/iframe/object/embed blocks, inline event handlers and `javascript:` -URLs removed) and keeps everything a rich-text editor legitimately emits — headings, -paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at -once: the grid, the kanban card, the gallery, the related list, the dashboard record -panel and the record detail page all resolve their read-mode cells through this same -`getCellRenderer`. - -The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" -one type by moving every `markdown` cell's trust boundary, so `markdown` still drops -raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. diff --git a/.changeset/safe-field-label-identity-5564.md b/.changeset/safe-field-label-identity-5564.md deleted file mode 100644 index b7ae0e40f..000000000 --- a/.changeset/safe-field-label-identity-5564.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`useObjectLabel` now keeps a stable identity when no i18next instance is bound, -so the memoization it advertises holds on the no-provider path too -(objectui#5564). - -react-i18next's `useTranslation` builds its return value out of a fresh `{}` on -every render when it has nothing to bind to (`const finalI18n = i18n || {}`, -which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived -with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`, -so the memo never held: measured 4 distinct returned objects across 4 renders -with no instance, against 1 with one. That is the wrong way round — the memo -exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in -heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case -as the one it exists to serve. - -Both memo dependencies are now pinned to module-level constants while no -instance is bound. The substitution is unobservable rather than merely -convenient: every `t()` call in the module sits inside a -`for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]` -under exactly the same "is there a usable instance" predicate — so while the -substitution is in effect, the closures cannot read either value. When an -instance appears the dependencies become the live values again, so a provider -mounting after first render recomputes the object exactly once and resolves -real translations from then on. - -No API change: no new exports, no signature changes, and the returned surface is -identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside -`useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the -memo's own docstring names. diff --git a/.changeset/schema-input-bridge-permanent-4622.md b/.changeset/schema-input-bridge-permanent-4622.md deleted file mode 100644 index 4b7805128..000000000 --- a/.changeset/schema-input-bridge-permanent-4622.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`toRenderableSchema`'s header now says the bridge is permanent, instead of instructing -callers to remove it (objectui#4622). - -No executable line changes — but the artifact is **not** unchanged, and that is worth -stating plainly rather than rounding to "comment-only". This package builds with plain -`tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc -is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both -what an editor shows on hover at every call site and bytes that ship. - -Measured by building the package the way the repo builds it, at both revisions: -`dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850 -to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19 -differing lines in the emitted file are JSDoc continuations and the three executable lines -are byte-identical, so the growth is the paragraph and nothing else. The trade is -deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old -paragraph's instruction produced once already. - -The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have -not been reconciled" and that "when it lands, the call sites using this can go back to -forwarding directly". Both halves went false when PR #4608 merged, and the second half is -the harmful one: it is an instruction whose trigger condition has now fired, sitting -directly above the function a future author is about to call. - -The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of -`@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring -an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548 -ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So -a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a -permanent crossing between two intentionally different types rather than scaffolding -awaiting a merge. - -Following the old instruction has a measured cost: five `apps/site` call sites were -forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly -five hours until PR #4621 routed all five through this function (objectui#4617). diff --git a/.changeset/sdui-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md deleted file mode 100644 index f14b09da7..000000000 --- a/.changeset/sdui-preview-page-source-tailwind-5470.md +++ /dev/null @@ -1,26 +0,0 @@ ---- ---- - -Publishes nothing — declared with an empty frontmatter rather than left undeclared. - -The changed files are `apps/console`'s three ADR-0080 browser preview harnesses -(`src/sdui-*-preview.tsx`) plus a test. They are dev-server-only: `apps/console`'s -vite config declares no `build.rollupOptions.input`, so the build's only entry is -`index.html` (resolved config, measured: `input` unset) — the three -`sdui-*-preview.html` entries and the modules behind them never enter `dist/`. -`@object-ui/console`'s `files` omits `src`, and its `exports` map has exactly one -entry (`.` → `./plugin.js`, built from `tsconfig.plugin.json` including -`plugin.ts` alone, which imports nothing from `src/`). Nothing here reaches a -consumer. - -Behind the change: page `source` is runtime metadata, and the console's Tailwind -is compiled at build time by scanning the console's own `src` with no safelist, so -a utility class authored in real page metadata produces no CSS and no error -(ADR-0065; ADR-0080's 2026-06-30 amendment). `sdui-tiers-preview.tsx` — the -harness making an explicit authoring claim — now styles with each tier's real -primitive (the html tier with ``'s structured props plus JSON `style` -objects, the react tier with inline `style` objects, colours as -`hsl(var(--token))`), so it demonstrates what authors are told to write and now -follows the theme in light and dark. The two renderer-plumbing harnesses keep -their Tailwind and declare the exception in their headers; a test pins both -halves against the shipped `page-source-className-tailwind` rule. diff --git a/.changeset/secret-widget-spellings-5375.md b/.changeset/secret-widget-spellings-5375.md deleted file mode 100644 index 2f405f211..000000000 --- a/.changeset/secret-widget-spellings-5375.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/core': minor ---- - -Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. - -Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path -(no `registerAllFields()`), before and after objectui#5322's fix: - -``` -type registry hit rendered type -ui:password true text -secret false text -field:secret false text -``` - -Two halves, per the maintainer ruling of 2026-08-20: - -- **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring - ERROR.** A form field's widget id (`widget`, else `type`) may name the - `field:` namespace or a bare name; any other namespace resolves no field - widget (objectui#5254) and used to degrade silently to a plain text box. - `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and - `assertValidSchema` throws. Behaviour change: a schema that previously - validated with e.g. `type: 'ui:password'` is now invalid — inventing a - plausible-looking widget id fails loudly instead of rendering clear text. - `field:` ids stay valid whether or not the widget is registered, since - registration is a runtime fact an authoring-time validator cannot see. -- **`@object-ui/components` — the known secret types cover the remaining - spellings.** Bare `secret` and `ui:password` render the native masked input, - and `field:secret` is refused outright like `field:password`. Existing authors - need no migration. - -`ui:password` **is** registered — as an SDUI node renderer for a top-level -`{ type: 'email' }`-style node — so an author who checked whether it resolved -got a yes and still got a clear-text box on the field path. No producer emits -any of the three; all are reachable only through a hand-authored standalone -form schema, which is exactly the surface where the author is the producer and -no normalizer sits in between. diff --git a/.changeset/self-import-gate-scan-once-5402.md b/.changeset/self-import-gate-scan-once-5402.md deleted file mode 100644 index 32ece0aee..000000000 --- a/.changeset/self-import-gate-scan-once-5402.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -CI tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`scripts/check-package-self-import.mjs` now separates the SCAN from the JUDGEMENT: -`scanRepository(root)` performs the expensive TypeScript parse once, and `judgeScan(scan, -exemptions)` is a pure filter over its result. `analyze()` keeps its signature and its -behaviour exactly (differentially verified against the previous implementation over both a -fixture tree and this repository, across all six exemption-table shapes), and the CLI's -output is byte-identical. - -The reason is `scripts/__tests__/check-package-self-import.test.ts`, which asserts this -repository is green under the repository's own exemption table AND under no exemptions at -all. Those were two calls to `analyze(repoRoot)` — the same full parse of ~3,100 files and -~30 MB of source performed twice — and one of them sat inside a 15-second `it()`. That -fits uninstrumented (measured 8.8 s) and does not fit under v8 coverage (measured 41.3 s), -so `ci.yml`'s `Test (coverage)` job failed 100% of the time from 2026-08-16 — 51 completed -jobs, 0 successes, 50 of them this one file, every one `Test timed out in 15000ms` — and -Codecov received nothing for four days (objectui#5402). - -It is a constant factor, not a race. `@vitest/coverage-v8` arms -`Profiler.startPreciseCoverage({ callCount, detailed })` in the worker BEFORE test modules -and their dependencies compile; V8 emits those block counters at compile time and does so -isolate-wide, so `node_modules/typescript` is instrumented too — `coverage.exclude` filters -the report, never the instrumentation. Measured here, the identical parse costs 4.1-4.9 s -uninstrumented and 32-35 s when coverage was armed first, and starting coverage AFTER the -same code is compiled costs nothing at all. - -The test file now scans once at module scope and judges it per assertion: the timeout-prone -test drops from 41,268 ms to 1 ms under coverage, and the file's total work halves. No -timeout was raised, nothing is skipped, and coverage is not disabled for anything. - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/sentry-undisableable-telemetry-5522.md b/.changeset/sentry-undisableable-telemetry-5522.md deleted file mode 100644 index 11941d4db..000000000 --- a/.changeset/sentry-undisableable-telemetry-5522.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in -(objectui#5522). - -`@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from -`apps/console/.env.production` — is what the hosted SaaS console and the on-premises / -air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the -bundle as a frozen object literal, so the DSN committed there was a live third-party -telemetry endpoint compiled into artifacts that land inside customer networks. It could -not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off -that same frozen literal, so on a shipped bundle it is `undefined` forever and editing -env vars on the deployed host does nothing. An air-gapped deployment was measured -sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by -the customer. - -- `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, - `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never - imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. -- `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so - IP address and User-Agent are never the inherited default of a build that did not ask - for them. -- The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not - send. The direction is deliberately inverted from the usual — an unreported error is - recoverable, PII leaving an air-gapped deployment is not. - -**Action required for deployments that want error reporting** (the hosted SaaS/demo -console): inject `VITE_SENTRY_DSN` from your build environment, the same way -`VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you -still want IP/User-Agent on events. Nothing else changes for builds that opt in. diff --git a/.changeset/shard-coverage-job-5403.md b/.changeset/shard-coverage-job-5403.md deleted file mode 100644 index 8e30f8848..000000000 --- a/.changeset/shard-coverage-job-5403.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. `ci.yml`'s coverage lane is sharded 4 ways with a blob-report -merge, and the Codecov upload can no longer go missing in silence: the merge job states -on every path whether Codecov received a report for the commit, and is red when it did -not. diff --git a/.changeset/skill-provider-envelope-teaching-5372.md b/.changeset/skill-provider-envelope-teaching-5372.md deleted file mode 100644 index 4e6dd9e2a..000000000 --- a/.changeset/skill-provider-envelope-teaching-5372.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Published-skill teaching only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. No package `src/` is touched: the -change is confined to `skills/objectui/**` (the published skill package) plus one new -test under `packages/components/src/__tests__/`, which pins the corrected teaching to -the real renderer. - -The rules told authors that the `properties` / `props` envelope belonged to the -`element:*` namespace and that every other key "lives on the node". Measured on a real -`SchemaRenderer` inside a `SchemaRendererProvider`, `properties` is evaluated and then -hoisted onto the node in *every* namespace — so it was the only spelling that reached a -`data-table`'s rows from a provider `dataSource`, while the node-level and `props` -spellings rendered a header over the empty state with nothing thrown and nothing logged. -The guides now record that measurement instead of contradicting it. diff --git a/.changeset/sparse-predicate-warning-cause-5399.md b/.changeset/sparse-predicate-warning-cause-5399.md deleted file mode 100644 index adebbd2ba..000000000 --- a/.changeset/sparse-predicate-warning-cause-5399.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it -states what it actually measured (objectui#5399). - -When an action's `visible` predicate references a `record.` the bound payload -does not carry, the warner names the missing key and then explained the cause: - -> Hidden (hidden: true) fields are stripped from detail payloads server-side, so a -> predicate gating on one may evaluate to a hide-by-default verdict. - -That cause is false, and it names a mechanism this repo does not own. `hidden` is a -UI concern — the framework spec describes it as "Hidden from default UI" -(`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against -the framework checkout rather than taken on trust: ObjectQL's own strip for the -`__search` companion documents that the `hidden` / `readonly` / `system` markers are -"None of them is a PROJECTION rule", which is precisely why a dedicated strip rule -had to be written for that one column; drivers answer a query with no `fields` using -`SELECT *`; and `metadata-protocol` enumerates what the read path does drop — -`internal: true` columns and the `__search` companion, and nothing else. The only two -read-side uses of `field.hidden` in the framework are auto-view/auto-form column -generation and companion-source eligibility, neither of which removes a key from a -record body. - -So an author who read this diagnostic went hunting for a `hidden` flag they would -either not find, or find on a field the payload demonstrably still returns — while -the real source of the sparseness (a projected or partial read) went unexamined. A -confidently wrong cause in a diagnostic is worse than no cause, because it is -actionable in the wrong direction. - -The replacement states the fact this surface can actually see and the consequence it -does own: the page bound a payload without those keys, a projected or partial read -will not carry them, and the predicate therefore fails closed and hides the action. -The measured half of the message — action name, missing fields, predicate source — -is unchanged, and nothing about what triggers the warning changed. - -Message text only. The same false claim also sat in this warner's own doc comments -and in the comments of the test that pins the message; both are corrected here, and -the docstring now carries an explicit note against re-attributing the cause to -`hidden: true`. No other call site was swept. diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md deleted file mode 100644 index 261be17ee..000000000 --- a/.changeset/spec-pin-17-1-0-5328.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/data-objectstack': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-view': minor -'@object-ui/core': minor -'@object-ui/app-shell': minor ---- - -Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. - -The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. - -**A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. - -The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. - -`@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. - -**A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. - -**The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. - -**The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. diff --git a/.changeset/stale-4163-pointers-5591.md b/.changeset/stale-4163-pointers-5591.md deleted file mode 100644 index 7b77930b5..000000000 --- a/.changeset/stale-4163-pointers-5591.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -`setLocalized`'s published docblock states the single-locale write rule that is -actually in force, instead of deferring the multi-locale-authoring question to a -closed card (objectui#5591). - -The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163 -closed as completed on 2026-08-15 with that product question still unanswered, so the -parenthetical pointed at nothing — and it read as though the question had been settled -somewhere a reader could go and check. This is the failure mode objectui#5428 -demonstrated is not harmless: there, a dangling deferral of exactly this shape let an -expired justification sit unread for a release cycle at two surfaces. - -The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in -force (`setLocalized` reaches only the entry for the locale the author is in), keep the -open product question open **in place**, and record why there is deliberately no tracker -reference — so the next reader cannot restore one. Re-pointing is how the class -regenerates, because the next card closes too. The same wording form already landed in -`plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`. - -Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with -`pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`. - -Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per -package rather than assumed, and the two packages this change touches differ: - -- `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches - the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is - `composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count: - `dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js` - `06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so - it publishes something. -- `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between - declarations and a test docblock, neither attached to an exported declaration. - `dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild - (`93252e8cdf5a6faa…` both sides). The only artifact that moved is - `dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above - the declarations; no declaration text changed. Nothing user-visible publishes from that - package, so it is not named here. diff --git a/.changeset/standing-pending-drafts-bar-5694.md b/.changeset/standing-pending-drafts-bar-5694.md deleted file mode 100644 index f1d669f65..000000000 --- a/.changeset/standing-pending-drafts-bar-5694.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -AI build surface gains a standing 「未发布改动」 bar (#5694): while the conversation's bound package has pending drafts, a bar floats above the composer — surviving scrolling — counting the unpublished changes and publishing them through the same governed `publish-drafts` route as the inline card button, with probe findings surfaced instead of a blind success toast. Renders nothing when the count is zero or the conversation is unbound. diff --git a/.changeset/studio-metadata-form-ux-5416.md b/.changeset/studio-metadata-form-ux-5416.md deleted file mode 100644 index 551450b9e..000000000 --- a/.changeset/studio-metadata-form-ux-5416.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button. - -Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on -the first surfaces a new author sees. - -**Validation no longer runs on mount.** `新建软件包` opened with both required -fields already red: the create draft is `{ version, type }`, so -`ManifestSchema.safeParse` reported `name` and `id` missing on the very first -render, and the dialog then jumped a line height per field as each error -cleared. `SchemaForm` now defers the error *line* until the row has been -touched — first focusout anywhere in it, or the field's own first edit — for -create forms only. The rule itself did not move: `issues` still reaches the host -unchanged, so the submit button is gated on exactly the same validation, and -edit/view forms still report from mount, where the issues describe stored values -rather than something half-typed. - -**The package form's help text is translated.** The labels came from the -metadata-admin i18n bundle and the help line under each one came straight from -`ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over -"Human-readable package name". `getPackageForm` now reads each field's help -through the same bundle as its label, via a new `tOptional` that returns -`undefined` rather than echoing the key back. Only zh entries exist: an en-US -console finds nothing and keeps falling through to the spec's own sentence, so -the English keeps exactly one producer — `@objectstack/spec` in the framework -repo — and this repo never holds a copy of it to drift. - -**The publish panel's primary button opens clear of the toast stack.** The -console mounts its toaster bottom-right and `DraftChangesPanel` is a -`side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way -there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed -out. Opening the panel now clears the stack the surface the author just left had -raised. Repositioning the toaster was measured and rejected: the draft preview -bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar` -anchors bottom-centre and the nav rail owns the left edge, so a move only -relocates the same collision onto a different primary control. - -Not fixed here, and not fixable here: the other strings the card names are -produced outside this repo. `Owning Business Unit` (`packages/spec`), -`Search Index` (`packages/objectql`) and the `Revise Window` flow node's name -and description (`packages/plugins/plugin-approvals`) all come from the -framework, which owns their translation catalogue; patching them in the console -would create a second source of truth that diverges at the next framework -release. diff --git a/.changeset/studio-nav-canonical-keys.md b/.changeset/studio-nav-canonical-keys.md deleted file mode 100644 index 73ce02c26..000000000 --- a/.changeset/studio-nav-canonical-keys.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio Interfaces: the nav-leaf binding reads the canonical target key only - -`resolveSurface` fell back to the bare spellings `page` / `object` / -`dashboard` / `report`, and carried a `case 'view'`. Every -`NavigationItemSchema` member is a `strictObject`, none of those bare -spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is -not one of the union's nine members — so all of them are keys `AppSchema` -answers with `unrecognized_keys`, and every one of those branches could only -fire on an app that cannot be saved. The Studio nav item inspector's object -picker likewise read `node.object ?? node.objectName`, preferring the rejected -spelling over the canonical one; it now reads the canonical key first. - -No shape that parses today stops parsing: this narrows the designer back to -what the contract already declares. diff --git a/.changeset/studio-new-object-asks-for-owd-5418.md b/.changeset/studio-new-object-asks-for-owd-5418.md deleted file mode 100644 index 05f2f1e0c..000000000 --- a/.changeset/studio-new-object-asks-for-owd-5418.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/i18n': minor ---- - -Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. - -Creating an object through Studio collected exactly two things — display name and -identifier — and saved a draft that declared no `sharingModel`. The draft saved -happily, the form designer worked, and the object was then refused at 发布 → -全部发布 by `security-owd-unset`: a required decision the surface never asked -for, delivered by failing, as English ADR prose in a toast that then vanished on -a timer. The one actionable word in it named a control three clicks away that -nothing routed to. - -The publish gate is correct and is unchanged — an org-wide default has to be an -authored decision, not an accident. What changes is when the console asks and -when it answers: - -- **The create dialog asks.** A third field collects the baseline, pre-selected - to `private` and glossed with the Settings tab's own strings, so a new object - is publishable by construction. `buildObjectSkeleton` now takes the value as a - required parameter — a future create path cannot omit the baseline without - failing to type-check. `controlled_by_parent` is deliberately not offered at - creation: it derives access from a master relation a brand-new object does not - have yet, so offering it would trade one publish refusal for another. -- **The review sheet reports it.** The pending-changes panel now runs the - framework's own `validateSecurityPosture` over the pending object drafts and - names any blocking finding, with its fix-it hint, next to the Publish button. - It mirrors the producer's rule rather than re-deriving it, and it reports - without blocking — the server door stays the authority. -- **The Settings tab stops calling an unset baseline safe.** It described unset - as "defaults to Private", which answers what the runtime does and not whether - the object can ship. It now reads as the publish-blocking problem it is, - styled like the external-wider warning beside it. diff --git a/.changeset/studio-route-entry-capability-5519.md b/.changeset/studio-route-entry-capability-5519.md deleted file mode 100644 index bed857904..000000000 --- a/.changeset/studio-route-entry-capability-5519.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The Console now gates the `/studio/*` routes on the `studio.access` ENTRY -capability, not just on the backend's refusal of the writes behind them -(objectui#5519). - -`/_console/studio/` rendered the full Studio pillar builder — Data / -Automations / Interfaces / Access, with Publish and Save draft — to any -authenticated principal who typed the URL, on deployments where the Studio nav -tile is deliberately absent and every metadata write is refused. A plain tenant -user was walked through the entire "new package" form and only refused at -submit (403). The lockdown criterion for that deployment shape is two-part — UI -entry hidden AND API refused — and only the API half was met; what stood on -this side was a write-level gate where an entry-level one belongs. - -The whole `/studio` subtree now hangs off one route element that reads -`systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this -app already consumes) and admits only a principal whose LOADED set carries -`studio.access` — the capability declared as "Enter the Studio metadata-design -surfaces", which a tenant org owner does not hold by design. Everyone else is -sent to `/home` without the builder ever mounting. - -The fail direction is deliberately inverted from this app's other capability -gates: those fail OPEN on an unknown answer because their bad outcome is a -holder losing a button, whereas a route gate's bad outcome is a non-holder -seeing the builder. So the loading window renders the console splash (never the -builder), an outright fetch failure renders the retryable error splash, and a -`200` that carries no `systemPermissions` at all is refused rather than waved -through. The server-side refusals are untouched. diff --git a/.changeset/surface-deeplink-live-channel-5476.md b/.changeset/surface-deeplink-live-channel-5476.md deleted file mode 100644 index 9515eb71b..000000000 --- a/.changeset/surface-deeplink-live-channel-5476.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Studio's pre-publish security block can now take you to the object it names. - -The pending-changes sheet reports what the publish door would refuse — as -`object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under -Settings → Record sharing". Naming it was the half that shipped; reaching it was not. -The `?surface=:` deep-link that would have carried the author there -captures the URL exactly ONCE, at mount, and the sheet is opened over an -already-mounted pillar — so writing the param changed the URL and moved nothing. - -That mount-time capture is deliberate and stays exactly as it was: the mirror half -rewrites the param on every in-pillar selection, so a capture that followed the URL -would re-trigger its restore on each one. What was missing is a third half — a live -target delivered BESIDE the URL, which is what a producer already inside the pillar -needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity -(`{type, name}`), the host routes cross-pillar requests back through the URL (that -pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the -author declines over unsaved edits, and the mounted pillar applies the rest. - -Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next -rail reload would drag the author back off whatever they had since selected — the -regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` -pins a hand-picked object surviving a package switch, and -`surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it -still ignores every URL change after mount, and a live request never moves it. - -The sheet's other home is the Home / draft-preview bar, where the Studio object editor -is not a reachable destination at all. Reachability is answered structurally — the -producer hook returns `null` when no host published the channel — so off-Studio the -item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both -directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. - -Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its -signature, its return and its behaviour, and only the Data pillar subscribes to the -new channel. The channel is its own React-only module on purpose — importing the hook -into the sheet would have pulled `nav-selection` and the App-nav inspector into the -console's eager graph. diff --git a/.changeset/table-renderer-declared-column-contract-5350.md b/.changeset/table-renderer-declared-column-contract-5350.md deleted file mode 100644 index a405857ba..000000000 --- a/.changeset/table-renderer-declared-column-contract-5350.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': minor ---- - -The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling. - -`renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a -cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared -on `TableColumn`, which declares `header` and `accessorKey` — both required -(`packages/types/src/data-display.ts`). This was the fourth site of the -column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid` -(objectui#5350). - -Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the -family direction — *retire the consumer-side alias; unify the producers* — and it -names this site: the declared `header`/`accessorKey` contract wins. - -What makes this site different from its three siblings is that the alias was not -merely tolerated, it was **published**. `content/docs/api/schema-reference.md` -§TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a -property row reading *"Column definitions with `name`, `label`, …"*, while -`packages/types` declared the opposite pair. Docs and type disagreed about one -type they both call `TableColumn`, each internally consistent. Retiring the alias -without correcting the page would have turned a documented, working example into a -silently broken one, so both halves land together: the page now authors -`accessorKey`/`header`. The same row also advertised a `render` property that -`TableColumn` has never declared — the renderer's hook is `cell` — and that claim -is dropped rather than re-spelled. - -The failure mode of a now-unresolvable column is worth stating, because it is -quiet: the column keeps its slot and its neighbours are unaffected, the header or -the cells simply render empty, and nothing throws. This renderer keys its cells by -index rather than by accessor, so unlike `data-table` it does not even produce -React's generic missing-key warning — a retired-spelling column is fully silent. -Whether that silence should become an authoring diagnostic is objectui#5349's -question; no diagnostic is added here. - -The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so -re-introducing an undeclared alias on this renderer is now a type error rather -than a reviewer's catch. diff --git a/.changeset/tenant-header-edge-contract-5279.md b/.changeset/tenant-header-edge-contract-5279.md deleted file mode 100644 index 3ba15b941..000000000 --- a/.changeset/tenant-header-edge-contract-5279.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the -unstamped-first-request window in which it is not sent (objectui#5279). Documentation -only — no behaviour changes. - -The header had no written contract anywhere, and the shape of the missing information was -actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a -search confined to this repo and the framework (`objectstack`) returns zero readers and -reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was -held until a cloud-side reading came back non-empty. Without the contract written down, -the next person to grep reaches the same false conclusion and deletes a live routing -input. - -`packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means -(a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim, -not an authorization input, not what scopes rows), who stamps it and under exactly which -condition, who reads it, and what a reader may and may not assume. The framework half is -stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the -API-key principal or `session.activeOrganizationId` and from no header — alongside -`plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability. -The configuration half is quoted from the contract this package can actually resolve, -`TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the -default of a configurable `tenantHeaderName` and `header` ranks second of six -identification sources behind `subdomain`. - -The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is -filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` -> -`getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at -all. What a reader observes is documented as **absent, never present-and-empty**, with the -five situations that open the window and the instruction to fall through to the next -identification source rather than fail closed. The gap is recorded, deliberately not -closed: the cloud readers observe today's behaviour, so changing when the header first -appears is its own decision. - -Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes -about the wire — no active organization means no header at all, the stamp is not gated on -`/api/` the way `Authorization` is, and the active organization overwrites a caller-set -`X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed. diff --git a/.changeset/tidy-donkeys-attack.md b/.changeset/tidy-donkeys-attack.md deleted file mode 100644 index a7314fd9e..000000000 --- a/.changeset/tidy-donkeys-attack.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@object-ui/plugin-form": patch ---- - -`navigateOnSuccess` now honours a mounted host, and says so when its destination is refused - -`ObjectForm` and `WizardForm` consume `navigateOnSuccess` through -`resolveSuccessNavigate`, and both arms travelled to an accepted destination with a bare -`window.location.assign`. A rooted path such as `/apps/x/o/record/{id}` assigned that way -resolves against the ORIGIN root, so under a host mounted at a sub-path (the framework CLI -configures one for every embedded deployment) an authored in-app destination left the -application. Both arms now route an app-relative destination through the injected -navigation seam both components already held for `submitBehavior.url`, so a mounted host's -basename is applied. With no host seam the behaviour is byte-for-byte what it was — a host -with no router has no basename, so origin-rooted resolution is already correct there. A -same-origin ABSOLUTE destination also keeps browser-level navigation: the seam's declared -input is an application-relative path, and an author who spelled out a whole address asked -for that address. - -A declared `navigateOnSuccess` whose destination is refused — a mistyped value, or a written -record carrying no usable id — used to produce a success toast identical to the one a form -with no `navigateOnSuccess` produces, so the navigation failed with nobody told. That toast -now carries a note that the declared navigation did not happen, and the template the author -wrote is logged for them. The write genuinely succeeded, so this stays a success rather than -becoming an error state. - -Which destinations are ACCEPTED is unchanged: the same-origin guard, the `{id}` / -`{recordId}` dialect and the unescaped interpolation are the subject of an open contract -question and are deliberately untouched here. diff --git a/.changeset/unbundled-node-boundary-5384.md b/.changeset/unbundled-node-boundary-5384.md deleted file mode 100644 index cefdbb061..000000000 --- a/.changeset/unbundled-node-boundary-5384.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-map': patch -'@object-ui/app-shell': patch ---- - -Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. - -`@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module -scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; -`@object-ui/app-shell` reaches the first of those through the static -`@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has -no loader for `.css` at all, so all three resolve and then die during evaluation: - -``` -TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" - for .../react-grid-layout/css/styles.css -``` - -Nothing about how these packages load has changed — every supported host bundles -them (Vite, webpack, or Next with the package in `transpilePackages`), and that is -still the only supported way to consume them. What changed is that the boundary is -now written where a consumer meets it, instead of being learned from a red import. - -objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying -plugin packages — permanently, over the three packages as a group — rather than -moving the stylesheet imports out of module scope. No unbundled-Node consumer -exists, and buying permanent machinery to close a capability gap nobody is pulling -on was the trade the ruling declined. A real consumer request reopens it as a -design question, not as a defect: the READMEs say so and name the issue. diff --git a/.changeset/uniqueness-deprecation-jsdoc-4765.md b/.changeset/uniqueness-deprecation-jsdoc-4765.md deleted file mode 100644 index 64fc315d6..000000000 --- a/.changeset/uniqueness-deprecation-jsdoc-4765.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/types': patch ---- - -Corrects the `@deprecated` prescription on `UniquenessValidation` in -`packages/types/src/data-protocol.ts`, which pointed authors at spellings the platform -no longer accepts (objectui#4765). - -Comment-only — no runtime behaviour changes. `patch` rather than an empty frontmatter -because the JSDoc sits on an **exported** declaration and therefore ships to consumers: -measured with the package's own build (`tsc`, and `tsconfig.base.json` deliberately sets -`removeComments: false`), `dist/data-protocol.d.ts` goes 40218 → 41781 bytes and the new -prose is present in the emitted `.d.ts`. What a consumer reads on hover changes, so it -is declared. The emitted `dist/data-protocol.js` is byte-identical (sha256 -`a3de34c5…`, 207 bytes both ways) — that file is a types-only module whose entire JS -output is the license banner plus `export {}`, so a comment on an erased `interface` -reaches the declaration file and nothing else. - -Two of the three spellings it prescribed were wrong, measured against the installed -`@objectstack/spec@17.0.0` (the report was written against `17.0.0-rc.6`): - -- **`indexes[].partial`** was retired in spec 17.0.0 under ADR-0049. It is a tombstone - (`z.never()`) that the parse rejects at any value, so "`partial` for a scoped - constraint" named a key that cannot be declared. A predicated unique constraint is - built at the database layer by a runtime migration issuing - `CREATE UNIQUE INDEX … WHERE`; the prescription now says so. -- **`{ fields, unique: true }`** on `ObjectSchema.indexes` is the deprecated positional - spelling of `unique: 'global'` under ADR-0120 — lint `unique/unscoped-declared-index` - warns in 17.x and protocol 18 rejects it. The prescription now states the scope: - `unique: 'global' | 'organization'`. - -The measurement also refined the report, and the refinement is the reason the rewrite is -not a uniform find-and-replace. The third spelling — **field-level** `unique: true` — is -NOT deprecated. `unique` is scope vocabulary shared by two surfaces on which the same -bare `true` means different things: at index level it stays verbatim (`isGlobalUnique` -and `isOrganizationUnique` both return `false`), which is why it is the positional -spelling of `'global'` and is being retired; at field level it is the positional spelling -of `'organization'` and, in the spec's own words, "stays valid indefinitely … no trap". -Rewriting both occurrences the same way would have replaced one piece of false guidance -with another, so the comment now names the per-surface difference explicitly. - -The interface's own deprecation is untouched and remains correct: `ValidationRuleSchema` -rejects `type: 'unique'` at the discriminator (accepted discriminants are `script`, -`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`), so a rule in -this shape cannot reach the server. - -The replacement closes with what would falsify it — `UniqueScopeSchema` and -`IndexSchema` in `@objectstack/spec` — so the next reader checks the schemas rather than -trusting the paragraph. This is the fourth piece of false guidance found in this -campaign (strictness ledger finding 18), and prose that cannot be checked is how the -first three survived. diff --git a/.changeset/utilities-index-phantom-provider-5360.md b/.changeset/utilities-index-phantom-provider-5360.md deleted file mode 100644 index 4c7a34666..000000000 --- a/.changeset/utilities-index-phantom-provider-5360.md +++ /dev/null @@ -1,27 +0,0 @@ ---- ---- - -Docs and gate ledger only — this publishes nothing, declared explicitly with an empty -frontmatter rather than left undeclared. - -`content/docs/utilities/index.md`'s "Data Integration" section taught -`import { ObjectStackProvider } from '@object-ui/data-objectstack'`, a React context -provider on a package that is headless and exports no such thing. Compiled against the -built `dist/index.d.ts` by the same harness `scripts/check-doc-snippet-types.mjs` uses, -the block read -`TS2724: '"@object-ui/data-objectstack"' has no exported member named 'ObjectStackProvider'`, -so a reader who copied it did not get a runtime bug — they got a compile error. #4124 had -already established the finding and PR #4129 fixed it on the sibling page -`content/docs/utilities/data-objectstack.mdx`; the identical phantom survived one file -over, in the utilities index, outside that card's file surface. - -The section now teaches the same shape PR #4129 established: `createObjectStackAdapter` -returning a plain `DataSource`, injected at the renderer boundary through -`@object-ui/react`'s `SchemaRendererProvider`. The block is also self-contained — it -imports every name it uses and types its schema literal as the real `ObjectGridSchema` — -so it compiles exactly as a reader who copies that one block experiences it. - -Because that was the page's only `ts`/`tsx` block and it now produces zero diagnostics, -`content/docs/utilities/index.md` LEAVES `check-doc-snippet-types.mjs`'s `UNGATED_DOCS` -ledger instead of getting a re-measured reason: the page is compiled by the gate from -here on, and no entry on that ledger names a missing export any more. diff --git a/.changeset/view-switcher-chart-gantt-icons-5586.md b/.changeset/view-switcher-chart-gantt-icons-5586.md deleted file mode 100644 index 257d32d34..000000000 --- a/.changeset/view-switcher-chart-gantt-icons-5586.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -`ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both -icon maps in the package now name only spellings lucide still resolves -(objectui#5586). - -`ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up -in lucide's runtime `icons` record. lucide retires a spelling by dropping it -from that record while KEEPING it as a deprecated named export, so a retired -name still imports, still type-checks and still renders as a component — and -silently resolves to nothing as a string. `ObjectView` composes the switcher -from names, and two of them had been retired on lucide-react 1.31.0: -`chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a -label with no icon at all while every sibling type had one, and nothing went red -because no lucide symbol appears in that map for the compiler to check. Measured -against the installed package: `BarChart3` and `GanttChart` are absent from -`icons`, while `ChartColumn` and `ChartGantt` are present. - -- `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`, - `gantt-chart` → `chart-gantt`. -- `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named - deprecated aliases move to the names the record carries — - `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`, - `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the - aliases already pointed at, so those two glyphs are unchanged; the `gantt` - default picks up the plain gantt glyph, which is what `iconMap` now supplies - for that view type. - -The regression pin widens from `tree` alone to EVERY name both maps supply: a -pin scoped to the two names that broke would not have caught this and would not -catch the next lucide bump. diff --git a/.changeset/visiblewhen-record-binding-5454.md b/.changeset/visiblewhen-record-binding-5454.md deleted file mode 100644 index 8cf9b2e4b..000000000 --- a/.changeset/visiblewhen-record-binding-5454.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/react': minor ---- - -Bind `record` into the node-level visibility evaluator, and stop a hoisted -`properties.visible` swallowing a declared `visibleWhen`. - -`@objectstack/spec` has declared since ADR-0089 that a page component's -`visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`, -`current_user`, `page.`"*. `SchemaRenderer` bound no `record` at all. Its -evaluator was built from the ambient predicate scope, `data: dataSource` (the -connector **adapter**, not the row) and `page: pageVariables`; the row lives in -`RecordContext`, which that evaluator never read. - -Because the surface is fail-soft, a `record.*` predicate did not misfire — it -resolved to **shown**. Both polarities of the same predicate returned the same -verdict, so a visibility gate silently did not gate, on every block on every -record page. Measured on `record:alert`, `record:path`, `page:card` and -`element:text`. - -Three changes, all in `SchemaRenderer`'s evaluation memo: - -- **`record` is bound**, as the `record` root only — the three roots the - describe promises and nothing more. Not as bare fields, and never over - `data`, which is what `${data.*}` in a props bag resolves against. Bound - conditionally, so "no row" binds nothing rather than shadowing a `record` a - host supplied through the ambient scope. -- **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` - onto the node, so a node carrying `properties.visible` short-circuited the - declared node predicate — the one key the spec tells authors to write was the - one key that could be silently ignored. The two deprecated aliases - (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into - `visibleWhen` at parse, so a spec-parsed page never reaches them. -- **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this - predicate is broken" and "this predicate said yes" with the same word. The - verdict is unchanged on every path — `evaluateCondition` already returned - `true` for every unevaluable predicate, including the non-negated `hidden` / - `hiddenOn` legs where that `true` means HIDE — so only the silence moved. - -**Behaviour change, stated plainly:** a shipped page whose node-level -`record.*` predicate was previously inert now evaluates. A block that was -permanently visible may begin to hide — which is the point, but it is a verdict -change, not a no-op. `properties.visible` is unaffected in verdict: an -in-tree census found **zero** node-level `record.*` predicates on page -components, so nothing in this repository changes verdict. diff --git a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md b/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md deleted file mode 100644 index 61e50a177..000000000 --- a/.changeset/vitest-invocation-guard-package-cwd-coverage-5406.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Test tooling only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. - -`assertCanonicalVitestInvocation` refuses a package-cwd Vitest run because such a run uses -a different config than CI does, so its green says nothing about CI. Its docstring claimed -that "every per-package `vitest.config.ts` re-exports the root config, and a package -without one resolves upward to it, so no package-level path skips this file". Measured, by -running `pnpm exec vitest run` from every directory that carries a config: 8 package -configs import the root config and were refused, 2 packages carry no config and resolve -upward and were refused, and **11 standalone configs never mention the root file at all** -(`plugin-calendar`, `-charts`, `-detail`, `-form`, `-gantt`, `-grid`, `-kanban`, `-list`, -`-map`, `-timeline`, `-view`) — nothing imported the guard from there, so it never ran. -From `packages/plugin-grid`, one such run printed `Test Files 1 passed (1)` / -`Tests 5 passed (5)` and exited 0, under a config carrying no `@object-ui/*` alias table -at all where the root config maps ~40 specifiers at sibling `src/`. The guard's hole sat -exactly where the divergence — and therefore the false-green risk — was largest. - -Those 11 configs now call the guard themselves, through a new -`repoRootFrom(import.meta.url)` landmark search rather than a hand-counted `../..` (which -resolves to a real directory when the count is wrong, so the guard would keep issuing -verdicts computed against the wrong root). The docstring and the root config's call-site -comment now describe the three routes a config can take instead of asserting one of them, -and the claim is enforced rather than restated: the guard's own test walks every -`vitest.config.*` in the repo and fails on any that takes neither route. - -No package `src/` is touched and the configs' test semantics are unchanged, so no -`@object-ui/*` package changes behaviour and there is nothing here for a consumer to -upgrade to. diff --git a/.changeset/widget-config-panel-locale-map-5301.md b/.changeset/widget-config-panel-locale-map-5301.md deleted file mode 100644 index 511b33d16..000000000 --- a/.changeset/widget-config-panel-locale-map-5301.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/i18n': minor ---- - -`WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. - -The dashboard widget config panel carried a private `resolveLabel` documented as -resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference -form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline -per-locale map `I18nLabelSchema` actually admits has neither limb, so -`{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy -of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, -`MetricWidget` and `MetricCard`. - -This was not a display bug. The resolved value seeds the panel's editable draft, -so a widget whose stored title was a map opened with an **empty** Title field and -the next save wrote `''` over the author's map — on the ordinary path, not an -exotic one: open the widget, change anything, save. - -Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: - -- **Reading** goes through `pickLocalized(value, language)`, so the panel shows - the active locale like every sibling surface post-objectui#4032. -- **Writing** replaces only the active locale's entry and carries every other - locale across. A title the author never touched round-trips the stored object - itself through an unrelated config edit; an edited one merges into the entry - that was displayed. The live-update callback (`onFieldChange`) forwards the - merged map for the same reason — hosts feed it back into the widget the panel - re-opens from, so a bare string there dropped the map before a save ever ran. - -`@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side -inverse of `pickLocalized`, so the rule is stated once instead of re-derived per -panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, -region-qualified sibling — and deliberately stops there: the `default` / `en` / -first-value limbs are display fallbacks that hand back *another* locale's string, -and writing to one would let an author editing in `fr` overwrite English. With no -entry for the active locale the edit adds one. The pairing -`pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a -write that lands where the read does not look is how a "saved" string disappears. - -A full multi-locale editing UI remains out of scope (objectui#4163). diff --git a/.changeset/zod-base-schema-mirror-parity-4605.md b/.changeset/zod-base-schema-mirror-parity-4605.md deleted file mode 100644 index 1d0ba8c9f..000000000 --- a/.changeset/zod-base-schema-mirror-parity-4605.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/types': minor ---- - -The zod `BaseSchema` mirror now accepts everything its TypeScript declaration -declares — five keys had drifted narrower (objectui#4605). - -`@object-ui/types/zod` is a published runtime validator hand-written to mirror the -`BaseSchema` interface. As the interface widened, the mirror did not, so five keys -refused at parse time a spelling the published types invite and the renderer -implements — "declared = enforced" inverted. `.passthrough()` rescued none of them: -passthrough admits UNDECLARED keys, and all five are explicitly declared, so the -narrow declaration won. - -Measured against the unmodified mirror before the change, these were the refusals: - -| key | authored input | old mirror said | -|---|---|---| -| `visible` | `'${data.status === "open"}'` | `expected boolean, received string` | -| `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` | -| `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` | -| `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` | -| `description` | `{ en: 'The record owner' }` | `expected string, received object` | - -`visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts, -no wider. `ariaLabel` takes the KEYED reference through a new exported -`KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema` -BY REFERENCE, so a change to the spec's label contract is picked up rather than -re-typed. Every spelling that parsed before still parses. - -The two i18n vocabularies are kept apart rather than merged into "some object". -`label`/`description` are the spec's INLINE locale map (resolved by -`resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by -`resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render -an EMPTY aria-label). Widening both slots to accept either shape would have -reproduced objectui#4167's confusability hazard inside the validator that exists to -catch it, so each slot admits only its own vocabulary and both cross pairings are -pinned as rejections. - -The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own -`.shape` and compares each key against the declaration, so the next widening of -`base.ts` that forgets this file turns it red with no list to maintain. It reads -`.shape` and not `keyof z.input<…>` because that spelling was measured vacuous — -`.passthrough()` collapses the inferred key union to bare `string`, and a pin written -over it resolved `never` while five keys were demonstrably narrow. Two guards pin the -derivation against both degenerations (`never` and `string`). diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md index 45a066827..3247a938f 100644 --- a/QUICK_REFERENCE.md +++ b/QUICK_REFERENCE.md @@ -120,7 +120,7 @@ release path runs the sync itself: `changeset:version` bumps every manifest and this block in the same commit, so a release can no longer leave the block a version behind (objectui#5394 — that had happened once per release, three times). -- **Version:** 17.6.0 (the version every `@object-ui/*` manifest carries — they are one +- **Version:** 17.7.0 (the version every `@object-ui/*` manifest carries — they are one `fixed` group in `.changeset/config.json`, so a release moves all of them together) - **Spec:** `@objectstack/spec` ^17.0.0 (declared by the root `package.json` and by `apps/console/package.json`) diff --git a/apps/console/CHANGELOG.md b/apps/console/CHANGELOG.md index 8ef1818ba..f160c2f64 100644 --- a/apps/console/CHANGELOG.md +++ b/apps/console/CHANGELOG.md @@ -1,5 +1,523 @@ # @object-ui/console +## 17.7.0 + +### Patch Changes + +- 9b9af8d: The Approvals Inbox stops remounting every row on every render. + + `ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions` + inside its own component body. React identifies a component by the identity of + its function, so each render produced three brand-new component *types* and React + unmounted and remounted every row's subtree instead of updating it — and the page + holds its clock in state and ticks it every 60s, so this fired on a timer whether + or not anyone was touching the page (objectui#5348). + + Two consequences were reproduced against `origin/main` before the fix, in + `apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`: + + - **Transient subtree state is discarded.** Focus placed on a row's Approve + button moved to `` on the next clock tick. + - **Input is silently swallowed.** A pointer sequence that spans a re-render — + press, tick, release — left the confirmation dialog unopened: the captured node + had been replaced, so React's delegated listener never saw the click. This is + the failure objectui#5211 hit and worked around at its call site + (`Unable to find role="alertdialog"`). + + The three cells are now at module scope beside `StatusBadge`, which was moved + there for the same reason and already carries the explanation. Everything they + closed over is passed in: `RequestCell` and `InlineActions` take the page's + scoped translator, and `RecordCell` takes `href: string | null` — one prop rather + than two, so the objectui#5211 readable/unreadable decision and the URL cannot be + handed in disagreeing with each other. + + The verification asserts the consequence, not the placement. A test that checks + the three functions now sit at module scope stays green for a refactor that moves + them and introduces a fourth inline component beside them; these cases compare + DOM-node identity for all three cells across a clock tick, which no remount can + pass, and re-drive the swallowed click. + + That guard is load-bearing because lint cannot supply one here. + `react-hooks/static-components` exists for exactly this class and is `error` in + this repo via the plugin's recommended set, yet it reports nothing on this page: + measured on `origin/main`, an arrow-form inner component injected into + `ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same + shape in a ten-line file produced two. The rule's analysis bails out on this + component, which is how three of them shipped. +- 7c0e417: The Approvals Inbox no longer shows a business approver the submitted record's raw + row JSON. + + The detail drawer's "Raw data (JSON)" panel rendered on `payload != null` alone — no + principal check of any kind — so every approver could expand (and one-click copy) the + complete raw snapshot: `id`, `created_by`, `updated_by`, `owner_id`, + `organization_id`, bare lookup ids, and **the fields the object's metadata declares + `hidden: true`**. Reported from a live EHR deployment on 17.1.0 + (objectstack-ai/objectstack#10734), where that declaration is a patient-data control. + The app author had no legitimate lever to remove the panel — field `hidden`, view + columns, app navigation, permission sets and env vars are all ineffective against it — + so the remedies available in the field were patching the shipped bundle or injecting + CSS. + + The panel is now gated on `holdsStudioAccess`, reused verbatim from the console's + `studioEntry` module: `studio.access` is a declared platform-scope capability that a + tenant org owner does not hold by design, and it already reaches the browser in + `systemPermissions[]` from `/api/v1/auth/me/permissions`. Nothing new is served, + computed or made authorable — no new config key, no new i18n copy, and the panel is + byte-for-byte unchanged for the platform operator it was written for. A business + approver keeps the structured record summary, the approval chain, the activity feed + and the decision actions; only the raw snapshot is gone. + + The gate reads the RAW `systemPermissions` signal and fails **CLOSED**, inverted from + `usePermissions().hasCapabilities`. That hook fails open on purpose — hiding a + holder's button while the server still refuses the write is the worse outcome for an + action. This panel has the opposite stake, since the measured defect is a non-holder + seeing it, so every not-a-reported-grant answer denies: no provider mounted, a backend + predating ADR-0066 that omits the field, the resolver's `catch` path that answers `200` + with no `systemPermissions` at all, and a reported empty array. A deployment whose + permission layer just failed must not be the one that leaks the snapshot. + + `ApprovalsInboxPage.rawPayloadGate.test.tsx` pins all four verdicts. Because the + acceptance condition is that something does *not* render — which an empty render + reproduces perfectly — every denial case also asserts the drawer it denies inside, and + the `studio.access` case drives the same fixture through the same helper and finds the + panel. `created_by` and `organization_id` are the witnesses: both are in the page's + `PAYLOAD_SYSTEM_KEYS`, so the summary card already drops them and their values can + reach the DOM only through the raw panel. Ablating the gate (restoring the bare + `payload != null` condition) turns the three denial cases red on exactly that + assertion and leaves the holder case green. + + Out of scope, tracked separately: trimming the summary by object metadata, and the + server-side residual that sends the unfiltered snapshot to the client at all. +- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop + clipping their tail steps. + + Both occurrences were a single non-wrapping flex row whose steps were each + `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable + items, so the bar's intrinsic width grew without bound with step count and + label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK + step names measured **1070px inside a 527px container** (objectui#5554). + + The two hosts failed differently, and neither failure was recoverable by the + reader: + + - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not + scrollable, so the nearest scroller was the drawer *panel*. Reaching steps + 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the + record card, the activity timeline and the action buttons off-screen and left + a near-blank panel. + - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one + carried `overflow-x-auto`, so it scrolled itself rather than its container. + Better, but the tail steps still sat behind a scroll gesture with no visible + affordance. + + In both, readers took the clipped bar for the end of the data; the reporting + customer acceptance tester said so verbatim. Widening the window does not help: + the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. + + Both now render as a column: one row per step, a badge-and-rail gutter, and a + label that may wrap. Width is capped by the container at every step count and + every label length, which also suits both hosts' tall-and-narrow aspect. The + rail segment below each step keeps the tint rule the horizontal connector used + — it is coloured by the step it leads *into*. + + **Always vertical, with no step-count or measured-width threshold**, because + the overflow is driven by intrinsic content width (labels x count), not by + count alone: three 16-character CJK labels already crowd a 527px drawer, so any + count threshold picks a cutoff that is wrong for some real flow, and a measured + one reintroduces a viewport-dependent branch. The card's requirement is a fix + that cannot break at an untested viewport or flow length, and a layout with no + breakpoint and no measurement is the form that satisfies it. Horizontal-with- + scroll was ruled out for both occurrences: it leaves steps behind a gesture. + + Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and + `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is + green against the broken code too — every step was always in the DOM, and the + clipping was layout — so the suites assert the property the defect names + instead: no row is `shrink-0`, every label is `min-w-0` and none is + `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no + axis, overflow or width-pinning class carries a breakpoint prefix (so there is + no viewport with untested behaviour). The reported failing regime is exercised + directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins + that the layout classes are byte-identical across all four, so no count + threshold can put some other flow length back on the old path. + + The two steppers are kept identical by hand rather than extracted to a shared + component: they live in different packages, and deduplicating them is a + refactor with its own surface. Filed separately. +- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) + + Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no + deprecation window, and the protocol-17 session face emits no `roles` key at + all. Three client sites still read it: + + - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's + Approvals tab and Home's To-do card. It read nothing else, so it sent **no + `role:` identity at all**: an approval addressed to a position rather than to + a person matched nothing and vanished from all three surfaces, silently. + - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the + Approve/Reject enablement. It also splits the scalar `user.role`, so it + degraded rather than dying: it still yielded `role:user` while dropping every + business position name (`manager`, `finance_approver`, …). + - **`AppContent`'s expression user** — forwarded a `roles` key that was always + `undefined` into every CEL predicate context. Removed; `positions` and + `isPlatformAdmin` were already forwarded correctly beside it. + + The retired spelling is **not** kept as a fallback — pairing the two is what + ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the + declaration. + + `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately + untouched: it is a semantics decision, not a rename, and is deferred to a + maintainer ruling. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- 71ee495: The two form CONTAINER contracts now have ONE declaration each, derived from + `@objectstack/spec`, and the console reads them instead of its own copies. + + objectui#5542 converged the LEAF of this contract — the field spec — and left the + two containers above it untouched, because converging them was a bigger call than a + mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared + twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once + in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean + subset — these two had **already drifted, in both directions**, so neither copy was a + subset of the other and there were two live answers to "what may an author write": + + - `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; + the console declared none of them. The console's `columns` admitted the string arm + (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. + - `FormViewSpec` — the console declared `label` / `groups` / `sharing` / + `submitBehavior`; app-shell stopped at `type` plus `sections`. + + The drift is decided by asking the **contract**, not by picking a side. `columns` + does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` + with the four numeric literals, folded to a number by its own transform), so + app-shell's numbers-only declaration was rejecting metadata the platform accepts — + objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is + the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured + against the installed `@objectstack/spec` 17.0.0), because a form config is titled, + not labelled. The value that read actually finds is the VIEW's identity label, which + arrives on the `ExpandedViewItem` envelope or beside the config on a flattened + runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to + the body it unwraps, rather than smuggled onto the form contract. + + Both types are therefore **derived from the spec's own `FormSection` / `FormView` + with named narrowings** — the repo's sanctioned form for a spec-shaped local type + (`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the + two layers agree on comes from the spec and cannot fall behind it; the four positions + where this layer is deliberately narrower are each named in an `Omit` list and + restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving + it would silently re-open #5542), and `label` / `description` / `visibleWhen` / + `visibleOn` keep the shapes this repo's renderers and evaluators actually consume + rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s + `submitBehavior` union — previously hand-written under the comment "Mirrors the spec + FormView.submitBehavior union" — is now read back off the shared type, making the + mirror structural. `@object-ui/app-shell` re-exports both names from its package root + (type-only, erased at build — nothing is added to the bundle), because a type that + cannot be imported is a type that gets retyped. + + The pins are what make future drift loud, and each half is pinned on both sides. + `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the + non-narrowed half of each type against the spec's own symbol, so re-hand-writing + either declaration fails `type-check` the day the spec moves rather than years later + when someone reads two files side by side — and the console's pins read both types + back out of the **exported** `buildSections` signature rather than naming them, so a + re-inlined local copy fails even if it agrees on every key on the day it is written. + Their liveness controls are what stop them being phantom checks: the removed copies + are pinned NOT equal to the shared types (proving the `Equal` helper still + discriminates), the renderer's honoured `RenderableSection` is pinned not equal + either (so the authored-document and honoured-row types cannot be collapsed again), + and an undeclared key is still rejected (so the derivation smuggled in no index + signature or `any`). Every narrowing carries a matching negative pin, so "derived" + cannot quietly become "widened to whatever the spec says". + + Behaviour is unchanged — the runtime always accepted these keys. The vitest halves + prove it: a section spelling its column count as the string `'3'` lays out identically + to the numeric `3` on both sides, and a section carrying the keys only one side used + to declare builds the same rows. +- cebdfe7: The form-field authoring contract now has ONE declaration, and the console reads it + instead of its own copy. + + objectui#5040 was not a missing key. It was that **two hand-written descriptions of + one contract drifted**, and nothing could notice, because each was only ever checked + against itself. PR #5537 converged the two app-shell descriptions into + `views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: + `FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same + name, in a different package — so the same failure mode stayed fully available. + + Measured key by key before choosing a route, because the two honest outcomes are + "same contract, import it" and "genuinely narrower layer, rename it and pin the + subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, + every one identical in type, none console-only — and it sat in a position that + describes an **authored document**: `FormSectionSpec.fields`, read straight off the + `/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both + files even spell the same six-member `type` union and call the element type + `FormFieldSpec`). The narrow, renderer-honoured shape is a different type that + already exists in that file, `RenderableField`. So this was one contract described + twice, and the console's description was wrong about the document: legal metadata — + `visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, + and ten more keys — was undeclared there. That is #5040's own symptom, "the type + rejects the configuration the runtime accepts", which no runtime test can see. + + `@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root + (type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` + imports it and deletes the local declaration. Reachability is the load-bearing half: + a type that cannot be imported is a type that gets retyped, and retyped copies drift. + `form-spec.ts` itself is untouched. + + `FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the + field-spec type back out of the **exported** `buildSections` signature rather than + naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even + if the copy agrees on every key on the day it is written — which is exactly what did + not happen to the copy this change removes. Its liveness controls are what stop it + being a phantom check: the removed nine-key shape is pinned NOT equal to the shared + type (so the `Equal` helper is proven to still discriminate), `RenderableField` is + pinned not equal to it either (so the honoured-row and authored-document types cannot + be collapsed again), and an undeclared key is still rejected (so the import did not + smuggle in an index signature). Behaviour is unchanged: the runtime always accepted + these keys, and the vitest half proves the same rows are built. +- b63a9a3: The console's standalone form renderer now evaluates conditional field visibility. + + `apps/console/src/components/FormPage.tsx` is a **second, independent form renderer** + — its own `buildSections`, its own JSX — and it serves both the public + `/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of + the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` / + `visibleOn` *read* inside that file returned zero. So a field an author conditioned on + `record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec` + normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both + authors and honours — rendered unconditionally on both routes. Fail-open and silent: + the author saw the field always, with no diagnostic. + + objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different + chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form` + `sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx` + is on that chain at no point, and #2212's regression pin lives with the chain it fixed, + so nothing in the suite could see this copy. One contract, two implementations, each + only ever checked against itself. + + The wiring is **#2212's ruling applied verbatim** rather than a second predicate + semantics invented for this renderer, because two form renderers disagreeing about what + `visibleWhen` *means* would be a worse defect than one renderer ignoring it. The + predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`, + `evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and + `{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*` + = the stored record an edit form started from), and the fail-open-but-loud behaviour on + an unevaluable predicate are the shared ones by construction. Resolution is + canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers: + `sectionFields.ts` and app-shell's `readVisibility`. + + Two things deliberately did **not** change. A field hidden by its predicate still + submits its value — conditional visibility is a rendering rule in both renderers, and + making it a submit-payload rule would be a new contract decided once for both, not + invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain: + the second-renderer question is real, but it belongs with the #5596 convergence track, + not with a predicate that is dead today. + + `FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this* + renderer on purpose — a pin that cannot see the second copy is how the first gap + survived. With the fix reverted and the pin in place the suite reports + `11 failed | 1 passed (12)`; the one green is the control that has to be green (a field + with no predicate still renders), without which every "the field is absent" assertion + would be equally satisfied by a renderer that draws nothing at all. +- fb934fb: The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to. + + `AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the + app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same + three statically for the platform portal at `/docs` (ADR-0046 section 6), so all + of them sat in the eager graph regardless and the `import()` moved nothing — + three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build` + (objectui#5467). A static import on either side silently defeats the split for + both, and the only signal is a build warning that fails nothing. + + `App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`, + matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even + though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot, + so nothing imported `DocsIndex` dynamically, but left static it alone would keep + `DocShell`, `use-book-data` and `book-nav` eager and the portal would only + half-leave the closure. + + Measured on this branch with the `dist/eager-closure.json` gauge added by + objectui#5324, both builds exiting 0: + + | | before | after | + |---|---|---| + | `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 | + | eager closure, gzipped | 3,881,609 B | 3,870,058 B | + | eager chunks | 58 | 52 | + + Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz), + `CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B), + `componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown + folds into the entry chunk rather than dropping — which is why the entry chunk + grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by + 11,551 B. The entry stays far under that budget's 350 KB line, and the eager + closure is the number a page load actually pays. + + What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this + looked like a bigger win than it is. Three eager chunks import it statically, + and only one of them was this portal: `plugin-chatbot` reaches it directly, and + `packages/fields`' `MarkdownContent` — lazy in source — is folded into the + eagerly imported `ui-components` chunk by the `advancedChunks` group that claims + every `packages/fields` module. That is objectui#5325's mechanism, not this + card's, and it is why the saving here is 0.30% rather than 4%. +- 7493bff: `registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing. + + The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a + `Suspense` fallback — naming the same barrel the line above it imports + statically for `registerAppComponent`, and the same barrel `App.tsx` pulls + `BuilderLanding` from to render the standalone `/studio` landing full-screen. + Either reason alone makes the `import()` unable to move a module into another + chunk (objectui#5486). + + **This moves no modules and is not a bundle improvement.** `BuilderLanding` was + already in the eager graph via `App.tsx` and still is. Measured on + `dist/eager-closure.json`, both builds exiting 0: the eager closure holds the + same 52 chunks with the same names, and the only difference is 130 B gzipped + (413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback + text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure, + because nothing could. + + What it does fix is honesty. The old code told every reader the builder was + deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every + console build — the console's count of those drops from 44 to 43, with the 43 + remaining ones all belonging to the `packages/fields` barrel (objectui#5325). + A permanent warning that fails nothing is how a team learns to skim past build + warnings, and a decorative `lazy()` is how the next reader learns something + false about the chunk graph. + + The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx` + lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in + statically, and is genuinely deferred; it is untouched. Making the *builder* + genuinely lazy would mean taking `App.tsx` off the static import too, changing + how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind + the barrel — a separate measured card, not folded in here. +- cdda37a: `buildSections` now honours a FormView field's `maxLength` override instead of always + taking the object's ceiling (objectui#5595). + + The function merges a form's field overrides with the target object's field definitions, + and its own docstring states the rule: *"Field-level FormField overrides take precedence + over object defaults."* Every key in the loop is built that way — `override.label ?? + def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder` + — except one, which read `def.maxLength` unconditionally. So an author who set a tighter + per-form limit (a short public intake form over a column whose object-level ceiling is + generous) got the generous one. + + The failure was silent in the worst direction: no diagnostic, no warning, and the form + still submits, so the symptom is a value the author believed the input refused being + accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two + `maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"` + arm. + + `override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys, + so an explicitly declared `0` stays a value the author wrote rather than falling through + to the column's ceiling. This narrows only what the input allows; the object's storage + ceiling still decides at submit time, so nothing that was accepted before is now rejected + anywhere but at the keyboard. + + Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all + until objectui#5542, so no one typing a spec in this app could write the override in the + first place, and the inert merge branch was never exercised. #5542 converged that type + onto the shared app-shell declaration, which does declare the key — making the gap + expressible, and therefore findable. + + The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in + `FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than + assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered + evidence for this fix, and it is what made the gap findable in the first place, so it + keeps its place and names the honoured answer. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- 7d0143c: The Console now gates the `/studio/*` routes on the `studio.access` ENTRY + capability, not just on the backend's refusal of the writes behind them + (objectui#5519). + + `/_console/studio/` rendered the full Studio pillar builder — Data / + Automations / Interfaces / Access, with Publish and Save draft — to any + authenticated principal who typed the URL, on deployments where the Studio nav + tile is deliberately absent and every metadata write is refused. A plain tenant + user was walked through the entire "new package" form and only refused at + submit (403). The lockdown criterion for that deployment shape is two-part — UI + entry hidden AND API refused — and only the API half was met; what stood on + this side was a write-level gate where an entry-level one belongs. + + The whole `/studio` subtree now hangs off one route element that reads + `systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this + app already consumes) and admits only a principal whose LOADED set carries + `studio.access` — the capability declared as "Enter the Studio metadata-design + surfaces", which a tenant org owner does not hold by design. Everyone else is + sent to `/home` without the builder ever mounting. + + The fail direction is deliberately inverted from this app's other capability + gates: those fail OPEN on an unknown answer because their bad outcome is a + holder losing a button, whereas a route gate's bad outcome is a non-holder + seeing the builder. So the loading window renders the console splash (never the + builder), an outright fetch failure renders the retryable error splash, and a + `200` that carries no `systemPermissions` at all is refused rather than waved + through. The server-side refusals are untouched. +- Updated dependencies [0b1326d] + - @object-ui/react-runtime@17.7.0 + - @object-ui/sdui-parser@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/apps/console/package.json b/apps/console/package.json index 0d050ae94..23cb4fb0e 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/console", - "version": "17.6.0", + "version": "17.7.0", "description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.", "license": "MIT", "type": "module", diff --git a/packages/app-shell/CHANGELOG.md b/packages/app-shell/CHANGELOG.md index c3876b79a..7606fe5bc 100644 --- a/packages/app-shell/CHANGELOG.md +++ b/packages/app-shell/CHANGELOG.md @@ -1,5 +1,1166 @@ # @object-ui/app-shell — Changelog +## 17.7.0 + +### Minor Changes + +- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id. + + A record waiting on a position rendered its approver as `positi…ager` — the + engine reference `position:sales_manager`, 22 characters, past the identity + formatter's 14-character truncation arm and middle-truncated to fit its chip. The + step names beside it were human prose; the one line answering *who is holding + this record* was an internal identifier, and not even a complete one. The same + reference reached the admin-override confirm dialog un-truncated, so a paragraph + of plain governance prose ended `— position:sales_manager` (objectui#5414). + + Both surfaces now resolve the reference before rendering, in three tiers, most + authoritative first. The server's own `pending_approver_names` wins whenever it + answers, and a backend that resolves its own slate costs the record page no extra + request. Otherwise the console reads the directory row the spec's approver + binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, + for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no + adapter and no row, the machine name still prettifies into prose rather than + truncating. The raw reference stays on hover, which is where an internal + identifier belongs. + + An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` + is actionable where `positi…ager` is not, and it is the motivating rescue case + for the admin-override path. Staffing is deliberately tri-state — a + `sys_user_position` read the viewer is not permitted to make leaves the seat's + staffing UNKNOWN and says nothing, because "I could not look" is a different + claim from "nobody holds it" and only one of them is safe to print on a + governance surface. + + Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` + and `approvalsInbox.approverNameSeparator`. The separator is a translated + punctuation key rather than `Intl.ListFormat`, which was measured on this tree + joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with + no separator, reading as one person's name. + + The directory-backed kinds and their value columns are read from + `@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new + approver type is covered the day the spec publishes it. Id-valued kinds + (`user` / `team` / `department`) keep the existing middle-truncation: a row id + has no prose to recover, and that arm is objectui#3461's answer, not this card's + defect. +- c40f3b8: A screen flow's resume result reaches the user — on both outcomes (objectui#5417). + + A dogfood walkthrough reported that a refused `resume` and a successful one + "render identically: the dialog closes and the page is unchanged", leaving no + gesture that distinguishes "created" from "rejected". Re-measured against `main` + before any change, one half of that was already fixed — `interpretFlowResponse` + reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its + prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors + after the version the report was measured on. There was no interpreter bug and + no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were + real, and they are what changed: + + - **A terminal failure no longer closes the dialog.** The reason it closed is + unchanged and is not reversed: on a `FLOW_FAILED` the engine has already + consumed the suspension, so a resubmit can only reach "No suspended run" and + must not be offered. Closing was one way to withhold that dead retry and the + expensive one — the user had just typed a form they could no longer see, and + the engine's sentence names a value that left the screen with it. The dialog + now stays open with the submit affordance withdrawn: the flat footer swaps + Submit for Close, and an `object-form` step drops its Save (which also stops a + second click from duplicating the record it had already persisted). + - **The refusal has a second, non-expiring carrier.** The toast stays — it is + viewport-fixed, so it still reaches a user scrolled past a tall step's header + — and an inline destructive `Alert` (`role="alert"`) now holds the same + sentence inside the dialog, beside the values that produced it. A retryable + refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, + and its banner clears as soon as the user starts editing. + - **A successful run invalidates what the flow WROTE, not just what the user is + looking at.** Both hosts answered `onComplete` with + `notifyDataChanged({ objectName: })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- f4becce: Console Home stops offering the metadata-authoring front door on a deployment + whose own runtime config says authoring is not offered there (objectui#5521). + + The "Build an app" cover card is now withheld when + `GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed + hosted-SaaS shape that card led a plain tenant into the full authoring flow + behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry + holds zero authoring handlers — the entry was offered and the refusal arrived at + submit. The lockdown criterion for that shape is two-part, UI entry hidden AND + API refused; only the backend half was green. + + - The card is **hidden, not dimmed**, because that is the flag's own declared + meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when + false, the SPA hides the AI authoring affordances", and the serving plugin + documents "set false to force-hide the authoring UI". + - `features.marketplace` keeps the different presentation objectui#5504 gave it + — a dimmed card plus a visible localized reason. That flag means a route is + reachable; this one means force-hide. "Start with a template" is untouched: + installing a marketplace package is not AI metadata authoring and answers to + its own flags. + - No reason line is rendered in its place. `home.build.noCapability` says the + *account* lacks "Manage Metadata"; on a runtime with no authoring at all the + surface is absent for everyone, and pointing a viewer at a permission that + would not help them is the misdirection objectui#5557 is about. + - Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()` + already encodes: a runtime predating the flag, or one whose config fetch + failed, keeps the card exactly as visible as before. + + No new authorable config key, no new server surface, and no new copy — the flag + was already being served and already reaches the browser. +- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a + first-class disabled state, and the load-failure hint describes the control plane + the runtime was actually pointed at (objectui#5504). + + `apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory + default, so a stock self-hosted stack has no marketplace at all. The Console still + recommended one: Home led with "Start with a template" and "Browse App + Marketplace", and the click landed on a red **Failed to load marketplace / Not + found** card whose hint claimed this runtime "points at the public ObjectStack + cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for + exactly the deployment reading them — the operator had not left the default, and + the advice pointed back at the template that told them to set `off`. "Marketplace + disabled by configuration" is a configuration conclusion, not a load failure. + + - `isMarketplaceEnabled()` (`runtime-config`) reads the server's own + `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the + serving app's route table (objectstack#8356). It is never inferred from the shape + of a failed request: a control plane that is merely DOWN leaves the flag `true`, + so an outage still renders as an outage. Unknown fails OPEN. + - The marketplace page renders an informational "App Marketplace is turned off" + state — muted, not `destructive` — and issues no request it knows will 404. + - Home's "Start with a template" cover greys out with a visible localized reason, + and the "Browse App Marketplace" shortcut is withheld, exactly as they already + are for the `manage_metadata` capability gate. + - `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the + configured control plane) and `failedHintSameOrigin`. The "points at the public + cloud by default" sentence is gone: it was rendered unconditionally, including on + every runtime whose operator had overridden `OS_CLOUD_URL`. + + All ten locale packs carry the new keys. +- 60d452e: `ReportView` reads a report's data binding through the one key the contract declares — `dataSource.object`. + + The view accepted `resource` as a second spelling of `object`, in two places, and + named that spelling in a warning the user could read: + + ``` + :171 liveReport?.objectName || liveReport?.dataSource?.object + || liveReport?.dataSource?.resource + :273 dataFetchSource.dataSource.object || dataFetchSource.dataSource.resource + :275 console.warn('ReportView: dataSource missing object/resource property') + ``` + + `resource` is not on this binding. `ElementDataSourceConfig` declares `object`, + `view?`, `filter?`, `sort?` and `limit?`; its `@objectstack/spec` twin + `ElementDataSourceSchema` is a strict object, so an extra `resource` key is + *rejected* there rather than ignored; and the binding's own predicate + `isElementDataSourceConfig` decides on `object`. A `resource`-only binding + therefore was never a binding on any other renderer in the system — it rendered + here and silently produced nothing anywhere else, with neither end reporting a + problem. That divergence is what a consumer-side alias buys: one renderer + answering a question the contract says has no answer. + + `resource` is a real key on other surfaces — `CRUDSchema.resource`, the + `DataSource` adapter's first parameter, `LiveExportOptions.resource` — and all + three are untouched. None of them is this one. + + Behaviour, measured by rendering each input shape before and after. Only the + `resource`-only shape moves: + + | binding | before | after | + | --- | --- | --- | + | `object` only | queries that object | unchanged | + | `resource` only | queries it as if declared | not queried; named warning, no rows, fallback field list | + | both | queries `object` | unchanged | + | neither | not queried; warning | unchanged | + + So off-spec report metadata that used to render now fails loudly instead of + appearing to work. A producer census found nothing that would notice: no site in + this repository, and none in the `objectstack` framework repository, writes + `resource` onto a report `dataSource`. The limb was speculative in the commit + that introduced it, and per AGENTS.md #0.1 an off-spec spelling is corrected at + the producer, never taught a second dialect by the renderer. + + The `:275` wording now names only `object`. A diagnostic that lists a key the + contract does not declare is not a small thing: it is the system telling an + author — increasingly, an author's code generator — that the wrong spelling is + supported. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. +- 8e32da7: AI build surface gains a standing 「未发布改动」 bar (#5694): while the conversation's bound package has pending drafts, a bar floats above the composer — surviving scrolling — counting the unpublished changes and publishing them through the same governed `publish-drafts` route as the inline card button, with probe findings surfaced instead of a blind success toast. Renders nothing when the count is zero or the conversation is unbound. +- 7a90afd: Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. + + Creating an object through Studio collected exactly two things — display name and + identifier — and saved a draft that declared no `sharingModel`. The draft saved + happily, the form designer worked, and the object was then refused at 发布 → + 全部发布 by `security-owd-unset`: a required decision the surface never asked + for, delivered by failing, as English ADR prose in a toast that then vanished on + a timer. The one actionable word in it named a control three clicks away that + nothing routed to. + + The publish gate is correct and is unchanged — an org-wide default has to be an + authored decision, not an accident. What changes is when the console asks and + when it answers: + + - **The create dialog asks.** A third field collects the baseline, pre-selected + to `private` and glossed with the Settings tab's own strings, so a new object + is publishable by construction. `buildObjectSkeleton` now takes the value as a + required parameter — a future create path cannot omit the baseline without + failing to type-check. `controlled_by_parent` is deliberately not offered at + creation: it derives access from a master relation a brand-new object does not + have yet, so offering it would trade one publish refusal for another. + - **The review sheet reports it.** The pending-changes panel now runs the + framework's own `validateSecurityPosture` over the pending object drafts and + names any blocking finding, with its fix-it hint, next to the Publish button. + It mirrors the producer's rule rather than re-deriving it, and it reports + without blocking — the server door stays the authority. + - **The Settings tab stops calling an unset baseline safe.** It described unset + as "defaults to Private", which answers what the runtime does and not whether + the object can ship. It now reads as the publish-blocking problem it is, + styled like the external-wider warning beside it. + +### Patch Changes + +- 169f6d2: `AiChatPage`'s public share-link base now resolves through the one console-mount + resolver instead of a private copy of it (objectui#4482). + + The page built `publicShareBase` itself — read the injected ``, take its + pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third + independent implementation of the mount resolution `resolveConsoleUrl` centralizes. + objectui#4472 had just deleted the other two on that rule; this was the surviving + sibling. Its output was correct, so nothing a user hits was broken and nothing a user + hits changes: measured over the base-href matrix, the deleted builder and + `resolveConsoleUrl('s')` return identical URLs for every shape the console is served in + — `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable + builds, nested mounts, and no `` at all. + + The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`, + which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM + it returns `undefined` rather than a URL built from an origin that does not exist, so + `ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument — + the mount is only ever carried by the injected ``, and a resolver with no + other input cannot be pinned by a test that steers something production never reads. + + `resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected + `` element for each deployment shape, plus a structural case asserting no other + app-shell file reads the `` tag — so a fourth copy fails a test rather than + waiting for mount semantics to change under it. +- aa3b810: `features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead + of being spelled inline at two call sites (objectui#5577). + + `features.marketplace` already had a documented accessor whose docblock is where the + fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating + `/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*, + plus the "never infer this from the shape of a failure" warning. `features.aiStudio` + had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false` + and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine + had two spellings and neither reader could cite it. + + The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and + against a runtime-config snapshot whose `features` is absent that read is a TypeError + rather than a fail-open — the exact shape that crashed 29 tests across four suites in + PR #5575 before it was corrected. Measured here: no live path can currently deliver + such a snapshot to `ChatDock` (the module's singleton constructs `features` on every + write and exports no setter, and no suite mounts the dock's default body under a + partial stand-in), so this closes a reachable-by-construction crash rather than a live + one — and it closes it at the source by leaving no inline read to get wrong. + + `isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`: + neither is re-exported from `src/index.ts`, so the package's published `exports` surface + is unchanged. +- 029e2fb: The console's `` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761). + + This repository has two components named `PageHeader`. `@object-ui/layout`'s is + the renderer for the authored `page:header` / `page-header` node and converged + on `subtitle` in objectui#3789, because `subtitle` is the key + `@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s — + the console's own title row, drawn by `ObjectView` and `ObjectDataPage` — + spelled the very same concept `description` and had no `subtitle` at all. Both + rendered correctly; the defect was one concept carrying two key names one + package apart, the objectstack#4115 shape moved up a layer. An author reading + one component to learn the other was being taught a key the contract does not + have. + + **Not a breaking change, measured rather than assumed.** The convergence is a + plain rename with no alias, because this component is not on the published + surface: + + | gauge | result | + |---|---| + | exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) | + | `exports` map | declares exactly `.` and `./styles.css` | + | Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves | + | in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) | + | emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) | + + No supported specifier reaches the prop, so there was nothing to keep + compatible, and a renderer-side `description` alias would have been exactly the + second dialect AGENTS.md #0.1 forbids — the layout side had just finished + retiring one. Out-of-repo consumers cannot be enumerated from this repository; + what can be, and is, is the set of import paths through which one could have + reached this component, which is empty. + + Rendered output is unchanged: same element, same classes, same position. The + patch tier is a declaration that the tarball moved, not a claim that a consumer + must act. + + `packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the + pin the card asked for. It asserts the subtitle on the DOM a reader gets (a + `

`, in the title block, after the `

`), that `description` now draws + nothing and is rejected by the compiler, and — the assertion that actually goes + red if either side drifts again — that both packages' `PageHeaderComponentProps` + declare `subtitle`. +- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop + clipping their tail steps. + + Both occurrences were a single non-wrapping flex row whose steps were each + `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable + items, so the bar's intrinsic width grew without bound with step count and + label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK + step names measured **1070px inside a 527px container** (objectui#5554). + + The two hosts failed differently, and neither failure was recoverable by the + reader: + + - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not + scrollable, so the nearest scroller was the drawer *panel*. Reaching steps + 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the + record card, the activity timeline and the action buttons off-screen and left + a near-blank panel. + - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one + carried `overflow-x-auto`, so it scrolled itself rather than its container. + Better, but the tail steps still sat behind a scroll gesture with no visible + affordance. + + In both, readers took the clipped bar for the end of the data; the reporting + customer acceptance tester said so verbatim. Widening the window does not help: + the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. + + Both now render as a column: one row per step, a badge-and-rail gutter, and a + label that may wrap. Width is capped by the container at every step count and + every label length, which also suits both hosts' tall-and-narrow aspect. The + rail segment below each step keeps the tint rule the horizontal connector used + — it is coloured by the step it leads *into*. + + **Always vertical, with no step-count or measured-width threshold**, because + the overflow is driven by intrinsic content width (labels x count), not by + count alone: three 16-character CJK labels already crowd a 527px drawer, so any + count threshold picks a cutoff that is wrong for some real flow, and a measured + one reintroduces a viewport-dependent branch. The card's requirement is a fix + that cannot break at an untested viewport or flow length, and a layout with no + breakpoint and no measurement is the form that satisfies it. Horizontal-with- + scroll was ruled out for both occurrences: it leaves steps behind a gesture. + + Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and + `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is + green against the broken code too — every step was always in the DOM, and the + clipping was layout — so the suites assert the property the defect names + instead: no row is `shrink-0`, every label is `min-w-0` and none is + `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no + axis, overflow or width-pinning class carries a breakpoint prefix (so there is + no viewport with untested behaviour). The reported failing regime is exercised + directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins + that the layout classes are byte-identical across all four, so no count + threshold can put some other flow length back on the old path. + + The two steppers are kept identical by hand rather than extracted to a shared + component: they live in different packages, and deduplicating them is a + refactor with its own surface. Filed separately. +- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) + + Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no + deprecation window, and the protocol-17 session face emits no `roles` key at + all. Three client sites still read it: + + - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's + Approvals tab and Home's To-do card. It read nothing else, so it sent **no + `role:` identity at all**: an approval addressed to a position rather than to + a person matched nothing and vanished from all three surfaces, silently. + - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the + Approve/Reject enablement. It also splits the scalar `user.role`, so it + degraded rather than dying: it still yielded `role:user` while dropping every + business position name (`manager`, `finance_approver`, …). + - **`AppContent`'s expression user** — forwarded a `roles` key that was always + `undefined` into every CEL predicate context. Removed; `positions` and + `isPlatformAdmin` were already forwarded correctly beside it. + + The retired spelling is **not** kept as a fallback — pairing the two is what + ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the + declaration. + + `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately + untouched: it is a semantics decision, not a rename, and is deferred to a + maintainer ruling. +- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock + noticed it (objectui#5054). + + One abandoned device approval could be noticed by either of two clocks, and the + Cloud Connection panel had a different answer for each. When the panel's own + `expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — + translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered + HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` + threw a bare `Error` carrying only that sentence, and the catch rendered it + verbatim. Same user, same failure, two languages, decided by which clock got + there first — visible on a zh console as the same abandoned approval reading + Chinese or English depending on whether the tab sat open past `expires_in`. + + `getJson` now carries the envelope's `declaredCode` and `code` across its throw, + and a single closed map turns the two RFC 8628 outcomes a user can actually cause + into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, + `access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten + locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream + spelling there — `code` is `DEVICE_CODE_FAILED` for both. + + Every other code is unchanged: `invalid_grant`, and anything upstream invents + next, still render the wire `message`, which stays the single source of truth for + failures this console has no copy for. No API, export or resolver was widened. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- 71ee495: The two form CONTAINER contracts now have ONE declaration each, derived from + `@objectstack/spec`, and the console reads them instead of its own copies. + + objectui#5542 converged the LEAF of this contract — the field spec — and left the + two containers above it untouched, because converging them was a bigger call than a + mechanical import. `FormSectionSpec` and `FormViewSpec` were each hand-declared + twice under the same names, once in `packages/app-shell`'s `SchemaForm.tsx` and once + in `apps/console`'s `FormPage.tsx`. Unlike the leaf — whose console copy was a clean + subset — these two had **already drifted, in both directions**, so neither copy was a + subset of the other and there were two live answers to "what may an author write": + + - `FormSectionSpec` — app-shell declared `description` / `visibleWhen` / `visibleOn`; + the console declared none of them. The console's `columns` admitted the string arm + (`'1' | '2' | '3' | '4'`); app-shell's took numbers only. + - `FormViewSpec` — the console declared `label` / `groups` / `sharing` / + `submitBehavior`; app-shell stopped at `type` plus `sections`. + + The drift is decided by asking the **contract**, not by picking a side. `columns` + does admit the string arm (`FormSectionSchema.columns` unions `z.enum(['1','2','3','4'])` + with the four numeric literals, folded to a number by its own transform), so + app-shell's numbers-only declaration was rejecting metadata the platform accepts — + objectui#5040's own symptom, not a deliberate narrowing. `label` on the form view is + the opposite answer: `FormViewSchema` **rejects** it (`unrecognized_keys`, measured + against the installed `@objectstack/spec` 17.0.0), because a form config is titled, + not labelled. The value that read actually finds is the VIEW's identity label, which + arrives on the `ExpandedViewItem` envelope or beside the config on a flattened + runtime overlay — so it is declared on `FormPage.tsx`'s own `FormViewBody`, next to + the body it unwraps, rather than smuggled onto the form contract. + + Both types are therefore **derived from the spec's own `FormSection` / `FormView` + with named narrowings** — the repo's sanctioned form for a spec-shaped local type + (`scripts/check-spec-symbol-derivation.mjs`) — rather than restated. Every key the + two layers agree on comes from the spec and cannot fall behind it; the four positions + where this layer is deliberately narrower are each named in an `Omit` list and + restated once next to its reason: `fields` keeps the converged 26-key leaf (deriving + it would silently re-open #5542), and `label` / `description` / `visibleWhen` / + `visibleOn` keep the shapes this repo's renderers and evaluators actually consume + rather than the spec's `I18nLabel` and `ExpressionInput`. `apps/console`'s + `submitBehavior` union — previously hand-written under the comment "Mirrors the spec + FormView.submitBehavior union" — is now read back off the shared type, making the + mirror structural. `@object-ui/app-shell` re-exports both names from its package root + (type-only, erased at build — nothing is added to the bundle), because a type that + cannot be imported is a type that gets retyped. + + The pins are what make future drift loud, and each half is pinned on both sides. + `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts` compare the + non-narrowed half of each type against the spec's own symbol, so re-hand-writing + either declaration fails `type-check` the day the spec moves rather than years later + when someone reads two files side by side — and the console's pins read both types + back out of the **exported** `buildSections` signature rather than naming them, so a + re-inlined local copy fails even if it agrees on every key on the day it is written. + Their liveness controls are what stop them being phantom checks: the removed copies + are pinned NOT equal to the shared types (proving the `Equal` helper still + discriminates), the renderer's honoured `RenderableSection` is pinned not equal + either (so the authored-document and honoured-row types cannot be collapsed again), + and an undeclared key is still rejected (so the derivation smuggled in no index + signature or `any`). Every narrowing carries a matching negative pin, so "derived" + cannot quietly become "widened to whatever the spec says". + + Behaviour is unchanged — the runtime always accepted these keys. The vitest halves + prove it: a section spelling its column count as the string `'3'` lays out identically + to the numeric `3` on both sides, and a section carrying the keys only one side used + to declare builds the same rows. +- cebdfe7: The form-field authoring contract now has ONE declaration, and the console reads it + instead of its own copy. + + objectui#5040 was not a missing key. It was that **two hand-written descriptions of + one contract drifted**, and nothing could notice, because each was only ever checked + against itself. PR #5537 converged the two app-shell descriptions into + `views/metadata-admin/form-spec.ts`. A **third** survived in `apps/console`: + `FormPage.tsx` declared its own nine-key `interface FormFieldSpec`, under the same + name, in a different package — so the same failure mode stayed fully available. + + Measured key by key before choosing a route, because the two honest outcomes are + "same contract, import it" and "genuinely narrower layer, rename it and pin the + subset". The console's copy was a strict subset — 9 of the shared type's 26 keys, + every one identical in type, none console-only — and it sat in a position that + describes an **authored document**: `FormSectionSpec.fields`, read straight off the + `/meta/view/:name` payload, the same spec `FormView` metadata-admin renders (both + files even spell the same six-member `type` union and call the element type + `FormFieldSpec`). The narrow, renderer-honoured shape is a different type that + already exists in that file, `RenderableField`. So this was one contract described + twice, and the console's description was wrong about the document: legal metadata — + `visibleWhen`, `dependsOn`, `type`, `options`, `immutable`, the recursive `fields`, + and ten more keys — was undeclared there. That is #5040's own symptom, "the type + rejects the configuration the runtime accepts", which no runtime test can see. + + `@object-ui/app-shell` therefore re-exports `FormFieldSpec` from its package root + (type-only, erased at build — nothing is added to the bundle), and `FormPage.tsx` + imports it and deletes the local declaration. Reachability is the load-bearing half: + a type that cannot be imported is a type that gets retyped, and retyped copies drift. + `form-spec.ts` itself is untouched. + + `FormPage.fieldSpec.test.ts` is the pin that makes future drift loud. It reads the + field-spec type back out of the **exported** `buildSections` signature rather than + naming it, so re-inlining a local `interface FormFieldSpec` fails `type-check` even + if the copy agrees on every key on the day it is written — which is exactly what did + not happen to the copy this change removes. Its liveness controls are what stop it + being a phantom check: the removed nine-key shape is pinned NOT equal to the shared + type (so the `Equal` helper is proven to still discriminate), `RenderableField` is + pinned not equal to it either (so the honoured-row and authored-document types cannot + be collapsed again), and an undeclared key is still rejected (so the import did not + smuggle in an index signature). Behaviour is unchanged: the runtime always accepted + these keys, and the vitest half proves the same rows are built. +- 4db5989: A widget title stored as an inline per-locale map is editable again in both dashboard + authoring surfaces, and a save writes back only the active locale's entry + (objectui#5428). + + `@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at + 17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both + authoring panels edit a title in ONE single-line input. Writing the input's value back + as the whole value would collapse every other locale on the first keystroke, so both + surfaces took the same conservative branch: show a map-valued title resolved, and make + it READ-ONLY. + + That branch could not lose data, but it rested on a premise the spec had already + invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was + plain `string` through rc.5" — stated sixty lines below a comment in the same file + documenting the rc.6 widening that makes a stored map reachable. Both could not hold. + The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward + was not protect an unreachable path: it denied an author the ability to edit a widget + title in their own locale. + + objectui#5301's maintainer ruling settled the write rule for the sibling surface — a + save replaces only the active locale's entry and preserves the others — and + `@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because + the read and the write have to agree. Both panels now adopt it: + + - `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; + - `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. + + A plain-string title keeps saving as a plain string, so the common path is unchanged. + An edit made in a locale the stored map does not carry ADDS an entry under that locale + rather than overwriting the entry the display fell back to. + + The pins are preservation pins, not "the input is editable" pins: at both surfaces a + keystroke on a map-valued title must leave every other locale's entry byte-identical. + Reverse-verified by mutating each write back to the flattening form and confirming those + assertions go red at both surfaces. + + Not a multi-locale editor: an author still reaches only the entry for the locale they + are in. Authoring every locale from one panel remains an open product question. The + stale deferrals both comments carried pointed at objectui#4163, which closed as + completed on 2026-08-15 while the placeholders were still in the tree; they are replaced + with the rule that is actually in force rather than re-pointed at another tracker. +- ca2b409: Studio's form designer canvas now emits the namespaced `FormField.type` spelling + instead of passing `objectDef.fields[x].type` through raw (objectui#4838). + + Per the maintainer ruling on that card, a bare spec-type name (`markdown`, + `html`, `richtext`, …) is **not** a legal `FormField.type` — one widget, one + legal spelling, the namespaced widget id. `ObjectFormDesigner` was the measured + producer of the bare spelling: it handed a raw object-metadata type straight to + `isWideFieldType`, a helper whose vocabulary is `FormField.type`. It now + normalizes through `mapFieldTypeToFormType`, the one place that widget decision + is made. + + User-visible effect: a `repeater` field is finally laid out full-row on the + canvas, matching the runtime form. `repeater` is a spec `FieldType` that + resolves to the wide `field:grid` widget, but bare `repeater` is not one of + `WIDE_FIELD_TYPES`' bare members, so the raw pass matched nothing — the canvas + showed it at normal width while the real form spanned it. Fields whose spec name + doubles as a widget id (`textarea`, `markdown`, `html`, `richtext`, `grid`) are + unaffected; they matched under both spellings. + + The two tolerant consumers this makes look redundant are deliberately left + alone, each scheduled under its own follow-up with deprecation care: the + `field:`-prefix fallback in `renderFieldComponent`, and `WIDE_FIELD_TYPES`' dual + spellings. Removing a tolerance is the consumer-tightening half, and other + producers have not been normalized yet. +- eba3a6e: metadata-admin: `FormFieldSpec` declares `dependsOn`, and the widget half reads + the same declaration instead of its own copy of it (objectui#5040) + + `FormFieldSpec` — the authoring type for a metadata-admin form layout, the + element type of `FormSectionSpec.fields[]` — did not declare `dependsOn`. + `widgets.tsx` held a second, inline description of the same object as + `WidgetProps.fieldSpec`, and that one did, because two registered widgets read + it as their primary configuration: `field-selector` resolves + `dependsOn || reference || 'objectName'` to decide whose field catalog to + offer, and `dynamic-config` uses it to pick a sub-schema out of + `WidgetContext.dynamicSchemas`. One value travelling down one channel, + described twice, disagreeing on the one key that decides what those widgets + show — so + + ```ts + { field: 'fields', widget: 'field-selector', dependsOn: 'objectName' } + ``` + + the only configuration that makes `field-selector` work, was a `TS2353` for + anyone who typed their spec. It survived because in-repo specs reach the form + through `as any` / loose types, so the authoring type was never asked. + + No runtime behaviour changes: `MetadataField` already handed `dependsOn` + through and both widgets already read it. What changes is the type face — it + now admits what the runtime has always accepted. The two descriptions are one + declaration, extracted to a leaf module + (`views/metadata-admin/form-spec.ts`) that both halves import, because + `SchemaForm.tsx` imports `./widgets.js` and a back-edge would close a cycle. + `SchemaForm.tsx` re-exports `FormFieldSpec` and `VisibilityPredicate`, so every + existing importer is unaffected. + + `dependsOn` is `string | string[]` here, deliberately **not** + `@object-ui/types`' wider canonical `DependsOnInput`, which also admits + `{ field, param }` objects: both readers index `[0]` and use the result as a + field name, so the wider shape would be a type that lies. Converging the two is + its own decision, pinned as a refusal rather than taken silently. +- d524bde: The marketplace **catalog** page now tells a non-admin that the runtime has no + marketplace, instead of telling them they lack permission (objectui#5557). + + `MarketplacePage` ordered its two early returns admin-first, so on a runtime that + mounts no marketplace at all (`features.marketplace: false` — an `OS_CLOUD_URL=off` + deployment, the EE deploy template's factory default) an unprivileged member got + "access denied" for a surface that exists for nobody. That answer sends them to + ask an administrator for a grant that would not help them, and it left the + informational disabled state built in objectui#5504 unreachable for every + non-admin. The runtime check now answers first, because "this deployment has no + marketplace" is true regardless of who is asking. + + This restores the sibling-page invariant for the one class of viewer it still + failed for: `MarketplacePackagePage` was reordered the same way in objectui#5533, + so on a marketplace-off runtime the catalog page and the package detail page now + give a non-admin the same kind of answer. + + Scope, deliberately narrow: + + - **Admin-first ordering stays correct where a marketplace exists.** On a runtime + with `features.marketplace: true`, a non-admin still gets `MarketplaceAccessDenied` + — the catalog is an install surface, and a member who cannot install has nothing + to do with it. That boundary is pinned by an explicit test, not left to prose: + without it, a change that simply dropped the admin check would look correct. + - Nothing an admin sees changes, on either kind of runtime. + - No new i18n keys, and no change to `MarketplaceAccessDenied` or + `MarketplaceDisabled` themselves — only which of the two the page reaches for, + and in which order it decides. + - The disabled state is still the server's own answer (`features.marketplace`), + never inferred from a failed request, and it still fails open. +- 13413f3: The marketplace package detail page decides "you are not an admin" before it fetches, + instead of after the load has already failed. + + `MarketplacePackagePage` ordered its early returns with the `!isAdmin` guard *after* + both the loading branch and the `error || !data` branch, and gated its two fetch + effects on `features.marketplace` alone. On a runtime that mounts a marketplace, a + non-admin who opened a package URL was therefore walked through the fetch and the + skeleton, and — when the load failed — was handed the destructive "Failed to load + package" card carrying the server's own error message. Whether that viewer was + refused or handed a diagnosis about a surface they are not allowed to use came down + to whether an unrelated request happened to succeed. + + The guard now sits ahead of both branches, and `getMarketplacePackage` and + `getCloudInstallationInfo` are gated on `isAdmin` as well, so the page stops issuing + requests on behalf of a viewer it has already decided to turn away. That is the + discipline objectui#5533 established on this same page for `features.marketplace`, + applied to the other predicate that decides the same thing. It is also the ordering + `MarketplacePage` carries after objectui#5557, so the two sibling pages now answer one + runtime the same way for every viewer. The server remains the authority on what a + non-admin may fetch; this only stops the client doing work it would discard. + + Unchanged for an admin, deliberately and under test: a failing load still produces the + destructive card with the server's message intact, and a successful one still renders + the package. A "fix" that hoisted the refusal unconditionally, or that deleted the + failure branch, would satisfy every non-admin assertion and fail those two. + + `loading` stays seeded from `marketplaceEnabled` alone rather than from + `marketplaceEnabled && isAdmin`. `isAdmin` reads `activeMember`, which `AuthProvider` + resolves asynchronously *after* the session settles, so an admin whose role comes from + the org member row renders once as a non-admin before the flag flips. Seeding `false` + there would leave that first admin render with `loading: false` and no data — the + destructive card, painted for a frame before the effect could raise the flag again. + `MarketplacePackagePage.guardOrder.test.tsx` pins the flip case for that reason, along + with the ordering, the skipped requests, and the marketplace-off boundary the guard + must not jump above. +- 2573ff4: The marketplace **package detail** page now states that the marketplace is turned + off, instead of red-erroring, on a runtime that has none (objectui#5533). + + `MarketplacePackagePage` takes the same `isMarketplaceEnabled()` early return its + sibling catalog page took in objectui#5504, rendering the informational + `MarketplaceDisabled` state when the server reports `features.marketplace: false` + (an `OS_CLOUD_URL=off` deployment — the EE template's factory default). Until now + the same runtime answered a bookmarked or pasted package URL with a destructive + "Failed to load package / Not found." card, so the two sibling pages reached + opposite conclusions about one runtime: the catalog called it configuration, the + detail page called it a failure. + + Both requests the page fires for its own view are skipped in that state — the + package fetch and the cloud-installation probe — rather than fired and discarded: + a discarded request still reaches the server and can race the destructive card + onto the screen before the disabled state settles. + + Scope, deliberately narrow: + + - **Not** a "swallow all errors" change. With `features.marketplace: true` the page + behaves exactly as before, and a package that genuinely is not there still + renders the destructive card carrying the server's own message. The flag is the + runtime's own answer, never inferred from the shape of a failure, and it fails + open — a runtime that answers nothing keeps its detail page. + - The `installLocal` surfaces are untouched. That is a different capability flag, + and an air-gapped `OS_CLOUD_URL=off` runtime still has a working install-local + path. + - No new i18n keys: `marketplace.disabled.*` and `marketplace.action.backHome` + already ship in all ten locale packs. +- 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no + title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three + times (objectui#5024). + + `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four + states out by hand, 42 lines apart in one file, compared by no gate. They are now + one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s + `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies + were restating a schema rather than filling a gap. + + The user-visible half is the banner. Its title was three independent `&&` branches + with no fallback, while the switch that opens the banner is true for any non-`none` + value — so a lock state outside the four opened the box and left the headline + empty. That is reachable without a fifth state ever being added here: + `MetadataClient.layered()` casts the wire value through unchecked, so a newer + server reaches this banner as-is. Measured, not assumed — feeding `no-publish` + through the page rendered the padlock, the border and an empty title. The title is + now a keyed lookup with a loud fallback that names the unrecognised token, so a + fifth state fails `type-check` here and, if one arrives from a server anyway, the + operator reads a sentence instead of a blank box. +- 26ff2d6: `sanitizeViewOverride` now strips a half-filled `between` from a stored view + overlay, instead of handing it back to the merge (objectui#5025, + objectstack#8815). + + The overlay recovery pass asked "is this filter row filled in?" with the + shape-blind predicate objectstack#8815 retired — `value == null || value === '' + || (Array.isArray(value) && value.length === 0)`. That is correct for `scalar` + and `list` and blind to `pair`: a `between` carrying one bound is + `['2024-01-01', '']`, an array of length 2, so the pass read it as a real + condition and kept it. The two write paths (`plugin-list`'s `ListView` and + app-shell's `viewFilterFold`) were converted to the builder's arity-aware + `isFilterValueComplete`; this read path was the third verbatim copy, and the one + whose job was to clean up exactly the rows the other two used to write. Until + now a stored half-range survived the pass, reached the query, and the server + refused the whole view (`400 INVALID_FILTER`) for every user on every later + read. + + The pass now delegates to the same `isFilterValueComplete` the write paths use, + on both of the at-rest shapes it handles (the spec `ViewFilterRule` object and + the legacy runtime triple). A complete range — bounds of `0` and `false` + included — is untouched, and the `scalar` and `list` families read exactly as + before, since the retired predicate was already right for them. One shape is + newly stripped beyond the half-filled array: a `between` whose value is a bare + scalar, which `ViewFilterRuleSchema` itself refuses. + + Which operators want no value at all is unchanged and still answered by + app-shell's `VALUELESS_FILTER_OPERATORS` — that set is already derived from the + builder's `VALUELESS_FILTER_BUILDER_OPERATORS` rather than being a private copy, + and its parity is pinned. +- 3211397: `ObjectSettingsPanel` now calls `isExternalWider` from `owd-sharing.ts` instead of + re-declaring `OWD_WIDTH` and the ADR-0090 D11 width comparison inline (objectui#5477). + + `owd-sharing.ts` states its own purpose — it is "the single home" for the pieces the + per-object Settings tab and the package-level OWD overview must agree on, the D11 + "external ≤ internal" comparison among them — and the Settings tab was the one surface + not calling it. The two implementations were verified equivalent before the swap, over + the full domain of both dials plus `undefined`, the rejected legacy aliases and + prototype-chain keys: 144 input pairs, zero disagreements. The module's extra `!!` + truthiness guards are redundant at this call site, which already normalizes both dials + to `''`, and `'' in OWD_WIDTH` is false regardless. So no author-visible verdict + changes — the same three pairs warn, and the same twenty-two stay calm. + + The substantive half is the pin that keeps it that way. `ObjectSettingsPanel.owdAgreement.test.tsx` + drives BOTH surfaces over the full 5×5 cross-product of the values their dials offer and + requires all three legs — the Settings tab's rendered warning, the overview's per-row + error, and `isExternalWider` itself — to agree on every pair, with the violating pairs + pinned by name so the sweep cannot pass vacuously. Re-inlining a drifted copy into either + surface, swapping the `(internal, external)` argument order, or refining D11 in only one + place now fails a test instead of silently leaving the authoring surface enforcing the + old rule. +- 76ceb1e: The console's param-collection dialog now titles itself from `action.label` alone — + the unreachable `|| action?.title` fallback beside it is removed (objectui#4282). + + `title` is declared on no action surface in the ecosystem: it is absent from + `@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from + `@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` + inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` + `ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, + `action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand + side of that `||` could not be reached by authored metadata: a fallback that cannot + fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. + Nothing a user hits changes; the line now reads exactly one key, matching the + `description` line directly below it. + + `useConsoleActionRuntime.paramDialogTitle.test.tsx` pins the reader so the alias cannot + be reinstated silently: an action carrying `title` and no `label` must open an untitled + dialog rather than a dialog named by a key no producer sets. +- 7e81168: `ActionParamDialog`'s "this param carries a reference target" rule now derives from the shared reference-field family instead of the last private copy of it. + + `packages/app-shell/src/utils/paramToField.ts` restated the rule inline as + `LOOKUP_WIDGET_TYPES.has(type) || type === 'user'` — the fourth and last + hand-maintained answer to one question ("does this widget resolve a foreign key, + so hand it `reference_to` / `display_field` / the rest of the picker config"). + The other three converged on `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` in + objectui#4770 / #4790 / #4815; this face is now the fourth. + + No reachable behaviour change. The shared set is one member wider (`tree`), and + that member can never be a widget key on this surface: it is absent from + `fields`' widget map and `mapFieldTypeToFormType` sends it to `field:lookup`, so + every key the rule tests arrives as `lookup`. Both halves are pinned, so + registering a real `tree` widget surfaces the change instead of shipping it + silently. + + The module's second rule — which widget keys degrade to a text input for want of + a declared `referenceTo` — is a different set over overlapping types (`user` + defaults its target to `sys_user` and must never degrade) and was deliberately + left un-merged, matching the same split the plugin-grid twin keeps. + + Also retires a comment that claimed the disjunction "moves in lockstep with + plugin-grid's `bulkParamToField` twin — the two param faces are never split". + Measured on the tip before this change, that was false in both senses: the twin + had read core's Set since objectui#4815 while this line read a private literal, + so the two shared nothing and no gate could report a split; and the two member + sets already differed, by `tree`. Lockstep now holds mechanically — the pin is + on object identity (a spy on core's `has`), so a member-identical private copy + fails where a value check would pass. +- c5200f0: Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. +- 76ceb1e: `RecordDetailView`'s param-collection dialog now titles itself from `action.label` + alone — the unreachable `|| action?.title` fallback beside it is removed + (objectui#5610). + + This was the second copy of the limb objectui#4282 removed from + `useConsoleActionRuntime`. `RecordDetailView` builds its own action runtime rather + than routing through that hook, so the two near-identical `paramCollectionHandler`s + have drifted as a pair and the first fix could not reach this one. + + `title` is declared on no action surface in the ecosystem: it is absent from + `@objectstack/spec`'s `ActionSchema` (44 keys walked at spec 17.0.0), from + `@object-ui/core`'s `ActionDef` and its pinned `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` + inventories, and from `@object-ui/types`' renderer view (`ui-action.ts`) and `crud.ts` + `ActionSchema` / `BaseSchema`. None of the four action renderers — `action:button`, + `action:icon`, `action:group`, `action:menu` — forwards it either. So the right-hand + side of that `||` could not be reached by authored metadata: a fallback that cannot + fire, which is the "declared is not enforced" shape objectstack#4075 exists to reduce. + Nothing a user hits changes; the line now reads exactly one key, matching the + `description` line directly below it. + + `RecordDetailView.paramDialogTitle.test.tsx` pins the reader so the alias cannot be + reinstated silently: an action carrying `title` and no `label` must open an untitled + dialog rather than a dialog named by a key no producer sets. A pin per reader is the + only shape that covers both handlers, since the hook's own pin cannot see this site. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- add10d8: Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button. + + Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on + the first surfaces a new author sees. + + **Validation no longer runs on mount.** `新建软件包` opened with both required + fields already red: the create draft is `{ version, type }`, so + `ManifestSchema.safeParse` reported `name` and `id` missing on the very first + render, and the dialog then jumped a line height per field as each error + cleared. `SchemaForm` now defers the error *line* until the row has been + touched — first focusout anywhere in it, or the field's own first edit — for + create forms only. The rule itself did not move: `issues` still reaches the host + unchanged, so the submit button is gated on exactly the same validation, and + edit/view forms still report from mount, where the issues describe stored values + rather than something half-typed. + + **The package form's help text is translated.** The labels came from the + metadata-admin i18n bundle and the help line under each one came straight from + `ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over + "Human-readable package name". `getPackageForm` now reads each field's help + through the same bundle as its label, via a new `tOptional` that returns + `undefined` rather than echoing the key back. Only zh entries exist: an en-US + console finds nothing and keeps falling through to the spec's own sentence, so + the English keeps exactly one producer — `@objectstack/spec` in the framework + repo — and this repo never holds a copy of it to drift. + + **The publish panel's primary button opens clear of the toast stack.** The + console mounts its toaster bottom-right and `DraftChangesPanel` is a + `side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way + there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed + out. Opening the panel now clears the stack the surface the author just left had + raised. Repositioning the toaster was measured and rejected: the draft preview + bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar` + anchors bottom-centre and the nav rail owns the left edge, so a move only + relocates the same collision onto a different primary control. + + Not fixed here, and not fixable here: the other strings the card names are + produced outside this repo. `Owning Business Unit` (`packages/spec`), + `Search Index` (`packages/objectql`) and the `Revise Window` flow node's name + and description (`packages/plugins/plugin-approvals`) all come from the + framework, which owns their translation catalogue; patching them in the console + would create a second source of truth that diverges at the next framework + release. +- 6cc62e2: Studio Interfaces: the nav-leaf binding reads the canonical target key only + + `resolveSurface` fell back to the bare spellings `page` / `object` / + `dashboard` / `report`, and carried a `case 'view'`. Every + `NavigationItemSchema` member is a `strictObject`, none of those bare + spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is + not one of the union's nine members — so all of them are keys `AppSchema` + answers with `unrecognized_keys`, and every one of those branches could only + fire on an app that cannot be saved. The Studio nav item inspector's object + picker likewise read `node.object ?? node.objectName`, preferring the rejected + spelling over the canonical one; it now reads the canonical key first. + + No shape that parses today stops parsing: this narrows the designer back to + what the contract already declares. +- d15a92d: Studio's pre-publish security block can now take you to the object it names. + + The pending-changes sheet reports what the publish door would refuse — as + `object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under + Settings → Record sharing". Naming it was the half that shipped; reaching it was not. + The `?surface=:` deep-link that would have carried the author there + captures the URL exactly ONCE, at mount, and the sheet is opened over an + already-mounted pillar — so writing the param changed the URL and moved nothing. + + That mount-time capture is deliberate and stays exactly as it was: the mirror half + rewrites the param on every in-pillar selection, so a capture that followed the URL + would re-trigger its restore on each one. What was missing is a third half — a live + target delivered BESIDE the URL, which is what a producer already inside the pillar + needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity + (`{type, name}`), the host routes cross-pillar requests back through the URL (that + pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the + author declines over unsaved edits, and the mounted pillar applies the rest. + + Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next + rail reload would drag the author back off whatever they had since selected — the + regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` + pins a hand-picked object surviving a package switch, and + `surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it + still ignores every URL change after mount, and a live request never moves it. + + The sheet's other home is the Home / draft-preview bar, where the Studio object editor + is not a reachable destination at all. Reachability is answered structurally — the + producer hook returns `null` when no host published the channel — so off-Studio the + item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both + directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. + + Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its + signature, its return and its behaviour, and only the Data pillar subscribes to the + new channel. The channel is its own React-only module on purpose — importing the hook + into the sheet would have pulled `nav-selection` and the App-nav inspector into the + console's eager graph. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- Updated dependencies [3b147a3] +- Updated dependencies [b2e85a9] +- Updated dependencies [77f846a] +- Updated dependencies [76ae729] +- Updated dependencies [dd19463] +- Updated dependencies [a31adc6] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [84d2e98] +- Updated dependencies [f24195a] +- Updated dependencies [56f4e34] +- Updated dependencies [4db5989] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cad512f] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [32ef595] +- Updated dependencies [15236e0] +- Updated dependencies [ec9fdaa] +- Updated dependencies [d6613a2] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [6c5ee71] +- Updated dependencies [6f017e9] +- Updated dependencies [f1c27f0] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [0ccbdc1] +- Updated dependencies [83ec618] +- Updated dependencies [4bb940b] +- Updated dependencies [0068348] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [934a532] +- Updated dependencies [26a2238] +- Updated dependencies [cfcff30] +- Updated dependencies [3c73d99] +- Updated dependencies [6b348d2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/data-objectstack@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/plugin-calendar@17.7.0 + - @object-ui/plugin-chatbot@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/plugin-form@17.7.0 + - @object-ui/plugin-dashboard@17.7.0 + - @object-ui/plugin-designer@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/layout@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/plugin-view@17.7.0 + - @object-ui/plugin-charts@17.7.0 + - @object-ui/auth@17.7.0 + - @object-ui/plugin-list@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/plugin-editor@17.7.0 + - @object-ui/plugin-kanban@17.7.0 + - @object-ui/plugin-report@17.7.0 + - @object-ui/collaboration@17.7.0 + - @object-ui/providers@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index 37335039e..45d66f90b 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/app-shell", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine", diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 06a23c79b..532968121 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,82 @@ # @object-ui/auth +## 17.7.0 + +### Patch Changes + +- 32ef595: Restore platform-admin detection for permission-set-derived administrators. + + `useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace + gating and the "Build an app" CTAs. Its third source read `user.roles`, a key + the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it + to `positions`). An administrator whose adminship comes from the + `admin_full_access` permission set — the single-tenant deployment shape, where + there is no organization member row and the server deliberately no longer + overwrites `user.role` — matched none of the three sources and read as **not an + administrator**: Setup and Studio simply disappeared for them. + + The hook now reads `user.positions[]`, the one spelling the session publishes. + Detection is restored for that path and unchanged everywhere else: an active + member row with an admin role, a stored `user.role` admin scalar, and + preview/no-auth mode all behave exactly as before, and nobody who was not an + administrator becomes one — pinned by four negative cases alongside the + positive one. + + Also corrects the now-stale documentation that described the removed spelling: + the hook's own docblock, the `roles?: string[]` declaration on the client + `AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and + two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the + comment corrections. +- 934a532: Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the + unstamped-first-request window in which it is not sent (objectui#5279). Documentation + only — no behaviour changes. + + The header had no written contract anywhere, and the shape of the missing information was + actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a + search confined to this repo and the framework (`objectstack`) returns zero readers and + reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was + held until a cloud-side reading came back non-empty. Without the contract written down, + the next person to grep reaches the same false conclusion and deletes a live routing + input. + + `packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means + (a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim, + not an authorization input, not what scopes rows), who stamps it and under exactly which + condition, who reads it, and what a reader may and may not assume. The framework half is + stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the + API-key principal or `session.activeOrganizationId` and from no header — alongside + `plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability. + The configuration half is quoted from the contract this package can actually resolve, + `TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the + default of a configurable `tenantHeaderName` and `header` ranks second of six + identification sources behind `subdomain`. + + The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is + filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` -> + `getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at + all. What a reader observes is documented as **absent, never present-and-empty**, with the + five situations that open the window and the instruction to fall through to the next + identification source rather than fail closed. The gap is recorded, deliberately not + closed: the cloud readers observe today's behaviour, so changing when the header first + appears is its own decision. + + Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes + about the wire — no active organization means no header at all, the stamp is not gated on + `/api/` the way `Authorization` is, and the active organization overwrites a caller-set + `X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed. +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/auth/package.json b/packages/auth/package.json index 195443c69..332bff441 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/auth", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Authentication system for Object UI with AuthProvider, useAuth hook, AuthGuard, and form components.", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index f54db40ee..ba2f3be6e 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,103 @@ # @object-ui/cli +## 17.7.0 + +### Minor Changes + +- 94021dd: `objectui check` judges a file's `type` only when the file is recognisable as an ObjectUI schema, and reports how many it declined to judge. + + A root `type` was treated as a component key wherever it appeared. `type` heads at + least seven unrelated JSON vocabularies, and the most common of them is + `package.json`'s `"type": "module"` — so the first line a user saw running + `objectui check` in their own project was a warning about their own package + manifest. Measured at this repository's root: 46 warnings, 45 of them + `package.json` (objectui#5127). + + A file now enters type judgement only when its root carries a structural key + declared on `BaseSchema` — `children`, `body`, `className`, `placeholder`, + `style`, the `visible`/`hidden`/`disabled` predicate family, `testId`, + `ariaLabel`. Every other root-`type` vocabulary — JSON Schema's `"array"`, an + `.eslintrc.json`'s `"commonjs"`, a package manifest's `"module"` — is simply + never judged. The key set is read out of the node contract rather than invented, + and it is closed: it grows only when `BaseSchema` grows. + + A list of filenames to exclude was the alternative and was rejected: it is a + second hand-maintained list of the shape objectui#5115 had just finished + deleting, and it can only ever enumerate the foreign vocabularies someone already + thought of. This is a positive marker instead. + + Because the marker narrows what is checked, the command now also reports the + count of files that had a root `type` and no marker, together with the marker + keys that opt one back in. That number is the coverage this gate gives up until + schema files are recognisable, and printing it is what keeps the loss visible + rather than silent. The `.yaml`/`.yml` half of the scan is unchanged — it was + never type-judged, before this change or after it. Exit codes are untouched: a + JSON parse failure remains the only thing that fails the run. + + No public `$schema` URL is introduced. An earlier revision also admitted a file + whose root `$schema` had an `objectui.org` host; the maintainer ruled against + minting that identifier (2026-08-20, objectui#5127), so the structural key is the + only marker. Because the matching was host-based rather than literal, that arm + can be added later without invalidating a single file. + +### Patch Changes + +- 100547e: `objectui validate` now refuses a form field whose widget id names a namespace + other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` + has given since objectui#5375 (objectui#5449). + + The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema + declared `type` and `widget` as bare optional strings — so a field typed + `ui:password` validated clean while the runtime validator rejected the same + document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an + author actually runs before shipping, so it was the one handing out the false + green: an author did exactly the diligence objectui#5375 asks for and still + shipped metadata that renders a secret into a plain text box. + + A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's + precedence (`widget` before `type`), the key it blames, its error code and its + message verbatim, so the two entry points cannot describe one defect two ways. + + **This rejects documents that previously validated.** Only colon-qualified + field widget ids outside the `field:` namespace are affected — `field:`-prefixed + ids and bare names such as `password` still pass, registered or not. A field + carrying, say, `type: 'ui:password'` must be rewritten as `password` or + `field:password`; it never rendered as a password box in any case. + + Which of the repo's authoring-time validators is canonical remains open + (objectui#4631) — this states the rule on the zod side rather than unifying + them. +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ff445a08..bf53a792e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/cli", - "version": "17.6.0", + "version": "17.7.0", "description": "Standalone CLI for Object UI — scaffold, develop, build and validate JSON/YAML schema-driven applications.", "type": "module", "homepage": "https://www.objectui.org/docs/utilities/cli", diff --git a/packages/collaboration/CHANGELOG.md b/packages/collaboration/CHANGELOG.md index 85e1e442d..1bbaa8833 100644 --- a/packages/collaboration/CHANGELOG.md +++ b/packages/collaboration/CHANGELOG.md @@ -1,5 +1,33 @@ # @object-ui/collaboration +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [c40f3b8] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [78cbdb5] +- Updated dependencies [38a9568] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [3c73d99] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index cacb880af..8e8ded0c2 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/collaboration", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Real-time collaboration for Object UI with presence tracking, live cursors, conflict resolution, and comment threads.", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d8a3eadc6..55bdbbfa4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,525 @@ # @object-ui/components +## 17.7.0 + +### Minor Changes + +- dd19463: `stack` now reads its spacing from `gap` and nothing else — the undeclared + `spacing` key it also accepted is gone (objectui#4890). + + `StackSchema extends Omit`, whose spacing key is `gap`. + `spacing` was declared by nothing: not the TypeScript interface, not the zod + mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway, + as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole + story, since it existed to get past the type system saying the key was not there. + A lenient consumer leg does not stay in the consumer: it becomes a second + de-facto contract that producers write to, and 135 nodes across 39 files of the + shipped schema catalog did exactly that. Every one of them rendered correctly, so + nothing ever pointed at it, while the examples went on teaching the key to every + author who copied them. + + The trap it was one edit away from springing: `flex` — semantically a `stack` + with a `direction` — never read `spacing`, so re-typing any of those nodes would + have dropped the spacing to the default silently. Fixed at the producer + (AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the + alias is deleted rather than legalised into `StackSchema`, where it would only + have been a second name for `gap`. + + **If you author `spacing` on a `stack`**, rename it to `gap`; the value and the + rendering are unchanged. A `stack` still carrying `spacing` now renders the + default gap, exactly as a `flex` always did. + + Also in the same sweep, and visible only in the published example catalog rather + than in any package API: 140 catalog nodes that were already `flex` / `stack` / + `container` stopped hand-writing their own declared props in `className` + (`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`, + `flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all + (objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in + `className`, because the props are not responsive. Both facts are ratcheted in + `examples/schema-catalog/test/layout-props-conversion.test.tsx`. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 490f482: The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling. + + `renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a + cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared + on `TableColumn`, which declares `header` and `accessorKey` — both required + (`packages/types/src/data-display.ts`). This was the fourth site of the + column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid` + (objectui#5350). + + Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the + family direction — *retire the consumer-side alias; unify the producers* — and it + names this site: the declared `header`/`accessorKey` contract wins. + + What makes this site different from its three siblings is that the alias was not + merely tolerated, it was **published**. `content/docs/api/schema-reference.md` + §TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a + property row reading *"Column definitions with `name`, `label`, …"*, while + `packages/types` declared the opposite pair. Docs and type disagreed about one + type they both call `TableColumn`, each internally consistent. Retiring the alias + without correcting the page would have turned a documented, working example into a + silently broken one, so both halves land together: the page now authors + `accessorKey`/`header`. The same row also advertised a `render` property that + `TableColumn` has never declared — the renderer's hook is `cell` — and that claim + is dropped rather than re-spelled. + + The failure mode of a now-unresolvable column is worth stating, because it is + quiet: the column keeps its slot and its neighbours are unaffected, the header or + the cells simply render empty, and nothing throws. This renderer keys its cells by + index rather than by accessor, so unlike `data-table` it does not even produce + React's generic missing-key warning — a retired-spelling column is fully silent. + Whether that silence should become an authoring diagnostic is objectui#5349's + question; no diagnostic is added here. + + The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so + re-introducing an undeclared alias on this renderer is now a type error rather + than a reviewer's catch. + +### Patch Changes + +- 3b147a3: `ActionParamDialog` (the `custom` barrel's published dialog) now resolves each + `select` param's options through `@object-ui/core`'s shared option evaluator, so a + per-option `visibleWhen` narrows the offered list here exactly as it does on the + app-shell action dialog and in the object form (objectui#4758). + + This surface is the repo's second action-param dialog, and its `select` branch + rendered `param.options?.map(...)` straight into Radix items. A per-option + `visibleWhen` was not evaluated wrongly — it was not evaluated at all, so an option + gated on `record.*` (a sibling param) or on `current_user.*` was offered + unconditionally, while the app-shell dialog filtered the identical field metadata. + Triage ruled the governed side authoritative; the dialog rebinds to + `resolveVisibleOptions`, resolving predicates against the dialog's own in-progress + values (the objectui#3765 Option B ruling) plus the ambient predicate scope. + + Rebind, not removal: the component stays a published export, its props and every + other branch are untouched, and retiring it remains a separate decision. + + A selection the predicate stops offering is now cleared rather than kept as a hidden + value — the same `isValueStillOffered` clear `SelectField` already performs. Without + it, filtering alone would let a picked-then-gated-out option vanish from the trigger + while still riding in the submitted payload. Params whose options declare no + predicate are untouched. +- fe76ece: The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. + + `vite-plugin-dts` emits one declaration file per source file, and TypeScript + copies a module specifier into the declaration verbatim. `export * from './ui'` + therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in + `@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted + trees. Node16/NodeNext resolution does not extension-search a relative + specifier, so the compiler could follow none of the hops and every symbol they + carried read as absent from the package: + + ``` + error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. + ``` + + Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls + in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from + `layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. + On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. + + The emitted `.js` never had the defect — rolldown resolves the same specifier + away — which is why `pnpm check:esm-specifiers`, whose verdict is about + specifier-preserving `.js` builds, correctly never scanned either package. The + fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, + shared by both `vite.config.ts` files), not in the sources: the same source line + produces a clean `.js` and a broken `.d.ts`, so no source edit can express the + difference. The rewriter resolves each specifier against the source tree the + output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — + throws on anything it cannot resolve, and after the build re-parses the emitted + declarations to assert every relative specifier both carries an extension and + names a file the build really emitted. + + `packages/fields` takes the `nodenext` pin as a result — the same two lines + `packages/react` has carried since objectui#4538 — so the property is enforced by + the compiler on the consumer side rather than by review. `packages/app-shell` + does not: it type-checks clean without the pin and still shows 23 errors with it, + none of them from these two packages. That residue is filed separately. +- 485f096: The form renderer now keeps a `defaultValues` reset off `onChange` and off the + `form_change` `onAction` for **every** caller — including one that memoizes the + callback (objectui#5235). + + "A record landing is not a user edit" was already this file's documented, + pinned behaviour, but two of the three channels delivered it by accident of + React's effect ordering: every layout DESTROY runs before any layout CREATE, so + a caller passing a fresh callback each render had its value subscription torn + down before the reset and re-established after. The guarantee was therefore + delivered by the callback's *identity changing*. Wrap the same callback in + `React.useCallback` — taught everywhere as a semantically neutral performance + optimization — and the identity stays put, the effect never re-runs, the + subscription survives the reset, and the whole loaded record comes back to the + host as if the user had typed it: the false "the user edited this" signal + objectui#2968 was filed about, in a form no type, doc or call site warned about. + + The reset now states what those two channels report, the way `onDirtyChange` + already did (it computes its payload against the freshly installed baseline and + calls the host outright). Callers passing inline arrows see byte-identical + behaviour; callers who memoize stop receiving a phantom edit. + + Not a contract change: whether a value channel *should* report a programmatic + reset stays open in objectui#5235. This only removes the answer's dependence on + caller identity. +- b655a9d: The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer + land on the rendered `
` as invalid HTML attributes (objectui#4787). + + `grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and + `style`, so everything else `SchemaRenderer` hands a registered component reached the + element. Measured on a canary node, eight attributes leaked: + `columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, + `props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an + unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as + `columns="[object Object]"`. Layout was unaffected, so every catalog grid example + rendered with them — the reason this went unnoticed. + + The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist + objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the + SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, + `role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the + designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be + forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly + any more, including keys `GridSchema` does not have yet; enumerating today's keys to + strip would have re-rotted on the next schema addition. + + No authored input changes and no layout changes: the grid's own vocabulary was always + read off `schema`, never off these props. +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. +- fa140b8: `element:record_picker`'s `emptyText` now resolves the inline per-locale map its + contract has admitted since rc.6, and its published declaration says so + (objectui#5590). + + `@objectstack/spec` widened this key to the `I18nLabel` union + (`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA + still carries it — measured, not assumed: + `ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })` + succeeds. The renderer honoured only the string arm, handing the map straight to a + text node. React refuses a plain object in a child position rather than stringifying + it, so an author writing the map form the contract accepts did not get a mis-rendered + empty state — the whole picker subtree threw + `Objects are not valid as a React child (found: object with keys {en, zh-CN})`. + + The read site now resolves through `pickLocalized`, the objectui-side helper the + sibling text-node sites already read through (`element:text.content`, + `element:button.label`, `page:card.title`), which spells a miss as `''` rather than + the spec resolver's `undefined`. The default is applied before resolution, so + `emptyText` absent still means "No records" and an authored empty string still + renders empty. + + The `ComponentMeta` entry, which held a single `'string'` arm precisely because the + renderer dropped the other one, now declares `['string', 'object']`. That narrowing + was correct for exactly as long as it was true: with the map arm reaching the screen + resolved, withholding it would be the false declaration in the other direction — the + manifest gate reporting `type-mismatch` on a legal write the same input's own + `description` teaches the author to make. The `apps/console` specimen that pinned the + narrow arm named this release condition in its own words ("keeps its single `'string'` + arm until the render site catches up") and is flipped here, keeping its controls. + + Three comments in the renderer deferred this gap to objectui#4163, which closed as + completed on 2026-08-15 while the gap was still open; the file now carries no + reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very + key as its worked example of an arm deliberately withheld, and is corrected in the + same change so the example stays true. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- 5a07e67: The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it + states what it actually measured (objectui#5399). + + When an action's `visible` predicate references a `record.` the bound payload + does not carry, the warner names the missing key and then explained the cause: + + > Hidden (hidden: true) fields are stripped from detail payloads server-side, so a + > predicate gating on one may evaluate to a hide-by-default verdict. + + That cause is false, and it names a mechanism this repo does not own. `hidden` is a + UI concern — the framework spec describes it as "Hidden from default UI" + (`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against + the framework checkout rather than taken on trust: ObjectQL's own strip for the + `__search` companion documents that the `hidden` / `readonly` / `system` markers are + "None of them is a PROJECTION rule", which is precisely why a dedicated strip rule + had to be written for that one column; drivers answer a query with no `fields` using + `SELECT *`; and `metadata-protocol` enumerates what the read path does drop — + `internal: true` columns and the `__search` companion, and nothing else. The only two + read-side uses of `field.hidden` in the framework are auto-view/auto-form column + generation and companion-source eligibility, neither of which removes a key from a + record body. + + So an author who read this diagnostic went hunting for a `hidden` flag they would + either not find, or find on a field the payload demonstrably still returns — while + the real source of the sparseness (a projected or partial read) went unexamined. A + confidently wrong cause in a diagnostic is worse than no cause, because it is + actionable in the wrong direction. + + The replacement states the fact this surface can actually see and the consequence it + does own: the page bound a payload without those keys, a projected or partial read + will not carry them, and the predicate therefore fails closed and hides the action. + The measured half of the message — action name, missing fields, predicate source — + is unchanged, and nothing about what triggers the warning changed. + + Message text only. The same false claim also sat in this warner's own doc comments + and in the comments of the test that pins the message; both are corrected here, and + the docstring now carries an explicit note against re-attributing the cause to + `hidden: true`. No other call site was swept. +- Updated dependencies [77f846a] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/react-runtime@17.7.0 + - @object-ui/sdui-parser@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/components/package.json b/packages/components/package.json index fba2d9db8..9d7dbc4a9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/components", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Standard UI component library for Object UI, built with Shadcn UI + Tailwind CSS", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 8da3d5884..dcd723c1c 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,386 @@ # @object-ui/core +## 17.7.0 + +### Minor Changes + +- d7573b3: `ComponentInput` is now declared once and re-exported, instead of restated in three + places (objectui#4972). + + `@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' + plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) + were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now + re-exports of that one declaration, which is the disposition objectui#4580 ruled for the + identical shape — *a structural copy would reproduce the defect the moment either side + moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. + + Either side had already moved. `base.ts` declared thirteen keys; both copies declared + nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every + component registration actually imports**. Those four keys were unwritable at any real + registration — a plain TypeScript error at the call site — while `ComponentInputSchema` + (the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face + advertised four keys the authoring face rejected. Measured over the repository, no + registration had tried to write one yet, so nothing a user hits was broken today; what + changes is that the four keys become writable, and there is no longer a second + declaration for the next widening to miss. + + `ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single + declaration imported by all three sites (objectui#3832); this converges the rest of the + interface. + + Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is + byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported + here only with its control — a probe that added a *required* key to `ComponentInput` left + the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of + `export *` lines that names `ComponentInput` zero times. The gauge that can actually fail + is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does + `@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted + declarations that change in either package. + + `WidgetInput`'s union-arm capability is deliberately untouched — a different gate path + and a separate judgment. +- bf3edfe: `ComponentRendererProps` is now declared once and re-exported, instead of + hand-declared a second time in `@object-ui/core` (objectui#4594). + + `@object-ui/core`'s `ComponentRendererProps` (`src/types/index.ts`) was a + non-generic interface typing `schema` as `SchemaNode`, while + `@object-ui/types`' declaration of the same name is generic — + `ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` with + `schema: TSchema`. Same name, both exported from their package entry, from two + packages the same consumers import together: which declaration a call site got + depended on which package it reached for, and the two disagree about whether a + primitive node is admissible. Core's is now a re-export of types', which is the + disposition objectui#4580 ruled for `SchemaNode` two lines above it in the same + file, and objectui#4972 for `ComponentInput` — *a structural copy would + reproduce the defect the moment either side moved*. + + **Published-surface effect, and the reason it is not neutral.** Resolved + through the TypeScript checker from `core/dist/index.d.ts` over a clean rebuild + of both legs, `ComponentRendererProps` as reached through `@object-ui/core` + moves from non-generic with + `schema: BaseSchema | string | number | boolean | null | undefined` to + `ComponentRendererProps` with `schema: TSchema`, defaulting to + `BaseSchema`. `schema` therefore **narrows** back to the object form — core's + copy had silently widened when objectui#4608 made core's `SchemaNode` a + re-export of types' union — and the type gains a parameter. **Nothing imported + it**, on either side, re-verified repo-wide on the merged ref, so no call site + can observe either move; the narrowing is recorded here because it is a change + to a published type, not because a consumer is affected. + + A compile-time pin now holds the reconciliation from + `@object-ui/react` — the only position that resolves both packages through + `node_modules` — alongside the existing `SchemaNode` one. It is a test-only + addition and emits nothing, so `@object-ui/react` takes no bump of its own. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 8ebd57f: Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction` + JSDoc are now qualified, so each resolves to the entity it means (objectui#5580). + + `ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method + on `ExpressionEvaluator` (bare expression, throws) and the module-level export + (context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block + referred to both under the one spelling, four lines apart. + + The prose link was not merely ambiguous, it was bound wrong. Measured with + `checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}` + resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the + sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}` + binds to the method, but only because no module-level `evaluate` exists to outrank + it, so the rule "an unqualified link resolves to the enclosing class's member" does + not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`, + which the checker resolves to the `MethodDeclaration`. + + The `@example`'s final line is the module-level export — its second parameter is a + context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat + two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover + carries no import to disambiguate. It now names the module-level export and shows the + import it needs. + + This is prose only: the diff is confined to a block comment and no declaration moves. + It is scored `patch` rather than the empty-frontmatter form because the block is + emitted into what npm ships — measured, this edit moves both + `dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js` + (this package builds with a bare `tsc`, which preserves comments in the JS emit), and + the ten changed lines in that JS are all comment lines. + + `registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather + than asserting it in prose, since a `{@link}` that binds to the wrong entity is + indistinguishable in source from one that binds right. +- 7138bc1: The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's + privileged-override safety copy (objectui#5611). + + `ActionRunner.execute` classifies the object it was HANDED, and a console host + hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes + `overrideNotice` on that dispatch and two param-collection handlers read it — + yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a + key two files read as one "no reader recognizes", and prescribed promoting it to + an explicit field on `ActionDef`. That prescription is the one shape the + 2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic + walked an author into a rejected design. A false warning on the product's own + privileged path — the branch that finalises an approval over approvers who have + not acted — is how a dev console gets muted. + + Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to + `actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the + fourth input to that set and the first one that is not an authored-surface + mirror. Measured before and after on the exact dispatch the bar composes: the + warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS` + grew by exactly one member, and an action carrying a real typo alongside it + still warns — naming `targt` only. + + The authored surface does not move. `overrideNotice` is still NOT declared on + `ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal + remains a compile error, and the AST-derived pin over the interface is unchanged. + Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what + an author may write — `actionKeys.pin.test.ts` now pins both halves, including + the new list's exact contents so a second member cannot arrive quietly. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- f2158ec: `ExpressionEvaluator.registerFunction` now documents the case-fold it has always + performed: the name is stored — and must be called — in UPPER CASE + (objectui#5363). + + `registerFunction('formatCurrency', fn)` registers `FORMATCURRENCY`, because the + method delegates to `FormulaFunctions.register`, which stores under + `name.toUpperCase()`. That fold is correct for the spreadsheet-style built-in + vocabulary (`SUM`, `IF`, `UPPER`) and is unchanged here — but nothing declared + it on the public method, and two things keep it from being self-evident at the + call site. The registry API stays case-insensitive, so `getFormulas().has()` and + `.get()` both answer to the original spelling and never reveal the fold; only + expressions see the stored key, because the evaluation scope is built from + `FormulaFunctions.toObject()`, a plain object whose identifiers are matched + case-sensitively. And a wrong-case call site does not raise: `evaluate()` + catches, warns, and returns `defaultValue ?? expression`, so the template + renders its own `${...}` source as literal text on screen rather than erroring. + + Behavior is untouched — this is the declaration catching up with what the code + enforces. It ships as a patch rather than as an empty changeset because the + JSDoc is emitted into the published `dist/evaluator/ExpressionEvaluator.d.ts`, + so it is what consumers see on hover. + + `ExpressionEvaluator.test.ts` gains three cases pinning the half that was + uncovered — that the given spelling does *not* resolve in an expression, that + the failure renders the raw template source instead of throwing, and that the + registry API stays case-insensitive underneath — so making registration + case-preserving fails a test instead of silently invalidating the new JSDoc. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 068fcc60f..39e1a5744 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/core", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/create-plugin/CHANGELOG.md b/packages/create-plugin/CHANGELOG.md index 9e3b4d302..5e1f0eae0 100644 --- a/packages/create-plugin/CHANGELOG.md +++ b/packages/create-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @object-ui/create-plugin +## 17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/create-plugin/package.json b/packages/create-plugin/package.json index 38fbec8df..66611aca1 100644 --- a/packages/create-plugin/package.json +++ b/packages/create-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/create-plugin", - "version": "17.6.0", + "version": "17.7.0", "description": "CLI tool to scaffold ObjectUI plugins", "type": "module", "license": "MIT", diff --git a/packages/data-objectstack/CHANGELOG.md b/packages/data-objectstack/CHANGELOG.md index 731d4a9c8..82215ea6f 100644 --- a/packages/data-objectstack/CHANGELOG.md +++ b/packages/data-objectstack/CHANGELOG.md @@ -1,5 +1,113 @@ # @object-ui/data-objectstack +## 17.7.0 + +### Minor Changes + +- 8e00bfd: **Breaking (published surface):** remove `options.actor` from `MetadataClient`'s + `save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor` + request header. + + The server stopped honouring that header. objectstack#7941 ruled that the + recorded actor is the identity the request was authorized as, and removed the + header limb from the `/meta` write resolver — attribution cannot drift from + authorization. The option therefore typed cleanly, sent a header, and could not + influence the audit or history row it appeared to address: a false affordance + that promised attribution and silently failed to deliver it. + + Three declarations go: `MetadataClientSaveOptions.actor` (inherited by + `MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`), + and the inline `{ actor?: string }` on each of `publish` and `rollback`. + `MetadataAuditEntry.actor` is unaffected — that is the server's read-back of + who acted, and it remains the way to see attribution. + + Marked `minor` rather than `major` per this repo's version-alignment policy + (the fixed group's major tracks `@objectstack`, and `major` in a changeset + would drag all 39 packages off that cadence). + + No caller in this repo passed `actor`; the census found the only in-repo + occurrence was the client's own unit test. Callers outside this repo that still + pass it are unaffected at runtime beyond losing a header the server already + ignored — the property is dropped rather than forwarded, pinned by + `metadata-actor-retired-4834.pin.test.ts`. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- b2e85a9: `ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata + types in the singular, matching the other twelve `client.meta.*` call sites in this + file (objectui#4940). + + `getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed + the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued + its singular spelling was deliberate — the plural site was silent. Both plural sites + resolved today only because the server folds plural → singular + (`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`, + confirmed by reading both the mapping and the by-name route handler that calls it), so + this is consistency restoration rather than a behavior change — nothing a user hits was + broken, and nothing a user hits changes. + + `appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended + with two new cases asserting `getApp`/`getPage` pass the singular type to + `client.meta.getItem`, so a future revert to the plural spelling fails a test instead of + depending on the server-side fold staying in place. +- 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no + title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three + times (objectui#5024). + + `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four + states out by hand, 42 lines apart in one file, compared by no gate. They are now + one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s + `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies + were restating a schema rather than filling a gap. + + The user-visible half is the banner. Its title was three independent `&&` branches + with no fallback, while the switch that opens the banner is true for any non-`none` + value — so a lock state outside the four opened the box and left the headline + empty. That is reachable without a fifth state ever being added here: + `MetadataClient.layered()` casts the wire value through unchecked, so a newer + server reaches this banner as-is. Measured, not assumed — feeding `no-publish` + through the page rendered the padlock, the border and an empty title. The title is + now a keyed lookup with a loud fallback that names the unrecognised token, so a + fifth state fails `type-check` here and, if one arrives from a server anyway, the + operator reads a sentence instead of a blank box. +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json index c4db2a5d9..c97bbcf31 100644 --- a/packages/data-objectstack/package.json +++ b/packages/data-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/data-objectstack", - "version": "17.6.0", + "version": "17.7.0", "description": "ObjectStack Data Adapter for Object UI", "license": "MIT", "type": "module", diff --git a/packages/fields/CHANGELOG.md b/packages/fields/CHANGELOG.md index c64ede524..f50f02769 100644 --- a/packages/fields/CHANGELOG.md +++ b/packages/fields/CHANGELOG.md @@ -1,5 +1,272 @@ # @object-ui/fields +## 17.7.0 + +### Minor Changes + +- 9a1fb41: **API addition (public-surface widening):** `FileCell` — the compact upload + control `@object-ui/fields` exports for line-item grid cells — gains the + published optional `error?: string` slot, mirroring `LookupField` and + `FileField`: the same validation slot `@objectstack/spec/ui`'s + `FieldWidgetPropsSchema` declares and `FieldWidgetComponentProps` names + (objectui#3222). When set, `FileCell` puts `aria-invalid` on its own focusable + picker button; the message text stays with the host (objectui#5431). + + `GridField` now passes that slot for a required-but-empty `file` cell — the one + cell type objectui#3318's per-cell `aria-invalid` delivery left out. Before + this, a required `file` cell flagged only the visual ring and `title` on the + `td`; no element in the cell subtree announced the state, so assistive tech was + told nothing (a wrapper-only mark is exactly what objectui#5223 forbids). Text, + number, select, and lookup cells were wired in PR #5429; `file` cells now + behave identically. + +### Patch Changes + +- a865c73: Grid field widget: announce a form-level validation failure to assistive tech. + + A required `grid` submitted while still empty rendered its "is required" message + but marked nothing — every row was a ghost row, and ghost rows were skipped by + the widget's per-cell validity channel. A sighted user saw the red message; a + screen-reader user was told nothing at all. + + The host failure now drives the per-cell channel the widget already owns: when + the `error` slot is set on an empty grid, the ghost entry row's required cells + flag, and the mark sits on each cell's own control rather than on the `td` + wrapper (a `td` is not focusable, and assistive tech reads validity from the + control). Populated grids are unaffected — they already marked their own empty + required cells inline. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- 7a28e1e: A lookup's inline dropdown renders its columns through the same cell renderer the browse-all picker uses, so one `lookup_columns` declaration cannot produce two answers. + + A form's lookup field offers two ways to pick a related record, and both read + the same declaration: the inline dropdown under the field, and the + "browse all records" picker behind it. The picker resolved every cell through + the type-aware cell renderer. The dropdown did not — it printed + `record[descriptionField]` verbatim into the option subtitle and concatenated + `label: String(rawValue)` into the row's `title` attribute. Measured on the + same declaration, on a real 17.1.0 deployment: + + ``` + column inline dropdown (before) browse-all picker + lookup T5MsMCuwP4t_yUHq (bare FK id) the related record's name + date 2026-08-20T00:00:00.000Z (ISO) a formatted date + select pending (enum code) the authored option label + ``` + + Both surfaces now call one shared module — `widgets/lookupColumnDisplay.tsx`, + which owns column normalisation, the field-descriptor enrichment from the + referenced object's schema, and the render itself. The picker's own + `renderCellContent` and `columnFieldDescriptors` are now thin calls into it, so + there is a single renderer left to drift from. The dropdown's extra columns are + rendered into the option row itself; the row's `title` keeps the full option + label, which is what a truncated label needs, instead of a raw-value dump. + + No query changed and no contract widened. `lookupColumns` entries stay bare + field names — no dot paths, no populate/expand semantics — because neither + surface's request carries populate to begin with: the picker resolves a + foreign-key id to a name client-side, in the lookup cell renderer, and the + dropdown now inherits exactly that. An unresolved reference therefore renders + what the picker renders for it, and keeps its column: a slot is dropped only + when the record holds no value for the field, decided on the raw value and + never on what the renderer makes of it, so an unresolved id can never degrade + into a silently empty column. +- 4bb940b: A readonly `markdown` / `html` / `richtext` form field now renders its content + FORMATTED instead of showing the user its markup source (objectui#5498). + + `RichTextField`'s readonly early return rendered `{value}` as a React text child, + so a readonly field of any of those three types displayed the stored markup as + literal characters — a markdown field's asterisks and hashes, a richtext field's + tags. The `prose` classes on that wrapper were the tell: they style rendered rich + content, and there was none to style. Every other read surface — grid, kanban + card, gallery, related list, dashboard record panel and the record detail page's + read mode — dispatches through `getCellRenderer` and rendered the same stored + bytes formatted, so one field disagreed with itself depending on which surface it + was read on. + + The readonly branch now renders through the same components `getCellRenderer` + resolves: `markdown` through the GFM renderer, `html` and `richtext` through the + sanitizing HTML renderer. The two renderers moved out of the package barrel into + `widgets/richTextDisplay.tsx` so the widget can reach them without importing the + barrel back, and both sides now read one shared type-to-renderer table rather + than two that can drift apart. + + The editor header's format label is fixed with it: it was computed as + `field.format || 'markdown'`, and `format` is declared on `date` / `datetime` / + `time` / `phone` / `auto_number` and on no rich-content type — so it read + `undefined` for every real field and labelled an `html` field "Format: markdown". + The label is now derived from the field type's display pipeline, so it names the + syntax the value is actually stored in. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- f1690d4: A populated `richtext` field no longer renders as a blank cell (objectui#5452). + + `richtext` stores HTML — the spec documents the type as "Formatted content with + HTML/WYSIWYG", the showcase seed's own specimen is `

Rich text

`, + and this repo's designer bridge already maps `richtext` onto its `html` type. The + display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose + sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops + raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the + cell body came out empty — with no error, no fallback and no console warning, so a + populated field read as an empty field and anyone auditing data through a grid + concluded the records were blank. Measured on the same stored bytes, a neighbouring + `html`-typed column rendered them correctly, which is what ruled out "the value never + arrived". + + `richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` + (script/style/iframe/object/embed blocks, inline event handlers and `javascript:` + URLs removed) and keeps everything a rich-text editor legitimately emits — headings, + paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at + once: the grid, the kanban card, the gallery, the related list, the dashboard record + panel and the record detail page all resolve their read-mode cells through this same + `getCellRenderer`. + + The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" + one type by moving every `markdown` cell's trust boundary, so `markdown` still drops + raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/providers@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/fields/package.json b/packages/fields/package.json index 5e442f18c..8226417c6 100644 --- a/packages/fields/package.json +++ b/packages/fields/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/fields", - "version": "17.6.0", + "version": "17.7.0", "description": "Field renderers and registry for Object UI", "license": "MIT", "type": "module", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 5fa542ac9..967c338ca 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,317 @@ # @object-ui/i18n +## 17.7.0 + +### Minor Changes + +- 77f846a: The approval panel identifies the pending approver by name, not by a truncated raw id. + + A record waiting on a position rendered its approver as `positi…ager` — the + engine reference `position:sales_manager`, 22 characters, past the identity + formatter's 14-character truncation arm and middle-truncated to fit its chip. The + step names beside it were human prose; the one line answering *who is holding + this record* was an internal identifier, and not even a complete one. The same + reference reached the admin-override confirm dialog un-truncated, so a paragraph + of plain governance prose ended `— position:sales_manager` (objectui#5414). + + Both surfaces now resolve the reference before rendering, in three tiers, most + authoritative first. The server's own `pending_approver_names` wins whenever it + answers, and a backend that resolves its own slate costs the record page no extra + request. Otherwise the console reads the directory row the spec's approver + binding names — `sys_position.label` gives `Sales Manager` / `销售经理` — and, + for a position, who fills the seat (`Sales Manager · Zhang Wei, Li Na`). With no + adapter and no row, the machine name still prettifies into prose rather than + truncating. The raw reference stays on hover, which is where an internal + identifier belongs. + + An unstaffed position is surfaced rather than hidden: `销售经理(暂无在岗人员)` + is actionable where `positi…ager` is not, and it is the motivating rescue case + for the admin-override path. Staffing is deliberately tri-state — a + `sys_user_position` read the viewer is not permitted to make leaves the seat's + staffing UNKNOWN and says nothing, because "I could not look" is a different + claim from "nobody holds it" and only one of them is safe to print on a + governance surface. + + Two locale keys are added across all ten packs: `approvalsInbox.approverUnstaffed` + and `approvalsInbox.approverNameSeparator`. The separator is a translated + punctuation key rather than `Intl.ListFormat`, which was measured on this tree + joining `['张伟','李娜']` into `张伟李娜` for `zh` — two names run together with + no separator, reading as one person's name. + + The directory-backed kinds and their value columns are read from + `@objectstack/spec`'s `APPROVER_VALUE_SOURCES` rather than restated, so a new + approver type is covered the day the spec publishes it. Id-valued kinds + (`user` / `team` / `department`) keep the existing middle-truncation: a row id + has no prose to recover, and that arm is objectui#3461's answer, not this card's + defect. +- c40f3b8: A screen flow's resume result reaches the user — on both outcomes (objectui#5417). + + A dogfood walkthrough reported that a refused `resume` and a successful one + "render identically: the dialog closes and the page is unchanged", leaving no + gesture that distinguishes "created" from "rejected". Re-measured against `main` + before any change, one half of that was already fixed — `interpretFlowResponse` + reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its + prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors + after the version the report was measured on. There was no interpreter bug and + no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were + real, and they are what changed: + + - **A terminal failure no longer closes the dialog.** The reason it closed is + unchanged and is not reversed: on a `FLOW_FAILED` the engine has already + consumed the suspension, so a resubmit can only reach "No suspended run" and + must not be offered. Closing was one way to withhold that dead retry and the + expensive one — the user had just typed a form they could no longer see, and + the engine's sentence names a value that left the screen with it. The dialog + now stays open with the submit affordance withdrawn: the flat footer swaps + Submit for Close, and an `object-form` step drops its Save (which also stops a + second click from duplicating the record it had already persisted). + - **The refusal has a second, non-expiring carrier.** The toast stays — it is + viewport-fixed, so it still reaches a user scrolled past a tall step's header + — and an inline destructive `Alert` (`role="alert"`) now holds the same + sentence inside the dialog, beside the values that produced it. A retryable + refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, + and its banner clears as soon as the user starts editing. + - **A successful run invalidates what the flow WROTE, not just what the user is + looking at.** Both hosts answered `onComplete` with + `notifyDataChanged({ objectName: })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a + first-class disabled state, and the load-failure hint describes the control plane + the runtime was actually pointed at (objectui#5504). + + `apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory + default, so a stock self-hosted stack has no marketplace at all. The Console still + recommended one: Home led with "Start with a template" and "Browse App + Marketplace", and the click landed on a red **Failed to load marketplace / Not + found** card whose hint claimed this runtime "points at the public ObjectStack + cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for + exactly the deployment reading them — the operator had not left the default, and + the advice pointed back at the template that told them to set `off`. "Marketplace + disabled by configuration" is a configuration conclusion, not a load failure. + + - `isMarketplaceEnabled()` (`runtime-config`) reads the server's own + `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the + serving app's route table (objectstack#8356). It is never inferred from the shape + of a failed request: a control plane that is merely DOWN leaves the flag `true`, + so an outage still renders as an outage. Unknown fails OPEN. + - The marketplace page renders an informational "App Marketplace is turned off" + state — muted, not `destructive` — and issues no request it knows will 404. + - Home's "Start with a template" cover greys out with a visible localized reason, + and the "Browse App Marketplace" shortcut is withheld, exactly as they already + are for the `manage_metadata` capability gate. + - `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the + configured control plane) and `failedHintSameOrigin`. The "points at the public + cloud by default" sentence is gone: it was rendered unconditionally, including on + every runtime whose operator had overridden `OS_CLOUD_URL`. + + All ten locale packs carry the new keys. +- 7a90afd: Studio's `新建对象` asks for the record-sharing baseline, and an unauthored one is reported before Publish rather than by it. + + Creating an object through Studio collected exactly two things — display name and + identifier — and saved a draft that declared no `sharingModel`. The draft saved + happily, the form designer worked, and the object was then refused at 发布 → + 全部发布 by `security-owd-unset`: a required decision the surface never asked + for, delivered by failing, as English ADR prose in a toast that then vanished on + a timer. The one actionable word in it named a control three clicks away that + nothing routed to. + + The publish gate is correct and is unchanged — an org-wide default has to be an + authored decision, not an accident. What changes is when the console asks and + when it answers: + + - **The create dialog asks.** A third field collects the baseline, pre-selected + to `private` and glossed with the Settings tab's own strings, so a new object + is publishable by construction. `buildObjectSkeleton` now takes the value as a + required parameter — a future create path cannot omit the baseline without + failing to type-check. `controlled_by_parent` is deliberately not offered at + creation: it derives access from a master relation a brand-new object does not + have yet, so offering it would trade one publish refusal for another. + - **The review sheet reports it.** The pending-changes panel now runs the + framework's own `validateSecurityPosture` over the pending object drafts and + names any blocking finding, with its fix-it hint, next to the Publish button. + It mirrors the producer's rule rather than re-deriving it, and it reports + without blocking — the server door stays the authority. + - **The Settings tab stops calling an unset baseline safe.** It described unset + as "defaults to Private", which answers what the runtime does and not whether + the object can ship. It now reads as the publish-blocking problem it is, + styled like the external-wider warning beside it. +- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. + + The dashboard widget config panel carried a private `resolveLabel` documented as + resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference + form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline + per-locale map `I18nLabelSchema` actually admits has neither limb, so + `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy + of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, + `MetricWidget` and `MetricCard`. + + This was not a display bug. The resolved value seeds the panel's editable draft, + so a widget whose stored title was a map opened with an **empty** Title field and + the next save wrote `''` over the author's map — on the ordinary path, not an + exotic one: open the widget, change anything, save. + + Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: + + - **Reading** goes through `pickLocalized(value, language)`, so the panel shows + the active locale like every sibling surface post-objectui#4032. + - **Writing** replaces only the active locale's entry and carries every other + locale across. A title the author never touched round-trips the stored object + itself through an unrelated config edit; an edited one merges into the entry + that was displayed. The live-update callback (`onFieldChange`) forwards the + merged map for the same reason — hosts feed it back into the widget the panel + re-opens from, so a bare string there dropped the map before a save ever ran. + + `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side + inverse of `pickLocalized`, so the rule is stated once instead of re-derived per + panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, + region-qualified sibling — and deliberately stops there: the `default` / `en` / + first-value limbs are display fallbacks that hand back *another* locale's string, + and writing to one would let an author editing in `fr` overwrite English. With no + entry for the active locale the edit adds one. The pairing + `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a + write that lands where the read does not look is how a "saved" string disappears. + + A full multi-locale editing UI remains out of scope (objectui#4163). + +### Patch Changes + +- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock + noticed it (objectui#5054). + + One abandoned device approval could be noticed by either of two clocks, and the + Cloud Connection panel had a different answer for each. When the panel's own + `expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — + translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered + HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` + threw a bare `Error` carrying only that sentence, and the catch rendered it + verbatim. Same user, same failure, two languages, decided by which clock got + there first — visible on a zh console as the same abandoned approval reading + Chinese or English depending on whether the tab sat open past `expires_in`. + + `getJson` now carries the envelope's `declaredCode` and `code` across its throw, + and a single closed map turns the two RFC 8628 outcomes a user can actually cause + into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, + `access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten + locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream + spelling there — `code` is `DEVICE_CODE_FAILED` for both. + + Every other code is unchanged: `invalid_grant`, and anything upstream invents + next, still render the wire `message`, which stays the single source of truth for + failures this console has no copy for. No API, export or resolver was widened. +- 1e66879: `console.ai.pendingDrafts` — the standing unpublished-changes bar's five strings — + now exists in all ten locale packs. It previously existed only in `en` and `zh`, so + `ar`, `ru`, `pt`, `es`, `fr`, `de`, `ko` and `ja` rendered the English defaults and + `all-locales-key-parity` failed on `main` (objectui#5705). + + The feature landed `en`-only in objectui#5696; the follow-up in objectui#5697 was + titled for the locale packs but reached only `zh`, so eight packs × five keys stayed + missing and the parity assertion — which carries no allowlist — was red on `main` and + on every PR whose diff touched source. Source-free diffs skip the shard that runs it, + which is why the breakage survived several merges. + + Each pack keeps its own conventions rather than `en`'s: the eight all quote with `"`, + `ru` puts the number last (`…: {{count}}`) as it already does for the sibling + `home.pendingDrafts` counts, and `ja` uses the full-width `:` before `{{detail}}` + because that value is a runtime message rather than a single token — both choices + carry an in-pack note. Terminology is taken from each pack's existing publish-bar + vocabulary (`home.pendingDrafts`, `console.ai.seedWarn`) so the two banners read + alike. + + Both interpolations survive verbatim in every pack — `{{count}}` in `count` and + `{{detail}}` in `publishedWithFindings` — asserted mechanically against the evaluated + packs, not by eye. The unrelated `home.pendingDrafts` block (`message` / `cta`) is a + different node and is untouched. +- c5200f0: Follow-up to #5696: the pending-drafts bar's strings live at `console.ai.pendingDrafts.*` with en+zh locale entries — the i18n call-site key gate and ratchet flagged the original root-level keys that existed nowhere. +- 38a9568: `useObjectLabel` now keeps a stable identity when no i18next instance is bound, + so the memoization it advertises holds on the no-provider path too + (objectui#5564). + + react-i18next's `useTranslation` builds its return value out of a fresh `{}` on + every render when it has nothing to bind to (`const finalI18n = i18n || {}`, + which then feeds that hook's own `useMemo` deps), so the `i18n` object arrived + with a new identity each render. `useObjectLabel` keyed its memo on `[t, i18n]`, + so the memo never held: measured 4 distinct returned objects across 4 renders + with no instance, against 1 with one. That is the wrong way round — the memo + exists to stop downstream `useMemo`/`useCallback` deps from being re-keyed in + heavy consumers, and `useSafeFieldLabel`'s docstring names the no-provider case + as the one it exists to serve. + + Both memo dependencies are now pinned to module-level constants while no + instance is bound. The substitution is unobservable rather than merely + convenient: every `t()` call in the module sits inside a + `for (… of getAppNamespaces())` loop, and `getAppNamespaces()` returns `[]` + under exactly the same "is there a usable instance" predicate — so while the + substitution is in effect, the closures cannot read either value. When an + instance appears the dependencies become the live values again, so a provider + mounting after first render recomputes the object exactly once and resolves + real translations from then on. + + No API change: no new exports, no signature changes, and the returned surface is + identical on both paths. Direct `useObjectLabel()` consumers are fixed alongside + `useSafeFieldLabel()` ones, including `ListView.filterFields` — the consumer the + memo's own docstring names. +- b2437a7: `setLocalized`'s published docblock states the single-locale write rule that is + actually in force, instead of deferring the multi-locale-authoring question to a + closed card (objectui#5591). + + The docblock read "is not a multi-locale authoring UI (objectui#4163)". objectui#4163 + closed as completed on 2026-08-15 with that product question still unanswered, so the + parenthetical pointed at nothing — and it read as though the question had been settled + somewhere a reader could go and check. This is the failure mode objectui#5428 + demonstrated is not harmless: there, a dangling deferral of exactly this shape let an + expired justification sit unread for a release cycle at two surfaces. + + The remedy is objectui#5428's, not a re-pointing at a successor card: state the rule in + force (`setLocalized` reaches only the entry for the locale the author is in), keep the + open product question open **in place**, and record why there is deliberately no tracker + reference — so the next reader cannot restore one. Re-pointing is how the class + regenerates, because the next card closes too. The same wording form already landed in + `plugin-designer`'s `writeWidgetTitle` and `DashboardWidgetInspector`. + + Prose only. No behaviour, no signature, no test changes — `setLocalized`'s pairing with + `pickLocalized` is unchanged and still pinned by `src/__tests__/setLocalized.test.ts`. + + Declared as a `patch` for `@object-ui/i18n` alone because the emit was measured per + package rather than assumed, and the two packages this change touches differ: + + - `@object-ui/i18n` — the docblock sits on the **exported** `setLocalized`, so it reaches + the published artifacts. Rebuilt with `tsconfig.tsbuildinfo` cleared first (the build is + `composite`, which otherwise skips emit), and compared by SHA-256 rather than byte count: + `dist/pickLocalized.d.ts` `1e2170ad…` -> `124a1c07…` and `dist/pickLocalized.js` + `06eb88bd…` -> `568cb703…`. A consumer reads this text on hover and in the API docs, so + it publishes something. + - `@object-ui/plugin-dashboard` — the two comments changed there are a `//` banner between + declarations and a test docblock, neither attached to an exported declaration. + `dist/WidgetConfigPanel.d.ts` is **byte-identical** across the rebuild + (`93252e8cdf5a6faa…` both sides). The only artifact that moved is + `dist/WidgetConfigPanel.d.ts.map`, whose mappings shift because lines were added above + the declarations; no declaration text changed. Nothing user-visible publishes from that + package, so it is not named here. +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [e719ebd] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [f2158ec] +- Updated dependencies [6c6cee7] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] + - @object-ui/core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index a1173f659..08ec2ead5 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/i18n", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/layout/CHANGELOG.md b/packages/layout/CHANGELOG.md index 728f273d4..3b76ba3fa 100644 --- a/packages/layout/CHANGELOG.md +++ b/packages/layout/CHANGELOG.md @@ -1,5 +1,83 @@ # @object-ui/layout +## 17.7.0 + +### Patch Changes + +- fe76ece: The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. + + `vite-plugin-dts` emits one declaration file per source file, and TypeScript + copies a module specifier into the declaration verbatim. `export * from './ui'` + therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in + `@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted + trees. Node16/NodeNext resolution does not extension-search a relative + specifier, so the compiler could follow none of the hops and every symbol they + carried read as absent from the package: + + ``` + error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. + ``` + + Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls + in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from + `layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. + On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. + + The emitted `.js` never had the defect — rolldown resolves the same specifier + away — which is why `pnpm check:esm-specifiers`, whose verdict is about + specifier-preserving `.js` builds, correctly never scanned either package. The + fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, + shared by both `vite.config.ts` files), not in the sources: the same source line + produces a clean `.js` and a broken `.d.ts`, so no source edit can express the + difference. The rewriter resolves each specifier against the source tree the + output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — + throws on anything it cannot resolve, and after the build re-parses the emitted + declarations to assert every relative specifier both carries an extension and + names a file the build really emitted. + + `packages/fields` takes the `nodenext` pin as a result — the same two lines + `packages/react` has carried since objectui#4538 — so the property is enforced by + the compiler on the consumer side rather than by review. `packages/app-shell` + does not: it type-checks clean without the pin and still shows 23 errors with it, + none of them from these two packages. That residue is filed separately. +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/layout/package.json b/packages/layout/package.json index 6581d4f15..45bd6519c 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/layout", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 4161e7dc5..8bc31c64d 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,5 +1,22 @@ # @object-ui/mobile +## 17.7.0 + +### Patch Changes + +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 11bf8a8e9..caf6c9ba7 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/mobile", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Mobile optimization for Object UI with responsive components, PWA support, and touch gesture handling.", diff --git a/packages/permissions/CHANGELOG.md b/packages/permissions/CHANGELOG.md index 70e748b38..49230fbeb 100644 --- a/packages/permissions/CHANGELOG.md +++ b/packages/permissions/CHANGELOG.md @@ -1,5 +1,26 @@ # @object-ui/permissions +## 17.7.0 + +### Minor Changes + +- 3c9fca3: Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. + +### Patch Changes + +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/permissions/package.json b/packages/permissions/package.json index 5c457f527..9ba91d28c 100644 --- a/packages/permissions/package.json +++ b/packages/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/permissions", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "RBAC permission system for Object UI with object/field/row-level access control, permission guards, and hooks.", diff --git a/packages/plugin-ai/CHANGELOG.md b/packages/plugin-ai/CHANGELOG.md index 028227253..d10cd5642 100644 --- a/packages/plugin-ai/CHANGELOG.md +++ b/packages/plugin-ai/CHANGELOG.md @@ -1,5 +1,47 @@ # @object-ui/plugin-ai +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-ai/package.json b/packages/plugin-ai/package.json index c925294af..77323afe9 100644 --- a/packages/plugin-ai/package.json +++ b/packages/plugin-ai/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-ai", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/packages/plugin-calendar/CHANGELOG.md b/packages/plugin-calendar/CHANGELOG.md index 428938088..be5887a14 100644 --- a/packages/plugin-calendar/CHANGELOG.md +++ b/packages/plugin-calendar/CHANGELOG.md @@ -1,5 +1,90 @@ # @object-ui/plugin-calendar +## 17.7.0 + +### Patch Changes + +- 76ae729: `README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema` + that does not exist. Measured against the interface itself + (`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's + only required key besides `type` — was published as `events?`, so a reader + following the README omits it and TypeScript rejects the node; `defaultDate` was + `string` where the schema says `string | Date`; and `onDateClick` was listed as a + schema key when it is a `CalendarViewProps` **component** prop, sending readers + to a different package's surface for a key `calendar-view` does not have (the + schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys + with nothing saying it was a summary (objectui#5045). + + The block now carries the requiredness the schema declares, names itself a + partial summary of `CalendarViewSchema`, and adds the author-facing + `defaultView` / `view` / `views` / `editable` / `date`. It also states plainly + what the registered `calendar-view` renderer actually reads — it builds events + from the node's `data` array and drops an authored `events` key (objectui#4433) — + so the corrected requiredness does not itself become a new wrong instruction. + + This is a documentation fix to a file `plugin-calendar` publishes to npm, which + is why it carries a version: the npm landing page only picks up the correction + on a release. No behaviour, export, type, or `dist` byte changes. The pin test + added alongside it publishes nothing. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [0068348] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-calendar/package.json b/packages/plugin-calendar/package.json index b2b8d67e1..5bec20d43 100644 --- a/packages/plugin-calendar/package.json +++ b/packages/plugin-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-calendar", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Calendar view plugins for Object UI - includes both ObjectQL-integrated and standalone calendar components", diff --git a/packages/plugin-charts/CHANGELOG.md b/packages/plugin-charts/CHANGELOG.md index f8807b8da..6c4649dbc 100644 --- a/packages/plugin-charts/CHANGELOG.md +++ b/packages/plugin-charts/CHANGELOG.md @@ -1,5 +1,144 @@ # @object-ui/plugin-charts +## 17.7.0 + +### Minor Changes + +- 6f017e9: Dashboard chart widgets no longer render as a blank area when their height class + resolves to `auto`. + + `ChartContainer`'s min-size fallback was applied only to the wrapper `div`. + Recharts measures its own `width:100%;height:100%` size-detector element, and a + percentage height never resolves against an ancestor's `min-height`, so the + wrapper obediently grew to 280px while the measured element stayed at 0 — and + Recharts renders no children at all for a non-positive box. The result was a + widget card with its title over an empty chart area: no marks, no refusal, no + empty state, and permanent, because a box that never changes fires no resize. + The floor is now applied to the measured element as well, under the same + condition, so an author's explicit height still wins. + +### Patch Changes + +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- 6c5ee71: `ObjectChart` now depends on the `fieldOptionLabel` resolver directly instead of + holding it behind a ref, so a chart re-resolves its groupBy option labels when + the resolver genuinely changes (objectui#5587). + + The ref existed for a reason that no longer holds. `useSafeFieldLabel()` returned + a fresh object on every render outside an i18next provider, so a direct + dependency made `fetchData`'s `useCallback` identity fresh on every render, and + the effect that depends on `fetchData` refetched on every render — an unbounded + loop. `ObjectChart` worked around that locally with `fieldOptionLabelRef` plus a + `useEffect` keeping it current. `useObjectLabel`'s memo now holds with or without + an i18next instance bound (objectui#5564), so the resolver's identity is stable + on both paths and the indirection buys nothing. + + It did cost something, and that is the user-visible half: a ref-hidden dependency + meant `fetchData` did NOT re-run when the resolver changed. A chart mounted + before its `I18nProvider`, or rendered across a language switch, kept serving + groupBy labels resolved by the old resolver until some unrelated dependency + (object name, filter, aggregate) happened to move. It now refetches once on that + transition and shows labels in the active language. + + Pinned by `ObjectChart.fieldOptionLabelRefetch.test.tsx`, which counts fetches + across forced re-renders both outside and inside a provider. Reverting + `useObjectLabel.ts` to its pre-objectui#5564 state turns the no-provider case red + (2 fetches instead of 1, alongside React's "Maximum update depth exceeded"), so + the removal is pinned to the fix that unlocked it rather than to a comment. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-charts/package.json b/packages/plugin-charts/package.json index ef57547a3..f2ef1019e 100644 --- a/packages/plugin-charts/package.json +++ b/packages/plugin-charts/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-charts", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Chart components plugin for Object UI, powered by Recharts", diff --git a/packages/plugin-chatbot/CHANGELOG.md b/packages/plugin-chatbot/CHANGELOG.md index 525453d4d..eb5662de9 100644 --- a/packages/plugin-chatbot/CHANGELOG.md +++ b/packages/plugin-chatbot/CHANGELOG.md @@ -1,5 +1,81 @@ # @object-ui/plugin-chatbot +## 17.7.0 + +### Patch Changes + +- a31adc6: `useObjectChat` no longer rebuilds its `DefaultChatTransport` on every render + (objectui#4187). + + The transport `useMemo` listed the caller's `body` and `headers` in its dep list. + Both are object props and every caller passes a fresh literal each render — the AI + page's chat pane builds its `body.context` inline — so the memo never hit and a + transport was constructed on every render of every chat surface, which during a + streaming turn is once per token batch. + + `body` and `headers` are now read through refs inside + `prepareSendMessagesRequest`, the idiom this hook already uses for the live model + (`modelRef`) and the handoff conversation id (`parentConvRef`), and they are gone + from the dep list. Unlike memoizing at each call site, a future caller cannot + undo it. + + No user-visible behaviour changes: `@ai-sdk/react` keeps the transport in a ref + and re-keys its `Chat` only on `chat`/`id` (verified against the installed + 4.0.68), which `useObjectChat` passes neither of, so the message thread was never + at risk — the rebuild was pure waste. The one real difference is *when* the two + values are sampled: a send now reads them at send time, so it observes the values + of the most recent render instead of those of the last render that happened to + rebuild the transport. That is never staler than before, and it is pinned by + `useObjectChat.transportIdentity.test.tsx`. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-chatbot/package.json b/packages/plugin-chatbot/package.json index da3497e4c..04fb3e77f 100644 --- a/packages/plugin-chatbot/package.json +++ b/packages/plugin-chatbot/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-chatbot", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Chatbot interface plugin for Object UI", diff --git a/packages/plugin-dashboard/CHANGELOG.md b/packages/plugin-dashboard/CHANGELOG.md index 0f5ae3c43..ffe459145 100644 --- a/packages/plugin-dashboard/CHANGELOG.md +++ b/packages/plugin-dashboard/CHANGELOG.md @@ -1,5 +1,326 @@ # @object-ui/plugin-dashboard +## 17.7.0 + +### Minor Changes + +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. + +### Patch Changes + +- 84d2e98: `DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained + (card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally + (objectui#4620). + + `isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the + `isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The + `datasetBound` side of that inner fork could never execute: reaching it required + `isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged — + the removed arm never ran, and the reachable fork in the Card branch (the one that gives + a dataset-bound metric its title and border chrome) is untouched. + + The cost was to readers, not to users: the shape read as "both branches handle + dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto + `DashboardGridLayout` had to pay for the reachability argument before it could decline to + copy the dead limb. A comment now names the invariant in place so the arm is not re-added. +- f24195a: `ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for + "no rows yet" instead of a fresh array literal per render (objectui#4629). + + Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so + whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind` + path that resolves to an object — the fallback produced a NEW array identity on + every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so + every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the + `isSystemField` pass, the `fieldLabel` lookups) and then discarded by the + `finalData.length === 0` early return. In `ObjectPivotTable` the value is handed + straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot + rebuilt its row/column sets, bucket map and totals on every render over no rows + at all. + + Nothing rendered wrong before or after; this is wasted work in the empty window, + plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is + the same module-scope frozen empty `data-table.tsx` adopted for its own + `EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings. +- 56f4e34: A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does. + + `ObjectDataTable` derives headers on two paths — from the author's declared + `columns`, and from the object schema when no columns were declared. The + declared path (and the static `data-table` half of the same widget family) + already used `humanizeFieldKey`, whose docstring names it "the single home for + the convention, because both halves of the `table` widget family need it and + they must agree". The auto-derived path carried a third, inline spelling that + split camelCase but never turned `_` into a space, so it left a raw underscore + on screen. Measured over the same object's columns: + + ``` + path close_date needs_analysis + object-bound, AUTO-DERIVED (before) Close_date Needs_analysis + object-bound, AUTO-DERIVED (after) Close Date Needs Analysis + object-bound, DECLARED columns Close Date Needs Analysis + static `data-table`, no columns Close Date Needs Analysis + ``` + + One dashboard can hold all three widgets over one object, so a single field key + rendered under two spellings — the defect class objectui#5425 rules out. The odd + path adopts the shared convention rather than the convention gaining a fourth + dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and + after — the coincidence that kept the snake_case divergence unnoticed), and a + translated header still wins: only the fallback handed to `fieldLabel` changed. + + Dimension MEMBER labels are untouched by this. The same card reported dashboard + members rendering a prettified enum instead of the picklist's translated label, + measured on 17.1.0; re-measured on this branch it no longer reproduces — the + analytics label net shipped in 17.5.0 routes every non-metric dataset dimension + through the field's declared options and the locale bundle. That behaviour had + no test stated in the card's terms and now has one, over the four dashboards the + card measured, including the property that a bar axis and a pivot header cannot + disagree about one stored value. +- f1c27f0: Dashboard record fields: percent columns now render through the one percent + scaling decision instead of a second, drifted copy of it. + + `renderFieldValue`'s `%`-format branch normalised the value itself before + calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`, + then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`, + which `@object-ui/core` documents as the single source of truth for percent + display scaling, so the branch was re-deciding what core owns — and its copy had + drifted from it in three measured ways: + + - `(value / 100) * 100` is not value-preserving in binary floating point, + re-introducing one call frame upstream the round trip that was removed from + inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000 + values change bit pattern and 1,108 rendered strings move, every one a + last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is + `1.61%`. + - A stored fraction below `0.01` was scaled twice — the local `* 100` put it + back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered + `50.00%`. + - The local test was `value > 1` rather than core's symmetric `|value| < 1`, so + a negative already in percentage points took the fraction arm: `-5` rendered + `-500.00%`. + + The branch now hands the raw stored value to `formatPercent` — the identical + call the list-view percent cell already makes for an ordinary percent column — + so a percent reads the same as a record field, as a grid cell and as a dashboard + measure. Output moves where it was wrong: values at or above 1 whose round trip + lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly + `1`, which is one percentage point by core's convention and now renders + `1.00%` at two decimals, where the local `value > 1` test had made it + `100.00%`. +- 0ccbdc1: `PivotTable` no longer re-runs its cross-tabulation memo on every render when it + has no rows (objectui#5562). + + The component spelled the empty array twice — as the destructuring default for + `schema.data` and as the `Array.isArray` fallback that keeps a provider-config + object out of iteration — so a schema declaring no `data` key, or one whose + `data` is a provider config rather than rows, produced a fresh array identity on + every render. That value is the first entry of the memo's dependency list, so + the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the + aggregated matrix and the row/column/grand totals on every render, over nothing. + Both spellings now resolve to one module-scope frozen empty, so "no rows" is a + stable value and the memo holds. + + Wasted work only: the churn feeds a memo rather than a `setState`, and + `PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no + render loop was possible. The identical fix landed for `data-table` in + objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the + direct-use path those two did not cover, where `DashboardRenderer` and + `DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the + chain. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. + + The dashboard widget config panel carried a private `resolveLabel` documented as + resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference + form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline + per-locale map `I18nLabelSchema` actually admits has neither limb, so + `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy + of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, + `MetricWidget` and `MetricCard`. + + This was not a display bug. The resolved value seeds the panel's editable draft, + so a widget whose stored title was a map opened with an **empty** Title field and + the next save wrote `''` over the author's map — on the ordinary path, not an + exotic one: open the widget, change anything, save. + + Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: + + - **Reading** goes through `pickLocalized(value, language)`, so the panel shows + the active locale like every sibling surface post-objectui#4032. + - **Writing** replaces only the active locale's entry and carries every other + locale across. A title the author never touched round-trips the stored object + itself through an unrelated config edit; an edited one merges into the entry + that was displayed. The live-update callback (`onFieldChange`) forwards the + merged map for the same reason — hosts feed it back into the widget the panel + re-opens from, so a bare string there dropped the map before a save ever ran. + + `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side + inverse of `pickLocalized`, so the rule is stated once instead of re-derived per + panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, + region-qualified sibling — and deliberately stops there: the `default` / `en` / + first-value limbs are display fallbacks that hand back *another* locale's string, + and writing to one would let an author editing in `fr` overwrite English. With no + entry for the active locale the edit adds one. The pairing + `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a + write that lands where the read does not look is how a "saved" string disappears. + + A full multi-locale editing UI remains out of scope (objectui#4163). +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index 0c866f67b..9b58ef1ee 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-dashboard", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Dashboard plugin for Object UI", diff --git a/packages/plugin-designer/CHANGELOG.md b/packages/plugin-designer/CHANGELOG.md index 38c0b0fee..6b084d859 100644 --- a/packages/plugin-designer/CHANGELOG.md +++ b/packages/plugin-designer/CHANGELOG.md @@ -1,5 +1,117 @@ # @object-ui/plugin-designer +## 17.7.0 + +### Patch Changes + +- 4db5989: A widget title stored as an inline per-locale map is editable again in both dashboard + authoring surfaces, and a save writes back only the active locale's entry + (objectui#5428). + + `@objectstack/spec` widened `I18nLabel` from `string` to `string | Record` at + 17.0.0-rc.6, so a stored widget title may be an inline per-locale map while both + authoring panels edit a title in ONE single-line input. Writing the input's value back + as the whole value would collapse every other locale on the first keystroke, so both + surfaces took the same conservative branch: show a map-valued title resolved, and make + it READ-ONLY. + + That branch could not lose data, but it rested on a premise the spec had already + invalidated — "nothing can reach this path from stored metadata yet, `I18nLabel` was + plain `string` through rc.5" — stated sixty lines below a comment in the same file + documenting the rc.6 widening that makes a stored map reachable. Both could not hold. + The pinned spec is 17.0.0. What the read-only branch did in practice from rc.6 onward + was not protect an unreachable path: it denied an author the ability to edit a widget + title in their own locale. + + objectui#5301's maintainer ruling settled the write rule for the sibling surface — a + save replaces only the active locale's entry and preserves the others — and + `@object-ui/i18n` ships it as `setLocalized`, co-located with `pickLocalized` because + the read and the write have to agree. Both panels now adopt it: + + - `@object-ui/plugin-designer`'s `DashboardEditor` widget property panel; + - `@object-ui/app-shell`'s `DashboardWidgetInspector` in metadata-admin. + + A plain-string title keeps saving as a plain string, so the common path is unchanged. + An edit made in a locale the stored map does not carry ADDS an entry under that locale + rather than overwriting the entry the display fell back to. + + The pins are preservation pins, not "the input is editable" pins: at both surfaces a + keystroke on a map-valued title must leave every other locale's entry byte-identical. + Reverse-verified by mutating each write back to the flattening form and confirming those + assertions go red at both surfaces. + + Not a multi-locale editor: an author still reaches only the entry for the locale they + are in. Authoring every locale from one panel remains an open product question. The + stale deferrals both comments carried pointed at objectui#4163, which closed as + completed on 2026-08-15 while the placeholders were still in the tree; they are replaced + with the rule that is actually in force rather than re-pointed at another tracker. +- Updated dependencies [3b147a3] +- Updated dependencies [b2e85a9] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [83ec618] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [26a2238] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/data-objectstack@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-form@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index 905734071..5ce5762e7 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-designer", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Visual designer plugin for Object UI with page, data model, process, and report designers plus collaborative editing.", diff --git a/packages/plugin-detail/CHANGELOG.md b/packages/plugin-detail/CHANGELOG.md index 1076d9b13..feb6d2d46 100644 --- a/packages/plugin-detail/CHANGELOG.md +++ b/packages/plugin-detail/CHANGELOG.md @@ -1,5 +1,265 @@ # @object-ui/plugin-detail +## 17.7.0 + +### Minor Changes + +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- 8a44390: `record:alert` binds the row through `usePredicateRecordContext`, so an + author-declared `properties.visible` is actually consulted. + + `renderers/record-alert.tsx` was the last predicate face in the repo still + handing `useCondition` a root-only `{ record }` bag. Every other row-scoped + predicate — the four generic action renderers (objectui#4075) and app-shell's + `DeclaredActionsBar` (objectui#4077) — binds the row through the shared + `usePredicateRecordContext(record)` helper, which resolves the three spellings + objectui#5330 ruled on: canonical `record.status`, the deprecated row-action + shorthand `status`, and deprecated legacy `data.status`. + + Under the root-only bag only the canonical spelling worked, and the two others + failed in **opposite** directions — both of them silently, because this call + site is fail-soft: + + - **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator + threw. The legacy `${…}` path answers a throw with its own source text, a + non-empty and therefore truthy string, so the verdict was **SHOWN on every + row**. A banner the author had gated was permanently on screen. + - **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's + ambient predicate scope (`providers/ExpressionProvider.tsx`) carries + `data: {}`, so the predicate read that object instead of the row, compared + `undefined`, and the verdict was a constant false — **never shown**. + + **Behaviour change, stated plainly:** a shipped `record:alert` whose `visible` + was written in either deprecated spelling was inert and is now live. A banner + that was permanently visible may begin to hide, and one that never appeared may + begin to show — that is the point of the fix, but it is a verdict change rather + than a no-op. Canonical `record.*` predicates are unaffected in verdict: they + resolved before and resolve now, pinned on both polarities. An in-tree census + found no `record:alert` `visible` predicate outside this package's own tests. + + A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`, + with its own deliberate bindings (`data` is the data-source adapter there, not + the row). This change does not touch it; the two still compose as AND. + + The renderer's header comment described the shared-scope behaviour it did not + have. It now describes what the file does, including the fail-soft policy and + the two-gate composition. + +### Patch Changes + +- 0068348: `record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is + widened to `string | I18nLabel` in both copies (`properties.*` and the flat + compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. + + The renderer already resolves `action.label` through the same inline-locale-map + `pickLocalized` call as `title` / `body` (`const ctaLabel = + pickLocalized(props.action?.label, language)`), so a bare `string` declaration + was narrower than the renderer's own runtime behavior — the same + declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for + `title` / `body` one level up in the same interface (objectui#4998). + + Type-only: the block's published authoring surface still declares `action` as + a bare `object` with the member shape in prose only + (`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — + that half stays parked on the `ComponentInput` member-shape question (PR + #3795) and is out of scope here. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index 071e61823..25994af3b 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-detail", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "DetailView plugin for Object UI - comprehensive detail page with sections, tabs, and related lists", diff --git a/packages/plugin-editor/CHANGELOG.md b/packages/plugin-editor/CHANGELOG.md index 9aca54cdc..f1b82ae11 100644 --- a/packages/plugin-editor/CHANGELOG.md +++ b/packages/plugin-editor/CHANGELOG.md @@ -1,5 +1,47 @@ # @object-ui/plugin-editor +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-editor/package.json b/packages/plugin-editor/package.json index 0ac4c314e..0015b4620 100644 --- a/packages/plugin-editor/package.json +++ b/packages/plugin-editor/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-editor", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Rich text editor plugin for Object UI, powered by Monaco Editor", diff --git a/packages/plugin-form/CHANGELOG.md b/packages/plugin-form/CHANGELOG.md index 8907c722c..395f47368 100644 --- a/packages/plugin-form/CHANGELOG.md +++ b/packages/plugin-form/CHANGELOG.md @@ -1,5 +1,145 @@ # @object-ui/plugin-form +## 17.7.0 + +### Minor Changes + +- 3c9fca3: Create forms pre-fill the `current_user` defaultValue token with the acting user (#5683). `PermissionContextValue` gains `userId` (from `/me/permissions`; `null` = unknown), and the create-form seeding resolves `defaultValue: 'current_user'` on `user` / `lookup→sys_user` fields to that id — the same value the engine stamps at insert, so the pre-fill is a preview of the server's own resolution, not a second default contract. Unknown user (no provider / anonymous / role-based provider) seeds nothing and keeps the omit-and-let-the-engine-resolve behavior. `NOW()` and CEL defaults stay server-owned. +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 83ec618: `README.md`'s "Not a `FormField` key" table said a field-level `className` is + "read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier + holds only for the renderer's *explicit* read — `className={fp.className}` on + the `section-divider` branch of + `packages/components/src/renderers/form/form.tsx`. The same renderer forwards + every key it did not destructure, and `className` is not among the names taken + off the field config, not among the ones `stripRendererOnlyProps` removes, and + so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input` + branch spreads it onto ``. A field-level `className` therefore lands + visibly on ordinary built-in controls, and a reader taking "exactly one" + literally concludes the opposite of what the code does (objectui#5131). + + The cell now describes the contract rather than the reader count: an undeclared + key still rides the props spread down to whichever component the field resolves + to, nothing in the contract promises that, and a registered widget honours it + only if it happens to spread its leftover props — the wording the docs site + already ships, so the two sources agree again. The advice in the row is + unchanged and was never wrong (`span` / `colSpan` for width, + `FormSchema.fieldContainerClass` for the grid), and the explicit + `section-divider` read is kept, now named as explicit. + + This is a documentation fix to a file `plugin-form` publishes to npm, which is + why it carries a version: the npm landing page only picks up the correction on a + release. No behaviour, export, type, or `dist` byte changes. +- 26a2238: `navigateOnSuccess` now honours a mounted host, and says so when its destination is refused + + `ObjectForm` and `WizardForm` consume `navigateOnSuccess` through + `resolveSuccessNavigate`, and both arms travelled to an accepted destination with a bare + `window.location.assign`. A rooted path such as `/apps/x/o/record/{id}` assigned that way + resolves against the ORIGIN root, so under a host mounted at a sub-path (the framework CLI + configures one for every embedded deployment) an authored in-app destination left the + application. Both arms now route an app-relative destination through the injected + navigation seam both components already held for `submitBehavior.url`, so a mounted host's + basename is applied. With no host seam the behaviour is byte-for-byte what it was — a host + with no router has no basename, so origin-rooted resolution is already correct there. A + same-origin ABSOLUTE destination also keeps browser-level navigation: the seam's declared + input is an application-relative path, and an author who spelled out a whole address asked + for that address. + + A declared `navigateOnSuccess` whose destination is refused — a mistyped value, or a written + record carrying no usable id — used to produce a success toast identical to the one a form + with no `navigateOnSuccess` produces, so the navigation failed with nobody told. That toast + now carries a note that the declared navigation did not happen, and the template the author + wrote is logged for them. The write genuinely succeeded, so this stays a success rather than + becoming an error state. + + Which destinations are ACCEPTED is unchanged: the same-origin guard, the `{id}` / + `{recordId}` dialect and the unescaped interpolation are the subject of an open contract + question and are deliberately untouched here. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index c27f526da..c02879638 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-form", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Form plugin for Object UI", diff --git a/packages/plugin-gantt/CHANGELOG.md b/packages/plugin-gantt/CHANGELOG.md index 88b03a315..009a4a7ae 100644 --- a/packages/plugin-gantt/CHANGELOG.md +++ b/packages/plugin-gantt/CHANGELOG.md @@ -1,5 +1,67 @@ # @object-ui/plugin-gantt +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [0068348] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index 6ef13f936..83ca6a4ab 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-gantt", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Gantt chart plugin for Object UI", diff --git a/packages/plugin-grid/CHANGELOG.md b/packages/plugin-grid/CHANGELOG.md index 06a5f2e8d..bddf6a0c2 100644 --- a/packages/plugin-grid/CHANGELOG.md +++ b/packages/plugin-grid/CHANGELOG.md @@ -1,5 +1,199 @@ # @object-ui/plugin-grid +## 17.7.0 + +### Minor Changes + +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 9e22085: `ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of + byte-copying it onto `$filter` (objectui#4082). + + The query assembly already lowered the canonical `filter` key through `toFilterNode` — + the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch + beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the + one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter` + and `plugin-view`'s non-grid fetch both already route the same value through + `toFilterNode` / `mergeFilterNodes`. + + Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is + declared `Record` (the MongoDB-style shape) and `isFilterAST` is false for a + plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one + answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. + + `toFilterNode` handles both without new logic: objects route through + `convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot + passes through untouched, so nothing is lowered twice. It also folds an absent or empty + source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` + used to send `$filter: {}`, asking the server a question with no content in a shape it + refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. + + **Grade — this is less dormant than the card assumed.** objectui#4082 was filed + observation-class on "no measured producer", reasoning that `defaultFilters` is not in + `object-grid`'s registered `inputs` so an author writing it only draws a save-gate + warning. That reasoning covers authors, but not the framework: `plugin-view`'s + `ObjectView` writes the slot itself, forwarding an active named view's `filter` as + `defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — + and `plugin-view`'s own README documents `listViews..filter` as + `[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as + `400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no + `renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, + and `ListView`'s lowering does not cover it. So a schema-registration host — the + documented authoring path — reached the raw assignment whenever an active named view + carried a filter. Not asserted here: a failing request captured against a running + deployment. `app-shell` is unaffected either way; it supplies `renderListView` and + delegates to `ListView`, which lowers. + + Not in scope, and deliberately not done: retiring `defaultFilters`. This is + consumer-side only — the key the schema admits is unchanged, and its precedence behind + the canonical `filter` is unchanged. + + The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the + measurements. +- c574dfb: `ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence. + + objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so + `ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer + reads. That was right — the spec refuses `accessorKey` and `header` by name, and the + census found zero authored usages. But it relocated a failure mode instead of removing + it: a column authored in a spelling the renderer does not read contributed nothing, and + nothing said so. No error, no warning, no empty state — the author got a grid with its + row-number column and no data columns, which is a success receipt for a disagreement + between the renderer and the author. + + An authored column that can never resolve now emits one `console.warn` naming the + address rather than the symptom: which block (`object-grid` or the `view:grid` alias), + which object and label, which `columns[i]`, the keys that entry actually carries, and the + rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }` + the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped + it" (the export-format warning), rather than adding a second differently-shaped one. + + Rendering is unchanged in every case: this is additive. The diagnostic reads the + `columns` input and nothing else — it never asks whether the grid found rows, because + `object-grid` legitimately draws them from five different places (a bare `data` array, + `data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and + passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView` + does). All five are pinned by test, in both directions. A `hidden: true` column is + authored intent and is never reported, and so are the arms that legitimately produce no + columns of their own: no `columns` key, an empty `columns` array, and the `string[]` + spelling. + + A throw was rejected: a grid that renders nothing today would become a page that renders + nothing. +- 02f48b6: `object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. + + `userActions` names two different blocks. On a **view** it is toolbar policy — + the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, + `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by + name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / + `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is + the only shape `listViewPredicates` can read, since its loop skips every + non-object value. + + `ObjectGrid` read the key view-first when building the `$select` projection + (`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a + perfectly legal toolbar block therefore shadowed the object's CRUD predicates, + the harvest found none, and the predicate's operand left the projection. CEL then + faults on the absent key, fails closed, and the row Edit/Delete button disappears + for everyone with nothing pointing at the projection — objectui#3501's failure, + reached with a success receipt at every step. + + The view-level block is not hypothetical: `SpecBridge.transformListView` copies + it onto the `object-grid` node the renderer receives, and `app-shell`'s + `ObjectView` builds one unconditionally. + + The harvest now reads the resolved object block only. Both `userActions` read + sites carry a comment naming the collision, and + `__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes, + the producer that writes the view one, the harvest's blindness to it, and the + projection that must keep the object's operand with a toolbar block present. + + Toolbar policy itself is untouched — it was never read through this path. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-grid/package.json b/packages/plugin-grid/package.json index 7883e0cd2..a533e1ccd 100644 --- a/packages/plugin-grid/package.json +++ b/packages/plugin-grid/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-grid", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Grid plugin for Object UI", diff --git a/packages/plugin-kanban/CHANGELOG.md b/packages/plugin-kanban/CHANGELOG.md index 055d64165..49f8058d6 100644 --- a/packages/plugin-kanban/CHANGELOG.md +++ b/packages/plugin-kanban/CHANGELOG.md @@ -1,5 +1,67 @@ # @object-ui/plugin-kanban +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [0068348] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-detail@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json index d30f7bbf5..4ab67722f 100644 --- a/packages/plugin-kanban/package.json +++ b/packages/plugin-kanban/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-kanban", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Kanban board plugin for Object UI, powered by dnd-kit", diff --git a/packages/plugin-list/CHANGELOG.md b/packages/plugin-list/CHANGELOG.md index b539389c4..ff8491dfd 100644 --- a/packages/plugin-list/CHANGELOG.md +++ b/packages/plugin-list/CHANGELOG.md @@ -1,5 +1,264 @@ # @object-ui/plugin-list +## 17.7.0 + +### Minor Changes + +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 15236e0: `list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. + + `userActions` names two different blocks. On a **view** it is toolbar policy — + the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, + `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by + name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / + `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is + the only shape `listViewPredicates` can read, since its loop skips every + non-object value. + + `ListView` read the key view-first when building the `$select` projection + (`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view + carrying a perfectly legal toolbar block therefore shadowed the object's CRUD + predicates, the harvest found none, and the predicate's operand left the + projection. CEL then faults on the absent key, fails closed, and the row + Edit/Delete button disappears for everyone with nothing pointing at the + projection — objectui#3501's failure, reached with a success receipt at every + step. + + This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and + it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level + `userActions` it hands down as an object literal of two spreads, so the left + operand was `{}` at worst — never nullish. The `??` never fell through, and the + object's CRUD predicates were never consumed at all on that path, whether or + not an author wrote any toolbar policy. + + The harvest now reads the object block only. Both `userActions` read sites in + `ListView.tsx` carry a comment naming the collision, and + `__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the + two shapes, a producer that manufactures the view one, the harvest's blindness + to it, and the projection that must keep the object's operand with a toolbar + block — or an empty block — present on the view. + + Toolbar policy itself is untouched — it was never read through this path. +- ec9fdaa: `ListView` now resolves the nested `aria.ariaLabel` against the audience's locale + instead of casting it to a string (objectui#5134). + + `@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain + string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only + read site in this repo spread it into the DOM as + `{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a + conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which + a screen reader announces as the list view's accessible name — in every locale. The + read now goes through the spec's own `resolveI18nLabel` (the resolver four other + in-repo read sites already use) against `useDisplayLocale()`. + + Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a + plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label + predates rc.6, and no measured author writes one today — but map values are legitimate + and arrive via API/import, so an imported list view carrying + `aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong + accessible name. This is the map form working as declared, not a defect users are + currently hitting. + + Behaviour on the string arm is byte-identical, including `''` (falsy before and after, + so no attribute). One edge changes for the better: a map that matches no locale used to + render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an + unnamed region beats a garbage-named one. + + The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different + vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by + `SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's + shape. +- d6613a2: Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime + behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does + change, which is why this is a `patch` rather than an empty frontmatter. + + **objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment + block above the `sortFields` memo still called a formula field "the supported + alternative (… which sorts like any text column)". Since objectui#4294 the + `list.sortRelationalHint` string in this same file says the opposite ("Not a formula + field: it is virtual, so no column is stored for it and the server refuses to sort by + one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the + server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical + now names the remedy the hint, the server's refusal and the README already share — a + stored field that denormalizes the name onto this object, written when the source + changes. This was the last copy of the retired advice in the repo. + + **objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.** + It had drifted two declarations up, so the exported `parseSortConfig` carried two + stacked leading comments and the helper carried none. This one was not cosmetic: because + `parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into + `dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort + parser with a sentence about action labels. Moving the block removes it from the `.d.ts`; + `formatActionLabel` is module-private, so its now-correct docblock does not appear there. + It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the + comment block *attached* to a declaration — a misattributed docblock is the mechanism by + which a claim gets scored against the wrong symbol. This block carries no spec-alignment + phrase, so nothing fired today. + + No tests accompany this change and none could: both edits are comment-only, and there is + no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real + `vite build` before and after, not asserted. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/permissions@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-list/package.json b/packages/plugin-list/package.json index 3eb263d6b..9f5b69220 100644 --- a/packages/plugin-list/package.json +++ b/packages/plugin-list/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-list", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "ListView plugin for Object UI - unified view component with view type switching", diff --git a/packages/plugin-map/CHANGELOG.md b/packages/plugin-map/CHANGELOG.md index e03920fb6..cdded96bf 100644 --- a/packages/plugin-map/CHANGELOG.md +++ b/packages/plugin-map/CHANGELOG.md @@ -1,5 +1,119 @@ # @object-ui/plugin-map +## 17.7.0 + +### Patch Changes + +- ff7543c: `ObjectMapProps.clusterRadius`'s JSDoc said "in pixels"; `clusterMarkers` has + always used it as a coordinate-degree grid cell edge (`radius / 2 ** zoom`, + divided into the marker's `[lng, lat]` degrees), not a screen-space radius — + a host tuning clustering granularity by the documented unit would get a + completely different result than intended (objectui#5020). + + No behavior, default, or name changes: clustering, the >100-visible-marker + auto-threshold, and tap-through zoom are unaffected, and `clusterRadius` has + no call sites outside `plugin-map/src` today (re-confirmed repo-wide, + including `apps/`, `examples/`, and the `objectstack` spec/server repo — the + default of `50` is what runs everywhere). This is a doc-comment correction + only, bringing the JSDoc in line with the README's already-correct wording + (post objectui#5002). + + The latitude-anisotropy trade-off (a degree grid distorts east-west as + latitude rises) is a known design trade-off, not part of this fix. +- b180a64: `ObjectMap` reads `schema.data` in one place again, so an array-shorthand map stops + making a metadata request it never uses. + + The fetch effect carried a second short-circuit beside the `props.data` one + objectui#5003 fixed: it read `schema.data` directly and tested whether that value was + itself an array. eslint reported it as `missing dependency: schema.data` — the last + `react-hooks/exhaustive-deps` warning on that effect. + + The dependency was never actually missing. `getDataConfig(schema)` already returns + `schema.data` verbatim, and the result is memoized on `JSON.stringify(rawDataConfig)` + into `dataConfig`, which **is** one of the effect's declared dependencies. The authored + rows therefore reached the effect before this change; the direct read was a duplicate of + an already-threaded value, which is why neither adding a dependency nor deleting the + branch was right. + + The array handling moved into `getDataConfig`, where `ObjectGrid`'s own `getDataConfig` + already pins the same normalization (`"Check if data is an array (shorthand format)"`). + Same rows render, and the effect now reads only `dataConfig`. + + One behavioural consequence, and it is the point: an array under `data` now yields + `provider: 'value'`, so `hasInlineData` is true and the sibling effect no longer calls + `dataSource.getObjectSchema()` for it. That request's only read site is + `buildExpandFields()` inside the object-provider fetch branch, which an inline schema + never reaches — so the call was pure waste, and the shorthand now behaves exactly like + the declared `{ provider: 'value', items }` form it is shorthand for. + + Deleting the branch instead was measured, not assumed: with no producer-side handling, + an array-shorthand map renders `Error: DataSource required for object/api providers` + rather than its markers. The shorthand is a live convention in six sibling blocks + (`ObjectGrid`, `ListView`, `ObjectTree`, `ObjectChart`, `ObjectDataTable`, + `calendar-view-renderer`), so `object-map` would have become the one block in the family + that answers it with an error box. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index c4d6e79a5..e834a1987 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-map", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Map visualization plugin for Object UI", diff --git a/packages/plugin-markdown/CHANGELOG.md b/packages/plugin-markdown/CHANGELOG.md index 71b410866..808375dfc 100644 --- a/packages/plugin-markdown/CHANGELOG.md +++ b/packages/plugin-markdown/CHANGELOG.md @@ -1,5 +1,47 @@ # @object-ui/plugin-markdown +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-markdown/package.json b/packages/plugin-markdown/package.json index 85553a5af..9bf6bbca1 100644 --- a/packages/plugin-markdown/package.json +++ b/packages/plugin-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-markdown", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Markdown rendering plugin for Object UI, powered by react-markdown", diff --git a/packages/plugin-report/CHANGELOG.md b/packages/plugin-report/CHANGELOG.md index d46584102..b67b6b130 100644 --- a/packages/plugin-report/CHANGELOG.md +++ b/packages/plugin-report/CHANGELOG.md @@ -1,5 +1,68 @@ # @object-ui/plugin-report +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/fields@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-report/package.json b/packages/plugin-report/package.json index e138a6354..c3ead0a02 100644 --- a/packages/plugin-report/package.json +++ b/packages/plugin-report/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-report", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/packages/plugin-timeline/CHANGELOG.md b/packages/plugin-timeline/CHANGELOG.md index 1752c39e3..3e6c9162e 100644 --- a/packages/plugin-timeline/CHANGELOG.md +++ b/packages/plugin-timeline/CHANGELOG.md @@ -1,5 +1,59 @@ # @object-ui/plugin-timeline +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/mobile@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index 4bb697caf..855d50f54 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-timeline", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Timeline component plugin for Object UI", diff --git a/packages/plugin-tree/CHANGELOG.md b/packages/plugin-tree/CHANGELOG.md index 70c8973ff..ef2ea6059 100644 --- a/packages/plugin-tree/CHANGELOG.md +++ b/packages/plugin-tree/CHANGELOG.md @@ -1,5 +1,58 @@ # @object-ui/plugin-tree +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index 3efeb26d0..579ff23a7 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-tree", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Tree / tree-grid visualization plugin for Object UI", diff --git a/packages/plugin-view/CHANGELOG.md b/packages/plugin-view/CHANGELOG.md index e0af743c3..4be72081d 100644 --- a/packages/plugin-view/CHANGELOG.md +++ b/packages/plugin-view/CHANGELOG.md @@ -1,5 +1,190 @@ # @object-ui/plugin-view +## 17.7.0 + +### Minor Changes + +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- cad512f: A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s + view switcher instead of the grid one, and the `tree` / `chart` view types are + recorded as host-composition-only surfaces (objectui#5321). + + `viewSwitcherSchema`'s `iconMap` carried an entry for every view type except + `tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn + with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by + adding a single key — nothing recorded that the map had to be COMPLETE, so the + next missing member went unnoticed. The map is now typed + `Record`, which is how `ViewSwitcher`'s own + `DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: + a future `ViewType` member fails `type-check` rather than silently rendering as + a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph + `DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback + stays for host props that carry an unrecognised type. Reached in practice by + the console, whose `CreateViewDialog` offers `tree` among the view types a user + can create. + + No authoring surface changes. `generateViewSchema` renders eight view types + while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of + them, so `tree` and `chart` are selectable only through the component's `views` + prop. The maintainer ruled on 2026-08-20 that both stay recorded as + host-composition-only rather than being added to those unions, following the + objectui#5097 precedent; the record now lives beside that one, with the branch + set derived from a source fence, the authored unions pinned at the type level, + and host reachability measured. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 6b348d2: `ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both + icon maps in the package now name only spellings lucide still resolves + (objectui#5586). + + `ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up + in lucide's runtime `icons` record. lucide retires a spelling by dropping it + from that record while KEEPING it as a deprecated named export, so a retired + name still imports, still type-checks and still renders as a component — and + silently resolves to nothing as a string. `ObjectView` composes the switcher + from names, and two of them had been retired on lucide-react 1.31.0: + `chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a + label with no icon at all while every sibling type had one, and nothing went red + because no lucide symbol appears in that map for the compiler to check. Measured + against the installed package: `BarChart3` and `GanttChart` are absent from + `icons`, while `ChartColumn` and `ChartGantt` are present. + + - `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`, + `gantt-chart` → `chart-gantt`. + - `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named + deprecated aliases move to the names the record carries — + `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`, + `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the + aliases already pointed at, so those two glyphs are unchanged; the `gantt` + default picks up the plain gantt glyph, which is what `iconMap` now supplies + for that view type. + + The regression pin widens from `tree` alone to EVERY name both maps supply: a + pin scoped to the two names that broke would not have caught this and would not + catch the next lucide bump. +- Updated dependencies [3b147a3] +- Updated dependencies [77f846a] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [485f096] +- Updated dependencies [9e22085] +- Updated dependencies [b655a9d] +- Updated dependencies [c574dfb] +- Updated dependencies [02f48b6] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [83ec618] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [490f482] +- Updated dependencies [26a2238] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-form@17.7.0 + - @object-ui/plugin-grid@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index aedb1d31f..4d5bf4139 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-view", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Object View plugin for Object UI", diff --git a/packages/providers/CHANGELOG.md b/packages/providers/CHANGELOG.md index 06d4ccf64..04ddcedb1 100644 --- a/packages/providers/CHANGELOG.md +++ b/packages/providers/CHANGELOG.md @@ -1,5 +1,22 @@ # @object-ui/providers — Changelog +## 17.7.0 + +### Patch Changes + +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/providers/package.json b/packages/providers/package.json index 8f4c97516..d08ce5dcd 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/providers", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "Reusable context providers for ObjectUI applications", diff --git a/packages/react-runtime/CHANGELOG.md b/packages/react-runtime/CHANGELOG.md index 761adb0e2..c092a21a8 100644 --- a/packages/react-runtime/CHANGELOG.md +++ b/packages/react-runtime/CHANGELOG.md @@ -1,5 +1,55 @@ # @object-ui/react-runtime +## 17.7.0 + +### Patch Changes + +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. + ## 17.6.0 ## 17.5.0 diff --git a/packages/react-runtime/package.json b/packages/react-runtime/package.json index f2f6f79ac..2c748e790 100644 --- a/packages/react-runtime/package.json +++ b/packages/react-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react-runtime", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index c6d1af825..a79d27c01 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,151 @@ # @object-ui/react +## 17.7.0 + +### Minor Changes + +- ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so + + The three object-bound blocks disagreed about how the data-source adapter reached + them. `object-grid` and `object-form` were registered through wrappers that read + it from `SchemaRendererProvider` context; `detail-view` was registered as the raw + component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads + only context, so the two wirings were mutually exclusive: measured with correct + keys in every cell, provider wiring gave the grid `find` 1 and the detail view + `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything. + + All three now resolve the adapter through one rule — an explicit `dataSource` + prop first, the provider context second. This is additive: `detail-view` keeps + its prop form (and direct `` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- c86185e: Bind `record` into the node-level visibility evaluator, and stop a hoisted + `properties.visible` swallowing a declared `visibleWhen`. + + `@objectstack/spec` has declared since ADR-0089 that a page component's + `visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`, + `current_user`, `page.`"*. `SchemaRenderer` bound no `record` at all. Its + evaluator was built from the ambient predicate scope, `data: dataSource` (the + connector **adapter**, not the row) and `page: pageVariables`; the row lives in + `RecordContext`, which that evaluator never read. + + Because the surface is fail-soft, a `record.*` predicate did not misfire — it + resolved to **shown**. Both polarities of the same predicate returned the same + verdict, so a visibility gate silently did not gate, on every block on every + record page. Measured on `record:alert`, `record:path`, `page:card` and + `element:text`. + + Three changes, all in `SchemaRenderer`'s evaluation memo: + + - **`record` is bound**, as the `record` root only — the three roots the + describe promises and nothing more. Not as bare fields, and never over + `data`, which is what `${data.*}` in a props bag resolves against. Bound + conditionally, so "no row" binds nothing rather than shadowing a `record` a + host supplied through the ambient scope. + - **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` + onto the node, so a node carrying `properties.visible` short-circuited the + declared node predicate — the one key the spec tells authors to write was the + one key that could be silently ignored. The two deprecated aliases + (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into + `visibleWhen` at parse, so a spec-parsed page never reaches them. + - **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this + predicate is broken" and "this predicate said yes" with the same word. The + verdict is unchanged on every path — `evaluateCondition` already returned + `true` for every unevaluable predicate, including the non-negated `hidden` / + `hiddenOn` legs where that `true` means HIDE — so only the silence moved. + + **Behaviour change, stated plainly:** a shipped page whose node-level + `record.*` predicate was previously inert now evaluates. A block that was + permanently visible may begin to hide — which is the point, but it is a verdict + change, not a no-op. `properties.visible` is unaffected in verdict: an + in-tree census found **zero** node-level `record.*` predicates on page + components, so nothing in this repository changes verdict. + +### Patch Changes + +- f90b8fb: `toRenderableSchema`'s header now says the bridge is permanent, instead of instructing + callers to remove it (objectui#4622). + + No executable line changes — but the artifact is **not** unchanged, and that is worth + stating plainly rather than rounding to "comment-only". This package builds with plain + `tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc + is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both + what an editor shows on hover at every call site and bytes that ship. + + Measured by building the package the way the repo builds it, at both revisions: + `dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850 + to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19 + differing lines in the emitted file are JSDoc continuations and the three executable lines + are byte-identical, so the growth is the paragraph and nothing else. The trade is + deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old + paragraph's instruction produced once already. + + The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have + not been reconciled" and that "when it lands, the call sites using this can go back to + forwarding directly". Both halves went false when PR #4608 merged, and the second half is + the harmful one: it is an instruction whose trigger condition has now fired, sitting + directly above the function a future author is about to call. + + The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of + `@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring + an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548 + ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So + a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a + permanent crossing between two intentionally different types rather than scaffolding + awaiting a merge. + + Following the old instruction has a measured cost: five `apps/site` call sites were + forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly + five hours until PR #4621 routed all five through this function (objectui#4617). +- Updated dependencies [b2e85a9] +- Updated dependencies [77f846a] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [20e317c] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [38a9568] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [3c73d99] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/data-objectstack@17.7.0 + - @object-ui/i18n@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/react/package.json b/packages/react/package.json index 780971cbe..e1bc72683 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "license": "MIT", "description": "React bindings and SchemaRenderer component for Object UI", diff --git a/packages/runner/CHANGELOG.md b/packages/runner/CHANGELOG.md index 63cb4bab6..8d71352df 100644 --- a/packages/runner/CHANGELOG.md +++ b/packages/runner/CHANGELOG.md @@ -1,5 +1,51 @@ # @object-ui/runner +## 17.7.0 + +### Patch Changes + +- Updated dependencies [3b147a3] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [485f096] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [ebce5a3] +- Updated dependencies [6c5ee71] +- Updated dependencies [6f017e9] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [fa140b8] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/components@17.7.0 + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + - @object-ui/plugin-charts@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/plugin-kanban@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/runner/package.json b/packages/runner/package.json index ac0769d5d..428a21437 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@object-ui/runner", "private": false, - "version": "17.6.0", + "version": "17.7.0", "description": "Universal Object UI Application Runner", "type": "module", "homepage": "https://www.objectui.org/docs/utilities/runner", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 195115f84..9c316f600 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,55 @@ # @object-ui/sdui-parser +## 17.7.0 + +### Patch Changes + +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. + ## 17.6.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 756e4ee6a..1928d43ec 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/sdui-parser", - "version": "17.6.0", + "version": "17.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index ee6986d27..1ccfe5062 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,390 @@ # @object-ui/types +## 17.7.0 + +### Minor Changes + +- d7573b3: `ComponentInput` is now declared once and re-exported, instead of restated in three + places (objectui#4972). + + `@object-ui/core`'s `ComponentInput` (`registry/Registry.ts`) and `@object-ui/types`' + plugin-scoped `ComponentInput` (`plugin-scope.ts`, published as `PluginComponentInput`) + were structural copies of the interface in `@object-ui/types`' `base.ts`. Both are now + re-exports of that one declaration, which is the disposition objectui#4580 ruled for the + identical shape — *a structural copy would reproduce the defect the moment either side + moved* — and the way `core/src/types/index.ts` already handles `SchemaNode`. + + Either side had already moved. `base.ts` declared thirteen keys; both copies declared + nine, so `min` / `max` / `step` / `placeholder` were missing from **the copy every + component registration actually imports**. Those four keys were unwritable at any real + registration — a plain TypeScript error at the call site — while `ComponentInputSchema` + (the zod schema) and `ComponentMeta.inputs` both accepted them. The publication face + advertised four keys the authoring face rejected. Measured over the repository, no + registration had tried to write one yet, so nothing a user hits was broken today; what + changes is that the four keys become writable, and there is no longer a second + declaration for the next widening to miss. + + `ComponentInput`'s arm vocabulary (`ComponentInputControlType`) was already a single + declaration imported by all three sites (objectui#3832); this converges the rest of the + interface. + + Measured, not assumed: `@object-ui/core`'s published entry `dist/index.d.ts` is + byte-identical across the change (sha256 `f6494f80…`, both legs). That gauge is reported + here only with its control — a probe that added a *required* key to `ComponentInput` left + the same file byte-identical, because `dist/index.d.ts` is a 63-line barrel of + `export *` lines that names `ComponentInput` zero times. The gauge that can actually fail + is the emitted declaration file: `dist/registry/Registry.d.ts` changes, as does + `@object-ui/types`' `dist/plugin-scope.d.ts`, and those two files are the *only* emitted + declarations that change in either package. + + `WidgetInput`'s union-arm capability is deliberately untouched — a different gate path + and a separate judgment. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- 78cbdb5: Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer + implemented (objectui#5489). + + `packages/types/src/theme.ts` declared a theme-manager **component** carrying + `themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and + `storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching + Zod object as a member of `ThemeUnionSchema` and therefore of + `AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no + `ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`, + and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS` + (`packages/components/src/renderers/placeholders.tsx`), so it did not even + resolve to a placeholder — a page declaring one got the registry's "Unknown + component type" panel (OBJUI-001) instead of a theme manager. Declared-but- + unenforced, removed under the maintainer ruling of 2026-08-21 on + objectstack#10485 (option B). + + Removed from the published surface: the `ThemeComponentSchema` type + (`@object-ui/types`), the `ThemeComponentSchema` Zod object + (`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the + `'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling + `type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than + accepted and then rendered as an error panel, which is pinned by a test. + + **The theme system is unchanged.** `Theme` (the spec's authoring theme + document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine` + (`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and + untouched — the same ruling retains them explicitly. Author a theme as a + document handed to `ThemeProvider`; that path never went through the removed + component kind. +- 4d73b07: The zod `BaseSchema` mirror now accepts everything its TypeScript declaration + declares — five keys had drifted narrower (objectui#4605). + + `@object-ui/types/zod` is a published runtime validator hand-written to mirror the + `BaseSchema` interface. As the interface widened, the mirror did not, so five keys + refused at parse time a spelling the published types invite and the renderer + implements — "declared = enforced" inverted. `.passthrough()` rescued none of them: + passthrough admits UNDECLARED keys, and all five are explicitly declared, so the + narrow declaration won. + + Measured against the unmodified mirror before the change, these were the refusals: + + | key | authored input | old mirror said | + |---|---|---| + | `visible` | `'${data.status === "open"}'` | `expected boolean, received string` | + | `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` | + | `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` | + | `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` | + | `description` | `{ en: 'The record owner' }` | `expected string, received object` | + + `visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts, + no wider. `ariaLabel` takes the KEYED reference through a new exported + `KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema` + BY REFERENCE, so a change to the spec's label contract is picked up rather than + re-typed. Every spelling that parsed before still parses. + + The two i18n vocabularies are kept apart rather than merged into "some object". + `label`/`description` are the spec's INLINE locale map (resolved by + `resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by + `resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render + an EMPTY aria-label). Widening both slots to accept either shape would have + reproduced objectui#4167's confusability hazard inside the validator that exists to + catch it, so each slot admits only its own vocabulary and both cross pairings are + pinned as rejections. + + The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own + `.shape` and compares each key against the declaration, so the next widening of + `base.ts` that forgets this file turns it red with no list to maintain. It reads + `.shape` and not `keyof z.input<…>` because that spelling was measured vacuous — + `.passthrough()` collapses the inferred key union to bare `string`, and a pin written + over it resolved `never` while five keys were demonstrably narrow. Two guards pin the + derivation against both degenerations (`never` and `string`). + +### Patch Changes + +- 100547e: `objectui validate` now refuses a form field whose widget id names a namespace + other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` + has given since objectui#5375 (objectui#5449). + + The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema + declared `type` and `widget` as bare optional strings — so a field typed + `ui:password` validated clean while the runtime validator rejected the same + document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an + author actually runs before shipping, so it was the one handing out the false + green: an author did exactly the diligence objectui#5375 asks for and still + shipped metadata that renders a secret into a plain text box. + + A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's + precedence (`widget` before `type`), the key it blames, its error code and its + message verbatim, so the two entry points cannot describe one defect two ways. + + **This rejects documents that previously validated.** Only colon-qualified + field widget ids outside the `field:` namespace are affected — `field:`-prefixed + ids and bare names such as `password` still pass, registered or not. A field + carrying, say, `type: 'ui:password'` must be rewritten as `password` or + `field:password`; it never rendered as a password box in any case. + + Which of the repo's authoring-time validators is canonical remains open + (objectui#4631) — this states the rule on the zod side rather than unifying + them. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- f9e4f91: `DashboardComponentSchema.dateRange.defaultRange` is now bound to + `DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a + hand-written 14-member union (objectui#4984). + + The union was byte-faithful to the spec — all 14 members, same order — so nothing + a user hits changes today. What was missing is the tie that keeps it faithful: + `resolveDashboardFilterDefs` takes `Pick`, so this union is what typechecks every TS-constructed dashboard, and + a preset the spec ADDS would have been a legal document that objectui's own types + said could not exist — the "narrower than the contract it implements" shape whose + consequence in objectui#4163 was that the bad reads were invisible to `tsc`. + + No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union + on an interface member has no symbol to collide with, while rule 2's claim heuristic + was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes + the file's existing "Aligned with @objectstack/spec" comment structural rather than + prose. + + The emitted `.d.ts` collapses the inline union to the imported alias; the published + type surface is unchanged — measured with the TypeScript checker over the emitted + declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, + and `defaultRange` resolving to the same 14 string-literal members before and after). +- 4e8622b: `ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a + plain string (objectui#4611). + + The paragraph explaining why `label` / `options[].label` are inherited rather than + locally overridden justified itself with a claim about `@objectstack/spec` that was + never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects + were dropped in favour of translation files". Measured against the installed GA pin + `@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of + a string and a string-to-string record, and the schema's own doc block states two + authorized forms with "Both are real; neither is deprecated by this schema". Executed + against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted, + `{ key, defaultValue }` rejected. A reader who believed the comment would have taken a + widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one + seat having nearly done exactly that. + + The replacement describes what `I18nLabel` admits and cites the spec's own doc block + rather than restating a zod expression; where today's spelling is named it is scoped as + a measurement against 17.0.0 with its file and line, so it ages as a reading rather than + as a standing fact. The decision itself is unchanged and never depended on the false + premise — `label` flows in by reference through the spec's schema, and a local + `string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds. + + Documentation only, and the release-visible surface is the declaration file: measured + with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`), + 108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every + other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha), + because the comment documents an `interface`, which is erased at emit along with its + leading comment. No behaviour changes; hover text and the shipped `.d.ts` do. +- dffd752: The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and + `ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613). + + Both are residue of the same removed local `label` / `options[].label` override. + + The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` + that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion, + `const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed + `I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain + string is assignable under the narrow shape *and* under the wide one, so that assignment + could only ever fail if the plain-string form were removed — the opposite of the event it + was written to catch. The widening had already landed: `@objectstack/spec@17.0.0` + declares `I18nLabelSchema` as a union of a string and a string-to-string record + (`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection + it did not provide, and asserted a false premise in its own name. + + It is retargeted at what actually holds the decision up — not which single form the spec + has, but that **both** authorized forms stay assignable, on the spec type and on the + inherited `ActionParam['label']` and `options[].label`. It now fails when either form is + withdrawn, and deliberately does not fail on a further widening, since inheriting by + reference is exactly what stays correct as the authorized set moves. The comment is + rewritten against the schema's own doc block (two authorized forms, "Both are real; + neither is deprecated by this schema") instead of the false premise. Verified by + construction: against a locally built narrow `type I18nLabel = string` the new assertions + fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both + shapes. + + `ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since + the override was removed, and nothing re-exported it — and the doc paragraph that + recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard. + + No behaviour changes; the release-visible surface is the declaration file. Measured with + the package's real `tsc` build, both legs building from a cleared `dist/` and cleared + composite build info: 108 emitted files on both sides, exactly one differing — + `dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed. + Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged + sha), because the comment documents an `interface`, which is erased at emit along with + its leading comment. The deleted type import contributes no emitted delta at all, and the + rewritten test file is not part of the build. +- a691c0b: `PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted. + + This is a published type surface: the TSDoc ships in `@object-ui/types`'s built + `.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'` + page is "constrained JSX/HTML + Tailwind" — and it links + `content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to + say the opposite. Shipped type documentation was contradicting the guide it points + readers to. + + ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that + framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind + is compiled at build time by scanning the console's own `src`, and there is no + safelist — so an authored utility class produces CSS only by coincidence, when + objectui already ships that exact class, and otherwise produces nothing with no + error anywhere. That is the ADR-0065 failure mode verbatim ("works only by + coincidence"), and it is how a modal's `bg-black/50` backdrop reached production + fully transparent. + + The tiers themselves are unchanged, and every load-bearing claim in the TSDoc + survives verbatim — parse-never-execute and untrusted-author safety for `html`, + the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the + `react-pages` host capability for `react`, the ADR-0080 citation and the guide + link. Only the styling conclusion changes, to the primitive each tier actually has: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `` ``, `` `` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows + light/dark and whatever theme the deployment installs. The TSDoc now also names the + rule that reports a violation — `page-source-className-tailwind`, shipped in + `@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by + `os validate` as a warning on both tiers. + + No behaviour change, and the accepted `kind` set is untouched. + + `packages/components/src/renderers/layout/react-page.tsx` carries the same + correction on its two source comments (the injected-scope note and + `buildComponentScope`), and gains the styling note the file was missing. Those are + internal comments — they do not project into any `.d.ts` and change no export — so + they get no entry of their own; there is nothing an `@object-ui/components` + consumer could read in a CHANGELOG and act on. +- 3c73d99: Corrects the `@deprecated` prescription on `UniquenessValidation` in + `packages/types/src/data-protocol.ts`, which pointed authors at spellings the platform + no longer accepts (objectui#4765). + + Comment-only — no runtime behaviour changes. `patch` rather than an empty frontmatter + because the JSDoc sits on an **exported** declaration and therefore ships to consumers: + measured with the package's own build (`tsc`, and `tsconfig.base.json` deliberately sets + `removeComments: false`), `dist/data-protocol.d.ts` goes 40218 → 41781 bytes and the new + prose is present in the emitted `.d.ts`. What a consumer reads on hover changes, so it + is declared. The emitted `dist/data-protocol.js` is byte-identical (sha256 + `a3de34c5…`, 207 bytes both ways) — that file is a types-only module whose entire JS + output is the license banner plus `export {}`, so a comment on an erased `interface` + reaches the declaration file and nothing else. + + Two of the three spellings it prescribed were wrong, measured against the installed + `@objectstack/spec@17.0.0` (the report was written against `17.0.0-rc.6`): + + - **`indexes[].partial`** was retired in spec 17.0.0 under ADR-0049. It is a tombstone + (`z.never()`) that the parse rejects at any value, so "`partial` for a scoped + constraint" named a key that cannot be declared. A predicated unique constraint is + built at the database layer by a runtime migration issuing + `CREATE UNIQUE INDEX … WHERE`; the prescription now says so. + - **`{ fields, unique: true }`** on `ObjectSchema.indexes` is the deprecated positional + spelling of `unique: 'global'` under ADR-0120 — lint `unique/unscoped-declared-index` + warns in 17.x and protocol 18 rejects it. The prescription now states the scope: + `unique: 'global' | 'organization'`. + + The measurement also refined the report, and the refinement is the reason the rewrite is + not a uniform find-and-replace. The third spelling — **field-level** `unique: true` — is + NOT deprecated. `unique` is scope vocabulary shared by two surfaces on which the same + bare `true` means different things: at index level it stays verbatim (`isGlobalUnique` + and `isOrganizationUnique` both return `false`), which is why it is the positional + spelling of `'global'` and is being retired; at field level it is the positional spelling + of `'organization'` and, in the spec's own words, "stays valid indefinitely … no trap". + Rewriting both occurrences the same way would have replaced one piece of false guidance + with another, so the comment now names the per-surface difference explicitly. + + The interface's own deprecation is untouched and remains correct: `ValidationRuleSchema` + rejects `type: 'unique'` at the discriminator (accepted discriminants are `script`, + `state_machine`, `format`, `cross_field`, `json_schema`, `conditional`), so a rule in + this shape cannot reach the server. + + The replacement closes with what would falsify it — `UniqueScopeSchema` and + `IndexSchema` in `@objectstack/spec` — so the next reader checks the schemas rather than + trusting the paragraph. This is the fourth piece of false guidance found in this + campaign (strictness ledger finding 18), and prose that cannot be checked is how the + first three survived. + ## 17.6.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index e65f4efde..f0192cddb 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/types", - "version": "17.6.0", + "version": "17.7.0", "description": "Pure TypeScript type definitions for Object UI - The Protocol Layer", "type": "module", "sideEffects": false, diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index c9ae926d1..895443d93 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 17.7.0 + +### Patch Changes + +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [a691c0b] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [6c6cee7] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [3c73d99] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 110f58c29..8d0ae06ae 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "object-ui", "displayName": "Object UI", "description": "VSCode extension for Object UI - Schema-driven UI development with IntelliSense, validation, and live preview", - "version": "17.6.0", + "version": "17.7.0", "publisher": "objectui", "private": true, "icon": "icon.svg",