diff --git a/.changeset/4918-stale-strip-mode-prose.md b/.changeset/4918-stale-strip-mode-prose.md deleted file mode 100644 index 5f16c6e8b..000000000 --- a/.changeset/4918-stale-strip-mode-prose.md +++ /dev/null @@ -1,16 +0,0 @@ ---- ---- - -Comment-only truthfulness fix in `@object-ui/plugin-detail`'s spec-parity tests. The -`relationshipValueField` block in `recordRelatedListInputs.spec-parity.test.ts` and the -`hideFields` block in `recordDetailsInputs.spec-parity.test.ts` each narrated strip mode -as a present-tense fact about the installed `@objectstack/spec` — that an undeclared -top-level key parses green and is silently dropped from `data`. The props schemas were -closed upstream, so an undeclared key now draws a named `unrecognized_keys` refusal, and -both comments read as current while being false against the installed pin. Both now state -the pin-independent verdict (an undeclared top-level key is never authoring surface) and -note that the contract expresses it two ways depending on the installed pin, pointing at -`specRefusesUnknownTopLevelKeys` in the sibling `recordHighlightsInputs.spec-parity.test.ts` -as the behavioural probe. This also settles `recordDetailsInputs` disagreeing with itself, -its section-key block having already taken the pin-aware disposition. No assertion changed -and no published behaviour changes. diff --git a/.changeset/4935-element-filter-comment-residue.md b/.changeset/4935-element-filter-comment-residue.md deleted file mode 100644 index caebf8b3b..000000000 --- a/.changeset/4935-element-filter-comment-residue.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Comment-only cleanup in `@object-ui/components`: the `elements.tsx` header note no -longer lists `element:filter` among the heavier interactive elements said to live in -their owning plugins. No such file ever existed for it, and the element was retired at -element grain upstream (ADR-0049), so the type is gone from `PageComponentType` -entirely. No published behaviour changes. diff --git a/.changeset/4947-tombstone-judge-convergence.md b/.changeset/4947-tombstone-judge-convergence.md deleted file mode 100644 index 7f6937b24..000000000 --- a/.changeset/4947-tombstone-judge-convergence.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-infrastructure convergence, no shipped code touched: the last two local copies of -the ADR-0087 D2 tombstone criterion — in `packages/plugin-detail`'s -`recordDetailsInputs` spec-parity test and `packages/app-shell`'s `block-config` -preview test — now import `@object-ui/test-support`'s shared judge instead of -spelling out `unwrap()._def.type === 'never'` by hand. Both copies carried the -structural channel alone; the shared judge OR-s it with the `[REMOVED]` description -channel, so neither channel can go quietly permissive on its own, and every -tombstone-aware gate in the repo now moves together when the criterion changes. diff --git a/.changeset/action-param-degrade-predicate-5654.md b/.changeset/action-param-degrade-predicate-5654.md deleted file mode 100644 index 2fc5978fe..000000000 --- a/.changeset/action-param-degrade-predicate-5654.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -**Behaviour change:** a `master_detail` action param with no reference target now -receives the same "paste a record id" placeholder and help text a targetless -`lookup` param has had since objectui#3405. Previously it degraded to a plain -text input exactly like `lookup` did, but silently — the user was shown an -unexplained empty box that wanted a bare record id. - -The cause was two hand-maintained answers to one question. `paramToField()` -performs the degradation over RESOLVED widget keys (`lookup`, `master_detail`), -while `ActionParamDialog` decided who gets the hints with its own literal over -RAW param spellings (`'lookup' || 'reference'`). Neither set contained the -other: `master_detail` degraded with no hints, and `reference` was a copy of an -alias-table row the adapter folds to `lookup` before it tests membership. - -`paramToField` now exports `paramDegradesWithoutTarget(param)` and both the -adapter's own fallback branch and the dialog's two hint readers ask it, so "who -degrades" and "who gets hints after degrading" are one member set answered once -(objectui#5654; same convergence shape as objectui#4770 / #4790 / #4815). - -Params that already had the hints keep them, including the `reference` spelling; -a param that renders as text without having degraded (an unknown type) still -gets no picker hints. diff --git a/.changeset/action-runtime-same-origin-only-5702.md b/.changeset/action-runtime-same-origin-only-5702.md deleted file mode 100644 index 1340b7143..000000000 --- a/.changeset/action-runtime-same-origin-only-5702.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -**Behaviour change:** the console action runtime (`useConsoleActionRuntime`) now -builds its authenticated fetch with `sameOriginOnly: true`, matching the -`provider: 'api'` data-source lane (`ConsoleShell`). A metadata `type: 'api'` -action whose resolved target is a different origin than the page is fetched -through the bare global fetch: the platform Bearer token, `X-Tenant-ID`, and -`Accept-Language` are no longer attached (objectui#5702, maintainer ruling -2026-08-22). Previously the Bearer rode to any off-origin target whose URL -contained `/api/`, and `X-Tenant-ID` rode to every off-origin target -unconditionally. - -Same-origin actions — including every relative target in a same-origin -deployment — are unchanged, and off-origin requests still execute (pass-through, -not a refusal). An off-origin integration that legitimately needs the platform -bearer declares itself explicitly or proxies same-origin. - -Note for split-host setups: `apiHandler` prefixes relative action targets with -`VITE_SERVER_URL`. When that is set to an origin different from the page's, -those action requests are off-origin and no longer carry credentials. The -committed dev and starter configurations are unaffected: since objectui#5745 -they ship `VITE_SERVER_URL` empty and reach a split-host backend through the -Vite `/api` dev proxy, so their action requests are same-origin. diff --git a/.changeset/active-org-clear-removal-failure-5731.md b/.changeset/active-org-clear-removal-failure-5731.md deleted file mode 100644 index aacb03bc7..000000000 --- a/.changeset/active-org-clear-removal-failure-5731.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually -gone and, when it is not, both reports the failure and stops `get()` answering from -the surviving value — instead of swallowing the failed removal (objectui#5731). - -`clear()` nulled `_memoryValue` and then removed the persisted key inside a -`try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a -NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two -halves of `clear()` were not equally strong: nulling memory always sticks, while a -removal that did not stick left the key readable and the read order preferred it. -Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does -not stick", and it was silent — the cleared organization went back on the wire as -`X-Tenant-ID` on every subsequent request. - -The removal is now judged by a READ-BACK rather than by catching the throw, which is -both narrower and wider in the right directions. Wider: a wrapped or proxied -`localStorage` whose `removeItem` is a silent no-op never throws and leaves identical -residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where -every operation throws have nothing readable to resurrect, were already safe, and are -not reported as failures. - -A key whose removal could not be verified is quarantined in memory for the rest of the -page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`, -which `clear()` has just nulled and which a later `set()` refills with the value that -write was meant to persist. The quarantine is released as soon as a removal on that key -sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract -(objectui#5279); a re-stamped signed-out organization is not. - -The failure is not thrown and not returned. All five call sites — sign-out's -`purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`, -`leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive -after the transition they follow up on has already happened, and none can act on a -storage failure; a `boolean` every caller ignores would read as handled when it is not. -So the invariant is restored inside `clear()` and the failure is reported to the -console. - -A working `localStorage` behaves exactly as before: the removal sticks, nothing is -quarantined, nothing is reported, and a non-null persisted read is still authoritative. -`set()`'s swallowed write failure is deliberately untouched — that swallow is -objectui#5703's memory fallback, and it is the correct kind, because the memory copy -upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s. diff --git a/.changeset/active-org-storage-memory-fallback-5703.md b/.changeset/active-org-storage-memory-fallback-5703.md deleted file mode 100644 index eef9655d9..000000000 --- a/.changeset/active-org-storage-memory-fallback-5703.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls -back to the in-memory value otherwise, instead of returning the `localStorage` read -unconditionally — so the fallback is reachable in the browser state it was written for -(objectui#5703). - -`set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but -`get()` only consulted `_memoryValue` when the READ itself threw. There is a real -browser state where the read does not throw and the fallback is nonetheless the only -copy: `localStorage` present and readable but rejecting writes — Safari private -browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`. -In that state the active org was stored and could not be read back, measured as -`_memoryValue = org-42` alongside `get() = null`. - -The cost was silent and lasted the whole session rather than the documented first-boot -window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on -any request. Per the edge contract documented on objectui#5279 that header is a routing -hint a reader falls through on — the framework scopes from the session — so no row -visibility rode on this; what was missing is the tenant-routing input, on every request. -`switchOrganization` also appeared to succeed while the client-side stamp never -followed. - -Sign-out is unaffected, and that is the half worth stating: the new fallback fires -exactly when the `localStorage` read is null, which is the state `clear()` leaves -behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That -property is now pinned by test rather than relied upon, so a future `clear()` that only -removed the persisted key fails a test instead of quietly re-stamping a cleared org. - -A non-null persisted read still wins over the memory value, so a working `localStorage` -behaves exactly as before. diff --git a/.changeset/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/analytics-error-branch-mapping-5663.md b/.changeset/analytics-error-branch-mapping-5663.md deleted file mode 100644 index e0809a768..000000000 --- a/.changeset/analytics-error-branch-mapping-5663.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's -ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an -unauthenticated session stop being reported as a missing analytics capability -(objectui#5663). - -Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`: -the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and -the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The -mapping tested `res.status === 501 || res.status === 404` and called all of it -"the analytics capability is not installed", so every unknown dataset produced a -banner telling the operator to install `@objectstack/service-analytics` and mount -`AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on -four HotCRM Executive Overview widgets while the analytics service was installed -and answering — the real condition was an installed `app.objectstack.hotcrm` at -1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of -the system from the remedy the banner named. - -Three conditions now get three answers, each keyed on the code the framework -declares for it: - -- `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and - `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing - `AnalyticsNotInstalledError` and its copy — one remedy, one message. -- `NOT_FOUND` (404, unknown `datasetName`) throws the new - `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the - dataset and pointing at the installed app's version rather than at the server. -- `UNAUTHENTICATED` (401, `enforceAuth`) throws the new - `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the - request was refused before it ran and therefore says nothing about the - capability. - -The banner also used to print the server's own message in a parenthetical while -contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not -found.` under a headline claiming a missing capability. That is now structurally -impossible rather than merely fixed: the headline is a pure function of `code` and -the parenthetical is a verbatim quote of `message`, both read off the same -response, and a test walks every branch asserting each message carries its own -headline and none of the others'. - -Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its -constructor signature (it gains an optional third `serverCode` argument and a -`serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including -the metadata-admin dataset preview — are unaffected. A 404 carrying a code this -client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now -keeps its server detail instead of being relabelled as a missing capability; a 404 -or 501 carrying no code at all is still read as the capability being absent, since -the route's own `NOT_FOUND` always ships a code. diff --git a/.changeset/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/appsidebar-deprecated-5720.md b/.changeset/appsidebar-deprecated-5720.md deleted file mode 100644 index 404094e32..000000000 --- a/.changeset/appsidebar-deprecated-5720.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`AppSidebar` is now `@deprecated` — use `UnifiedSidebar` instead. - -A census (objectui#5720) found `AppSidebar` has no in-repo mount point -(`ConsoleLayout` renders `UnifiedSidebar`, not this component) and no -downstream consumer visible anywhere across this org's GitHub-visible -repositories. It stays exported — from the package barrel and the published -`dist/index.d.ts` — because `@object-ui/app-shell` is a public npm package -(`publishConfig.access: "public"`) and an external consumer outside this org -is structurally invisible to that census; that is why it is deprecated -rather than deleted outright. No behavior change in this release — the -component still renders exactly as before. Its admin nav cluster is a -near-duplicate of `UnifiedSidebar`'s and has already drifted from it (it -gates only `sys-marketplace` on the workspace-admin flag, where -`UnifiedSidebar` gates the whole cluster); that divergence is not being -reconciled, since the component is scheduled for removal rather than kept -in parity — see objectui#5817 for the removal plan. - -Migration: replace any `AppSidebar` usage with `UnifiedSidebar` from the same -package. diff --git a/.changeset/authguard-positions-5424.md b/.changeset/authguard-positions-5424.md deleted file mode 100644 index 438aa5d16..000000000 --- a/.changeset/authguard-positions-5424.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@object-ui/auth": minor ---- - -`AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424. diff --git a/.changeset/built-moment-transition-5799.md b/.changeset/built-moment-transition-5799.md deleted file mode 100644 index b42946326..000000000 --- a/.changeset/built-moment-transition-5799.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio//interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app::build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio. diff --git a/.changeset/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/calendar-view-event-rename-5044.md b/.changeset/calendar-view-event-rename-5044.md deleted file mode 100644 index 0ba8d30bf..000000000 --- a/.changeset/calendar-view-event-rename-5044.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-calendar': patch ---- - -`@object-ui/plugin-calendar` now exports the `CalendarView` component's runtime event -type as **`CalendarViewEvent`**, and keeps `CalendarEvent` as a **`@deprecated` alias** -of it. **Non-breaking:** the alias is a working re-export denoting the same type, so -code importing `CalendarEvent` from this package keeps compiling unchanged — nothing is -removed and no behaviour changes. - -Why: `@object-ui/types` exports its own `CalendarEvent`, the AUTHORING event -(`id: string`, `start` / `end` accept ISO strings with `end` required, plus -`description`), while this package's was the runtime event (`id: string | number`, -`start: Date`, `end?: Date`). Neither is assignable to the other, and IDE auto-import -chose between the two identical names essentially at random — the wrong pick surfaced as -a remote `TS2322` about `Date` rather than as a wrong import, which is how this package's -own README example stayed uncompilable through an earlier import-path fix. The authoring -type keeps the canonical `CalendarEvent` name; the runtime type gets the self-describing -one (objectui#5044, following the `ObjectCalendarProps` -> `ObjectCalendarComponentProps` -rename in objectui#4650). - -Write `CalendarViewEvent` in new code. diff --git a/.changeset/calendar-view-mode-agenda-retired-5740.md b/.changeset/calendar-view-mode-agenda-retired-5740.md deleted file mode 100644 index f50f849b4..000000000 --- a/.changeset/calendar-view-mode-agenda-retired-5740.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema` -(objectui#5740 — the value-level residue of objectui#5667's key-level -convergence of `CalendarViewSchema` on the registered `calendar-view` -renderer's measured read set; ADR-0049 enforce-or-remove). - -The union declared a value nothing enforced: the registered renderer's `view` -input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves -any off-enum value — `'agenda'` included — to `undefined` (the component's -`'month'` default), and `CalendarView` renders no agenda view. An author -writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with -no error or warning. No in-repo, example, or catalog app authors -`view: 'agenda'` (measured during objectui#5667's sweep and re-measured for -this change, including the objectstack tree). - -**This narrows the accept set — unlike #5667's key retirements, which created -no new rejections.** `view` is a declared key, and declared keys are validated -even under `.passthrough()`, so `view: 'agenda'` is now a **validation error -that previously parsed green** (an `invalid_value` issue on the `view` path, -offering `month`/`week`/`day`). Undeclared keys still pass through unchanged. -Breaking on the published zod surface; ships as `minor` per this repo's -version-alignment policy (majors track `@objectstack`). - -The runtime boundary is unchanged: an off-union `view` in raw metadata still -falls back to the component's `'month'` default at the renderer, and the -registry input already declared the three-value enum. Docblocks, the schema -reference table, and the zod `describe` no longer teach an `'agenda'` -fallback. diff --git a/.changeset/calendar-view-schema-converge.md b/.changeset/calendar-view-schema-converge.md deleted file mode 100644 index 61f9a3c1d..000000000 --- a/.changeset/calendar-view-schema-converge.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-calendar': patch ---- - -`CalendarViewSchema` (TS interface and zod mirror) converges on the registered -`calendar-view` renderer's measured read set (objectui#5667, maintainer ruling -option A — the renderer is authoritative). - -**Breaking for consumers of the published type** (deliberate; per-repo policy -breaking changes ship as `minor` — the fixed group's `major` tracks -`@objectstack`): - -- Nine inert keys are retired: `events` (the interface's only required key, - which the renderer deliberately drops — objectui#4433), `defaultView`, - `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, - `onEventUpdate`, `onDateChange`. None had a read site on the authored-node - path and no measured app authors them (ADR-0049 enforce-or-remove). -- The type now declares what the renderer actually reads: `data`, `titleField`, - `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, - `currentDate`, `allowCreate`, `className`, plus the two host-only function - hatches it forwards (`onEventClick`, `onViewChange`). -- Practical radius, measured: `BaseSchema` carries an index signature and the - zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys - neither fail to compile nor get rejected at validation — they are simply no - longer declared, documented, or type-checked. The material accept change is - that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` - node without it now validates (previously the one key validation demanded - was the one key guaranteed to do nothing). - -Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README -and `content/docs/api/schema-reference.md` are repaired to the converged -surface in the same change, so no copy of the old contradiction survives. diff --git a/.changeset/canvas-design-run-5800.md b/.changeset/canvas-design-run-5800.md deleted file mode 100644 index 18e7c4938..000000000 --- a/.changeset/canvas-design-run-5800.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one. diff --git a/.changeset/catalog-layout-props-sweep-4891-4890.md b/.changeset/catalog-layout-props-sweep-4891-4890.md deleted file mode 100644 index 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/chart-height-chain-5451.md b/.changeset/chart-height-chain-5451.md deleted file mode 100644 index 3ab583677..000000000 --- a/.changeset/chart-height-chain-5451.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -`ObjectChart`'s wrapper div now carries `h-full`, keeping the height chain intact from a dashboard grid cell's declared height down to the element Recharts measures. Previously the chain died at the plain auto-height wrapper: `height: 100%` on the chart container computed to `auto`, Recharts measured a permanent zero, and only the `CHART_MIN_HEIGHT` floor (#5503) kept dashboard charts visible — at a fixed floor height instead of filling the cell (#5451). Under auto-height parents `h-full` resolves to `auto`, so non-dashboard hosts are unchanged. diff --git a/.changeset/chat-transport-memo-4187.md b/.changeset/chat-transport-memo-4187.md deleted file mode 100644 index 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/confirm-card-terminal-5695.md b/.changeset/confirm-card-terminal-5695.md deleted file mode 100644 index 7d3d67a83..000000000 --- a/.changeset/confirm-card-terminal-5695.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/plugin-chatbot': minor -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. diff --git a/.changeset/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-runtime-default-seed-5727.md b/.changeset/console-formpage-runtime-default-seed-5727.md deleted file mode 100644 index d69e013e0..000000000 --- a/.changeset/console-formpage-runtime-default-seed-5727.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's form routes no longer seed a RUNTIME `defaultValue` into the -control, so the server-side resolution the declaration asks for actually happens -(objectui#5727). - -`readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared -default unconditionally. A `defaultValue` may be a literal, or an *instruction* -the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`, -`current_user`) or a CEL Expression envelope. Seeding one of those literally put -the text `NOW()` into a datetime input on both `/forms/:name` and the public -`/f/:slug` route, and submitting it sent that string as the field's value — -which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved -the declared default and the column stored the token text instead of a timestamp. - -The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same -published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its -seeding with, and the one this renderer already reads once removed (through -`isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default -leaves the key ABSENT rather than empty, because absent is precisely the case the -engine resolves. - -Nothing else about the prefill precedence moves: a literal default still seeds, a -stored record value still wins over a default, and an explicit `prefill_=` -param still wins over both — including for a field whose default is a runtime -token, since a value a producer supplies is not a declaration awaiting -resolution. diff --git a/.changeset/console-formpage-visible-predicates-5594.md b/.changeset/console-formpage-visible-predicates-5594.md deleted file mode 100644 index 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-preboot-branding-origin-5660.md b/.changeset/console-preboot-branding-origin-5660.md deleted file mode 100644 index d642b3dd3..000000000 --- a/.changeset/console-preboot-branding-origin-5660.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/console': patch ---- - -The console's pre-boot branding script now resolves its server origin from -`VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of -`window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set -(objectui#5660). - -The two callers of `GET /api/v1/runtime/config` share one in-flight request, and -that sharing keys on the FULL URL. So the origin was half of the contract: the -inline script in `index.html` read one spelling and `src/main.tsx` read another, and -whenever they disagreed the two never found each other. A same-origin production -build hid it completely — both spellings collapse to `''` — so the split surfaced -only in a dev pointing the console at a separate server, where it cost two requests -to two different servers and let pre-boot branding paint from the wrong one. - -The pre-boot fetch is kept, not deleted: it is the request the module side JOINS. -`sharedGetJson()` can only hand an earlier request to a later caller, and this -script is the earlier one by construction — it runs during HTML parse, before the -bundle is fetched, while `initRuntimeConfig()` is awaited before -`createRoot().render()`. Deleting it would not remove a request; it would move the -single remaining one later, onto the critical path to first paint, and leave the -page's empty `` and favicon unbranded until React mounts. - -Vite substitutes its HTML env token only when the variable is set and leaves it -verbatim otherwise, so the unset case is read as same-origin `''` rather than -allowed to reach the URL as a path segment. diff --git a/.changeset/console-studio-builder-decorative-lazy-5486.md b/.changeset/console-studio-builder-decorative-lazy-5486.md deleted file mode 100644 index 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/crud-guide-declarative-view-binding-5446.md b/.changeset/crud-guide-declarative-view-binding-5446.md deleted file mode 100644 index 936ad20c9..000000000 --- a/.changeset/crud-guide-declarative-view-binding-5446.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Test-only change: extended `packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx` -to measure the `content/docs/guide/building-crud-app.md` rewrite for objectui#5446 (a -docs-only fix — the guide now teaches the declarative `dataSource: { object, view }` -binding instead of the `view` / `data.queryParams` keys `ObjectGrid` never read). No -published package behaviour changes. diff --git a/.changeset/current-user-default-prefill-5683.md b/.changeset/current-user-default-prefill-5683.md deleted file mode 100644 index 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-declare-honoured-inputs.md b/.changeset/dashboard-declare-honoured-inputs.md deleted file mode 100644 index c0129d95e..000000000 --- a/.changeset/dashboard-declare-honoured-inputs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -`dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title. diff --git a/.changeset/dashboard-expandable-family-convergence-5692.md b/.changeset/dashboard-expandable-family-convergence-5692.md deleted file mode 100644 index b7fcc7a8c..000000000 --- a/.changeset/dashboard-expandable-family-convergence-5692.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`plugin-dashboard`'s two private copies of the reference-bearing field family now -read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating -it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside -`computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the -already-converged consumers carry, so a member-identical private copy fails -rather than quietly re-forking the table. - -Two behaviour changes on the dashboard table's `$expand`, in opposite directions: - -- **A `tree` column is now expanded.** A self-referencing hierarchy field is - reference-bearing and a member of the shared family, so its cell renders the - parent record's display name instead of a bare id — the same treatment the form - and grid roads already gave it. -- **A `reference`-typed column is no longer expanded.** Measured before removing - it: `reference` is absent from `@objectstack/spec`'s closed `FieldType` - vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant - object schema can declare a field whose stored type is `reference`. Dropping it - is a no-op on real data; the spelling is a legacy dialect alias on the - action-param surface, folded to `lookup` before any field-type data is read. - -`EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license -widening a published shared set. diff --git a/.changeset/dashboard-header-collapse-5812.md b/.changeset/dashboard-header-collapse-5812.md deleted file mode 100644 index 9c0d11f88..000000000 --- a/.changeset/dashboard-header-collapse-5812.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`DashboardRenderer` no longer emits an empty dashboard header wrapper. The -wrapper used to render whenever `header` was declared, while each of its -children — title, description, actions — was additionally suppressible. With -the console page chrome present (`hideHeaderText`, set because the chrome -already renders the dashboard's title and description) and no `header.actions`, -every child evaluated falsy and the DOM still received -`<div class="col-span-full mb-4"></div>`: zero children, yet a full grid row -(measured 64px) plus `mb-4` of dead band above the filter bar, on every console -dashboard page (objectui#5812, measured on HotCRM 17.1.0). - -The three children are now computed first and the wrapper renders only if one -of them survives. Nothing else changes: a standalone embed (no chrome) renders -title and description exactly as before, and declared `header.actions` keep the -wrapper alive even under the chrome, since the chrome renders text only. Authors -needed this — dropping `header` from the metadata to reclaim the pixels would -have cost the standalone embed its title, which is what `header` is for. diff --git a/.changeset/dashboard-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-table-measure-desc-first-5845.md b/.changeset/dashboard-table-measure-desc-first-5845.md deleted file mode 100644 index 6eb74642b..000000000 --- a/.changeset/dashboard-table-measure-desc-first-5845.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard `table` widget — a numeric **measure** column now sorts **descending -on the first click** (objectui#5845). - -The sortable headers shipped in objectui#5827 started every column ascending, -which left the card's own motivating complaint — the largest industry rendering -last — two clicks away. For a measure in an analytics widget the question a -click asks is "who is biggest", so that column now cycles -**descending → ascending → the dataset's own order**. - -Unchanged: a **dimension** column, and a measure whose values are not numbers -(a `min()`/`max()` over a text field), keep **ascending → descending → the -dataset's own order** — the idiom the console's own DataTable uses. The -first-click direction is decided by the same measure-and-all-values-are-numbers -classification that decides right-alignment, so it follows the column's *role* -rather than "it looks like digits": a digit-keyed dimension (a year, a quarter) -still starts ascending. - -Blanks still sort **last in both directions** — that arm is now what a measure -column's very first click runs — and `aria-sort` reports the direction actually -applied. diff --git a/.changeset/dashboard-table-widget-polish-5827.md b/.changeset/dashboard-table-widget-polish-5827.md deleted file mode 100644 index 9714c2330..000000000 --- a/.changeset/dashboard-table-widget-polish-5827.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering -fixes so it reads as a table rather than a raw dump (objectui#5827): - -- **Numeric columns right-align**, header and cells. `tabular-nums` was already - applied to every cell and could not do its job while the digits were flushed - left. A column qualifies when it is a MEASURE and every non-null value in it - is a number — the declared column `type` cannot decide it, because the - analytics executor stamps `type: 'number'` on every measure regardless of the - aggregate, so a `min()`/`max()` over a text field would have been - right-aligned on the strength of a label. Dimensions stay left-aligned even - when their bucket keys are digits: a grouping axis is not a quantity. -- **Sortable headers.** Each header is a keyboard-reachable button that cycles - ascending → descending → back to the dataset's own order, with the console's - existing sort indicator and `aria-sort` on the `th`. Blanks sort last in - BOTH directions. The sort is client-side over the rows already fetched — the - widget issues one `queryDataset` call and paginates nothing, so no round trip - is involved. -- **The empty-dimension bucket sorts last** in the default order instead of - floating to the top. It keeps its `—` label; only its position moves. -- **Row hover feedback on every row**, not only on a drillable one. A drillable - row keeps its stronger accent fill. - -The CSV export now follows the order the table is showing, which is the same -"the CSV is the table's data" convention its cell text already followed. -Drill-through is unaffected: rows are reordered as `(row, incoming index)` -pairs, so a drill still resolves through the server's parallel raw-value -sidecar. - -Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and -chart paths, and every non-dataset table in the console. diff --git a/.changeset/dashboard-title-locale-writeback-5428.md b/.changeset/dashboard-title-locale-writeback-5428.md deleted file mode 100644 index 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<Partial<SpecDashboardWidget>, -…>` (`dataset` is not in the `Omit` list). `widget.dataset` type-checks directly; the cast -was pure redundant widening, and a harmful one — after `as any`, `.dataset` reads as -`any`, so a future spec change to that key's shape would not go red here. - -**No runtime behaviour changes.** `!!(widget as any).dataset` and `!!widget.dataset` -evaluate identically for every input; `as any` is a compile-time-only annotation. Proven -with a reverse check: temporarily typo'ing the property to `widget.datasetTypoXYZ` turns -`pnpm --filter @object-ui/plugin-dashboard type-check` red with `TS2339: Property -'datasetTypoXYZ' does not exist on type 'DashboardWidgetSchema'`, confirming the removed -cast was suppressing real type coverage rather than papering over a genuine gap. - -The ADR-0021 point the original comment made — a dataset-bound widget renders through -the governed `queryDataset` path (`DatasetWidget`) instead of the inline object-aggregate -schema — is unchanged and kept; only the now-false justification for the cast is rewritten. diff --git a/.changeset/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<DashboardComponentSchema, 'globalFilters' | -'dateRange'>`, so this union is what typechecks every TS-constructed dashboard, and -a preset the spec ADDS would have been a legal document that objectui's own types -said could not exist — the "narrower than the contract it implements" shape whose -consequence in objectui#4163 was that the bad reads were invisible to `tsc`. - -No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union -on an interface member has no symbol to collide with, while rule 2's claim heuristic -was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes -the file's existing "Aligned with @objectstack/spec" comment structural rather than -prose. - -The emitted `.d.ts` collapses the inline union to the imported alias; the published -type surface is unchanged — measured with the TypeScript checker over the emitted -declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, -and `defaultRange` resolving to the same 14 string-literal members before and after). diff --git a/.changeset/dead-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/default-view-agenda-retired-5784.md b/.changeset/default-view-agenda-retired-5784.md deleted file mode 100644 index ae59ebf29..000000000 --- a/.changeset/default-view-agenda-retired-5784.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/types': minor ---- - -`'agenda'` leaves `defaultView` on all three of its declaration faces -(objectui#5784 — the `defaultView` sibling of objectui#5740's retirement on -`CalendarViewSchema.view`; ADR-0049 enforce-or-remove): the -`ObjectCalendarSchema` TS interface (an inline union `#5740`'s -`CalendarViewMode` narrowing could not reach), the zod `ObjectCalendarSchema`, -and the list-view `calendar` config's objectui-only `defaultView` extension. -All three are now `['month', 'week', 'day']`. - -The declarations admitted a value nothing enforced: `ObjectCalendar`'s props -declare `defaultView?: 'month' | 'week' | 'day'`, its schema read casts to the -same three values, and `CalendarView` renders no agenda view. An author -writing the type-legal, zod-valid `defaultView: 'agenda'` on an -`object-calendar` node or in a list view's `calendar` config got a month -calendar with no error or warning. The spec side already agrees: -`@objectstack/spec`'s `ObjectCalendarProps.defaultView` is -`['month', 'week', 'day']`. No in-repo, example, or catalog app authors -`defaultView: 'agenda'` (measured for objectui#5784 with positive controls, -including the objectstack tree — its only `agenda` token is the Agenda -job-scheduler library). - -**This narrows the accept set: an author who writes `defaultView: 'agenda'` -will now be refused at validation.** `defaultView` is a declared key, and -declared keys are validated even under `.passthrough()`, so -`defaultView: 'agenda'` is a **validation error that previously parsed -green** (an `invalid_value` issue on the `defaultView` / -`calendar.defaultView` path, offering `month`/`week`/`day`). Undeclared keys -still pass through unchanged. Breaking on the published zod surface; ships as -`minor` per this repo's version-alignment policy (majors track -`@objectstack`). - -The runtime boundary is unchanged: `ObjectCalendar` still resolves an -off-union raw `defaultView` away to its `'month'` default. Docblocks and both -zod `describe` strings now teach the three-value set. diff --git a/.changeset/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/<pkg>@<semver>`) and not one begins with `v`. Nothing creates tags except -the Changesets action, so the trigger had no way to fire. - -It is obsolete rather than misconfigured, which matters because the two have opposite -fixes. The workflow parsed and registered fine (GitHub lists it `active`) and would fire -if a `v*` tag were pushed — the job it would do, "Create GitHub Release", is simply -already being done by `changeset-release.yml`, whose Changesets action publishes GitHub -Releases tagged `@object-ui/<pkg>@<version>`. Its own npm publish step was still -commented out under "Uncomment the following steps when ready to publish to npm" while -the repo has been publishing to npm through Changesets for months, and its one-version -model (a single `v<semver>` for the whole repo, pointing at the root `CHANGELOG.md`) -never matched the 39-package fixed group this repo actually releases. - -The doc edit is not optional housekeeping: `scripts/__tests__/ci-cd-pipeline-doc.test.ts` -pins that page to `.github/workflows/` in both directions, so a page still naming -`release.yml` after the file is gone fails "never names a workflow file that does not -exist". - -No package `src/` is touched, so no `@object-ui/*` package changes behaviour and there is -nothing here for a consumer to upgrade to. diff --git a/.changeset/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/dom-leak-sweep-fixture-depth-5630.md b/.changeset/dom-leak-sweep-fixture-depth-5630.md deleted file mode 100644 index d471f6afd..000000000 --- a/.changeset/dom-leak-sweep-fixture-depth-5630.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only: deepen the `element:definition-list` / `element:repeater` / -`element:metadata_viewer` fixtures in the DOM-leak sweep -(`packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`) past their -empty-state placeholder branch, so their clean reading covers the real, -populated markup instead. No renderer source changed and no published -behaviour changes. diff --git a/.changeset/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: <this page's object> })`, so a flow that - created a quote from an Opportunity page never told the related list that - would now contain it — the record did not appear until a manual reload. The - runner cannot know which objects a flow touched, so it emits - `{ objectName: '*' }`: the same scope, for the same stated reason, that the - record page's manual ⟳ already uses. Everything mounted refetches in place - over the invalidation bus, with no remount. - -The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded -English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, -`nextStep`, `completed`) in all ten packs, plus reuse of -`common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The -server's own refusal sentence is still passed through untranslated — it is prose -the automation engine composed for a human, not copy with a key. diff --git a/.changeset/form-control-renderers-dom-props-5632.md b/.changeset/form-control-renderers-dom-props-5632.md deleted file mode 100644 index c13febb1a..000000000 --- a/.changeset/form-control-renderers-dom-props-5632.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/components': patch ---- - -**Behaviour change:** the eighteen renderers whose host element is a form -control no longer forward their whole prop bag to it. They route it through a -form-control DOM declaration — the same `pickDomProps` mechanism `grid`, `flex`, -`stack`, `container` and `text` were converged on — so an authored schema key -becomes an HTML attribute only if the contract declares it one (objectui#5632, -the `BARE_SPREAD_MINUS_NAME` slice of objectui#5574). - -The eighteen: `action:button`, `action:icon`, `ui:button`, `ui:checkbox`, -`ui:combobox`, `ui:date-picker`, `ui:email`, `ui:file-upload`, `ui:input`, -`ui:input-otp`, `ui:password`, `ui:radio-group`, `ui:sidebar-menu-button`, -`ui:slider`, `ui:sonner`, `ui:switch`, `ui:textarea`, `ui:toggle`. - -What this stops reaching the DOM: the renderer's own declared props, consumed -off `schema` to render the control AND spread onto the element a second time as -attributes HTML does not define, plus the authored node's SDUI metadata and the -flattened `props` container. Measured across `examples/schema-catalog`, rendered -through the real `SchemaRenderer`: 284 illegitimate attributes over 287 -form-control nodes — `button[label]` 140, `button[icon]` 25, `input[inputtype]` -23, `input[label]` 19, `toggle[label]` 14, `radio-group[options]` 8, -`date-picker[placeholder]` 7, `file-upload[label]` 7, `file-upload[buttontext]` -7, `checkbox[label]` 6, `textarea[label]` 6, `toggle[arialabel]` 6, -`switch[label]` 4, `password[label]` 3, `email[label]` 2, `radio-group[direction]` -2, and six singletons. The same probe reads 0 after, with `grid`'s 26 nodes at 0 -both times as the control and an unchanged node census across the two runs. - -`name` and `disabled` are DELIBERATELY still forwarded. Both are legal on a form -control and neither was ever part of the leak — the ledgered shape for this -group is thirteen attributes, not fourteen, precisely because HTML defines -`name` on these hosts. The whitelist this group uses is therefore the shared -SDUI one PLUS those two, not the bare `toDomProps` the container renderers take: -that would have stripped the form-serialization key off every control and -silently re-enabled every disabled one, and no gate in the repo would have gone -red for it. - -Nothing an author writes renders differently: every leaked key was already being -read off `schema` and applied. `id`, `role`, `tabIndex`, `className`, `style`, -event handlers and the open `data-*` / `aria-*` families are unchanged. - -Anything that read one of the leaked attributes off the DOM — a CSS attribute -selector such as `[label="Save"]`, or a test asserting `inputtype` on a rendered -`ui:input` — must read the schema instead. No `@object-ui` code did; this is -called out because the attributes were externally visible while they lasted. diff --git a/.changeset/form-designer-namespaced-field-type-4838.md b/.changeset/form-designer-namespaced-field-type-4838.md deleted file mode 100644 index 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-conditional-rule-surfaces-5627.md b/.changeset/formpage-conditional-rule-surfaces-5627.md deleted file mode 100644 index 6f9028e01..000000000 --- a/.changeset/formpage-conditional-rule-surfaces-5627.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/console': patch ---- - -`FormPage` — the console's own form renderer, serving both the public `/f/:slug` -route and the internal `/forms/:name` one — now honours the three conditional-rule -surfaces it still dropped after objectui#5594: section-level `visibleWhen` / -`visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` / -`requiredWhen` (objectui#5627). - -This is the second form renderer in the repo, and it honoured exactly one of the -four surfaces the sibling chain does. A section an author conditioned away rendered -in full — heading and every control — on both routes including the anonymous one. -The object-level half was worse than fail-open hiding: `readonly` was whatever the -static flag said, so a field a `readonlyWhen` should have locked stayed editable and -paired with the server's fail-closed unbound-scope behaviour into "the user edits, -the save reports success, and the value never lands". - -Both halves evaluate through the SHARED machinery rather than a fourth consumer-side -copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three -field rules — which brings the settled rulings with it, including the `serverOwnedValue` -carve-out that keeps a create form from requiring a producer-owned control (#4069 / -#4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first -`visibleWhen ?? visibleOn` read every sibling reader spells. - -Visibility stays a RENDERING rule at both granularities: a hidden section's fields -still submit their values, exactly as a hidden field's have since #5594. diff --git a/.changeset/formpage-maxlength-override-5595.md b/.changeset/formpage-maxlength-override-5595.md deleted file mode 100644 index 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/gantt-viewmode-declared-both-branches-5074.md b/.changeset/gantt-viewmode-declared-both-branches-5074.md deleted file mode 100644 index 650da3c57..000000000 --- a/.changeset/gantt-viewmode-declared-both-branches-5074.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/plugin-gantt': minor ---- - -**`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both -gantt renderer branches honour it** (objectui#5074, maintainer ruling -2026-08-19: declare-and-wire; the spec half landed first upstream). - -- `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, - DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` - enum by reference, so the member list cannot drift. Deliberately no - default: an omitted `viewMode` keeps letting a persisted layout - (`persistLayoutKey`) seed the timeline granularity before the renderer's - `'day'` fallback. -- The timeline branch (`GanttView`) now receives an authored `viewMode`. - Previously only the resource-workload branch (`resourceView` + - `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt - view was silently ignored. -- The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both - branches read the declared `ganttConfig.viewMode`, which also honours the - key when authored inside the spec's `gantt` config block. -- Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value - (e.g. `viewMode: 'hour'`) becomes a zod validation error where it - previously passed through unvalidated. Values on the published spec enum - are unaffected. diff --git a/.changeset/gate-listlocalinstalls-marketplace-5620.md b/.changeset/gate-listlocalinstalls-marketplace-5620.md deleted file mode 100644 index cb6c4165c..000000000 --- a/.changeset/gate-listlocalinstalls-marketplace-5620.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`MarketplacePackagePage`'s third fetch effect — `listLocalInstalls` — is now gated on -`marketplaceEnabled` and `isAdmin`, the same two predicates the page's other two fetch -effects already check, in addition to (not instead of) its existing -`features.installLocal` check (objectui#5620). - -`listLocalInstalls`'s only consumer is `localInstalls.find(...)` in the content branch, -which is unreachable whenever the page has already returned `MarketplaceDisabled` (no -marketplace on this runtime) or `MarketplaceAccessDenied` (a refused viewer) — both -decided ahead of the content branch since objectui#5533 and objectui#5583. Before this -fix, a runtime with `features.installLocal: true` still fired the request — and -discarded its answer — on a marketplace-off runtime and for a non-admin, the same -wasted-round-trip class objectui#5533 established the fix for on this page, on the flag -that card was not about. - -`features.installLocal` remains its own axis: a runtime can mount a local kernel install -path with no marketplace proxy at all, so the fix adds the two predicates as a -conjunction rather than replacing the existing check. The request still fires exactly -when it did before AND the viewer would actually see its answer. diff --git a/.changeset/governed-surface-human-merge-5149.md b/.changeset/governed-surface-human-merge-5149.md deleted file mode 100644 index 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<string, any>` (the MongoDB-style shape) and `isFilterAST` is false for a -plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one -answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. - -`toFilterNode` handles both without new logic: objects route through -`convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot -passes through untouched, so nothing is lowered twice. It also folds an absent or empty -source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` -used to send `$filter: {}`, asking the server a question with no content in a shape it -refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. - -**Grade — this is less dormant than the card assumed.** objectui#4082 was filed -observation-class on "no measured producer", reasoning that `defaultFilters` is not in -`object-grid`'s registered `inputs` so an author writing it only draws a save-gate -warning. That reasoning covers authors, but not the framework: `plugin-view`'s -`ObjectView` writes the slot itself, forwarding an active named view's `filter` as -`defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — -and `plugin-view`'s own README documents `listViews.<name>.filter` as -`[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as -`400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no -`renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, -and `ListView`'s lowering does not cover it. So a schema-registration host — the -documented authoring path — reached the raw assignment whenever an active named view -carried a filter. Not asserted here: a failing request captured against a running -deployment. `app-shell` is unaffected either way; it supplies `renderListView` and -delegates to `ListView`, which lowers. - -Not in scope, and deliberately not done: retiring `defaultFilters`. This is -consumer-side only — the key the schema admits is unchanged, and its precedence behind -the canonical `filter` is unchanged. - -The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the -measurements. diff --git a/.changeset/grid-dom-attribute-whitelist-4787.md b/.changeset/grid-dom-attribute-whitelist-4787.md deleted file mode 100644 index 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 `<div>` as invalid HTML attributes (objectui#4787). - -`grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and -`style`, so everything else `SchemaRenderer` hands a registered component reached the -element. Measured on a canary node, eight attributes leaked: -`columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, -`props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an -unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as -`columns="[object Object]"`. Layout was unaffected, so every catalog grid example -rendered with them — the reason this went unnoticed. - -The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist -objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the -SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, -`role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the -designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be -forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly -any more, including keys `GridSchema` does not have yet; enumerating today's keys to -strip would have re-rotted on the next schema addition. - -No authored input changes and no layout changes: the grid's own vocabulary was always -read off `schema`, never off these props. diff --git a/.changeset/grid-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/hollow-view-overlay-hydration-pin-5773.md b/.changeset/hollow-view-overlay-hydration-pin-5773.md deleted file mode 100644 index a389f4ca4..000000000 --- a/.changeset/hollow-view-overlay-hydration-pin-5773.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` is touched; the only file added is -`packages/app-shell/src/views/InterfaceListPage.hollowOverlayHydration.test.tsx`. - -objectui#5773 — pins the intended post-objectui#5233 behaviour at the seam the card -named: `InterfaceListPage`'s hollow-view hydration effect merging a personalization -overlay row it fetched through `listViewOverrides`/`getView` (the same undocked-narrowing -read path `narrowPersonalizationOverlay`'s docblock in `packages/data-objectstack/src/index.ts` -describes). - -Reachability was measured, not reasoned about — constructed through the REAL -`ObjectStackAdapter` write (`updateViewConfig`/`buildPersistedViewBody`, the same seam -`ObjectView.overlayPatchOnly.test.ts` uses) and the REAL `InterfaceListPage` render, not a -hand-written override fixture. Two cases: - -- A hollow ADR-0017 expansion item (system view, no `columns`) plus the CURRENT - (post-#5233) thin overlay row a toolbar toggle writes today: the page renders - `defaultColumnsFromObject`'s defaults — reachable, and the intended behaviour per the - card's disposition (an overlay was never a legitimate source of a view body). -- The same hollow view with a PRE-#5233 fat overlay row (the shape an install that has - not touched this view since before the fix is still carrying, per - `ObjectView.overlayPatchOnly.test.ts`'s own "PRE-FIX" framing): the page renders the - frozen stale columns instead — a control proving the pin above discriminates a real - hydration-effect outcome rather than passing vacuously. - -Confirmed load-bearing by reverse verification: with the hydration effect's guard -short-circuited (`if (true) return;`), the CONTROL case flips from the frozen `["status"]` -to the un-hydrated defaults `["name","status"]` — the assertion depends on the fetch + -merge actually running. No production code changed; `InterfaceListPage.tsx`'s hydration -effect already behaves this way today, this was untested rather than unreachable. diff --git a/.changeset/home-ai-studio-flag-5521.md b/.changeset/home-ai-studio-flag-5521.md deleted file mode 100644 index 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<ViewType, string>`, which is how `ViewSwitcher`'s own -`DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: -a future `ViewType` member fails `type-check` rather than silently rendering as -a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph -`DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback -stays for host props that carry an unrecognised type. Reached in practice by -the console, whose `CreateViewDialog` offers `tree` among the view types a user -can create. - -No authoring surface changes. `generateViewSchema` renders eight view types -while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of -them, so `tree` and `chart` are selectable only through the component's `views` -prop. The maintainer ruled on 2026-08-20 that both stay recorded as -host-composition-only rather than being added to those unions, following the -objectui#5097 precedent; the record now lives beside that one, with the branch -set derived from a source fence, the authored unions pinned at the type level, -and host reachability measured. diff --git a/.changeset/humanize-label-single-home-5444.md b/.changeset/humanize-label-single-home-5444.md deleted file mode 100644 index 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/issue-5474-static-table-narrow-types.md b/.changeset/issue-5474-static-table-narrow-types.md deleted file mode 100644 index 11b8b4627..000000000 --- a/.changeset/issue-5474-static-table-narrow-types.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -Split the static `table` column type off the rich shared `TableColumn` -(objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = -enforced holds per renderer. - -`TableColumn` is unchanged and remains the rich shape `data-table`, -`CRUDSchema` and detail-view relations honour. The static `table` renderer's -`TableSchema.columns` now declares the new narrow `StaticTableColumn` -(`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the -keys that renderer reads). The eleven keys the static renderer never read are -retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on -`TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / -`filterable` / `resizable` / `editable` / `cell` on its columns. - -Breaking for authored metadata that wrote those keys on a `type: 'table'` -node: they were silently inert before and are now refused loudly — a tsc -error on the interface (`?: never`) and a parse rejection naming the key in -`@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: -nodes that wanted the interactive behaviour move to `type: 'data-table'` -(whose columns keep the rich `TableColumn`); right-aligned columns on the -static table use `cellClassName: 'text-right'`; alternate-row styling uses -Tailwind on `className`. diff --git a/.changeset/issue-5619-workspace-admin-unresolved.md b/.changeset/issue-5619-workspace-admin-unresolved.md deleted file mode 100644 index a6dd79f81..000000000 --- a/.changeset/issue-5619-workspace-admin-unresolved.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/auth': minor -'@object-ui/app-shell': minor -'@object-ui/console': minor ---- - -Stop showing an access-denied screen to a real administrator while their -adminship is still resolving. - -`useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not -arrived yet" and "resolved: not an admin" were the same answer. One of its three -sources — the active organization member row — is fetched some round trips after -the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), -so an administrator whose adminship lives only in that row rendered at least -once as a non-admin, and every gate downstream acted on it: the two marketplace -surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and -re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" -replace>` that the later flip could not undo. - -**Breaking (published API, hence `minor` per this repo's version policy):** -`useIsWorkspaceAdmin(): boolean` is replaced by -`useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old -name is removed rather than kept alongside, so a call site that ignores the -third state fails to compile instead of silently refusing an administrator. - - -const isAdmin = useIsWorkspaceAdmin(); - +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); - -`AuthProvider` gains `isMembershipResolved` on its context — the organization / -member pipeline has reached a terminal state — because `organizations`, -`activeOrganization` and `activeMember` read `[]` / `null` / `null` both before -the pipeline starts and after it finds nothing. - -No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, -so an admin the session already identifies through `positions[]` never waits on -the member row. diff --git a/.changeset/issue-5709-unconsumed-widget-option-warning.md b/.changeset/issue-5709-unconsumed-widget-option-warning.md deleted file mode 100644 index c4a548574..000000000 --- a/.changeset/issue-5709-unconsumed-widget-option-warning.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/sdui-parser': minor ---- - -`validateTree` now reports a dashboard widget `options` key that no renderer -consumes as a `unconsumed-widget-option` **warning** naming the consumed set -(objectui#5709 ruling). The census behind the accepted set — the spec's five -declared query keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, -`stageOrder`) plus the `description` sub-caption convention key — is -re-measured on every test run against `@objectstack/spec` and the -`plugin-dashboard` renderer sources. The check fires only on dataset-bound -shorthand widgets (the spec-legal form) hosted by `dashboard` / -`dashboard-grid` nodes, and honours the spec's per-widget -`suppressWarnings: ['unconsumed-widget-option']` escape hatch. Warning -severity only: documents keep parsing, saving and rendering. Exported for -other surfaces: `checkDashboardWidgetOptions`, `CONSUMED_WIDGET_OPTION_KEYS`, -`DASHBOARD_WIDGET_HOST_TYPES`, `UNCONSUMED_WIDGET_OPTION`. diff --git a/.changeset/layout-renderers-dom-props-5574.md b/.changeset/layout-renderers-dom-props-5574.md deleted file mode 100644 index b018f67b4..000000000 --- a/.changeset/layout-renderers-dom-props-5574.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/components': patch ---- - -**Behaviour change:** the `flex`, `stack`, `container` and `text` renderers no -longer forward their whole prop bag to the host element. They route it through -`toDomProps` — the same whitelist `grid` was converged on — so an authored -schema key becomes an HTML attribute only if the SDUI DOM contract declares it -one (objectui#5574). - -What this stops reaching the DOM: the renderer's own declared props, which were -consumed off `schema` to build the class list AND spread onto the element a -second time as attributes HTML does not define. Measured across -`examples/schema-catalog`, rendered through the real `SchemaRenderer`: 1194 -illegitimate attributes over 1141 nodes — `text[content]` 522, `flex[align]` -198, `flex[gap]` 193, `stack[gap]` 153, `flex[justify]` 98, `container[padding]` -14, `container[maxwidth]` 6, `flex[direction]` 5, `stack[align]` 4, -`text[value]` 1. The same probe reads 0 after, with `grid`'s 26 nodes at 0 both -times as the control. - -Nothing an author writes renders differently: every leaked key was already being -read off `schema` and applied as a class, so the markup loses attributes that -never had meaning and keeps the styling that did. `id`, `role`, `tabIndex`, -`className`, `style`, event handlers and the open `data-*` / `aria-*` families -are unchanged — including `data-obj-id` / `data-obj-type`, which now arrive -through the `data-*` family rather than by hand. - -Anything that read one of the leaked attributes off the DOM — a CSS attribute -selector such as `[gap="4"]`, or a test asserting `align` on a rendered `flex` — -must read the schema or the class instead. No `@object-ui` code did; this is -called out because the attributes were externally visible while they lasted. diff --git a/.changeset/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 `<DetailView dataSource={…} />` callers are untouched), -`object-form` gains a prop form it did not have, and `object-grid` no longer -throws `useSchemaContext must be used within a SchemaRendererProvider` when a -page has no provider. - -And the silence is over. A block in this family that resolves no adapter renders -a **No data source resolved** panel naming the block, the object it was about to -read, and the ancestor that injects the adapter — instead of a header-only grid, -a field-less form card, or nothing at all. The check is opt-in per block, so a -placement with inline rows, inline `customFields`, an inline record or an `api` -endpoint is untouched. - -New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, -`noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. diff --git a/.changeset/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/nav-selection-root-keys-edge-5600.md b/.changeset/nav-selection-root-keys-edge-5600.md deleted file mode 100644 index 5fd1bb1b3..000000000 --- a/.changeset/nav-selection-root-keys-edge-5600.md +++ /dev/null @@ -1,25 +0,0 @@ ---- ---- - -Internal layering fix, behaviour-unchanged: publishes nothing, declared explicitly -with an empty frontmatter rather than left undeclared. - -Inverts the dependency edge objectui#5600 found: `nav-selection.ts` — otherwise pure -URL-parsing plumbing (`parseSurfaceParam` / `formatSurfaceParam` / `parseNavSelParam` -and friends) — statically imported `APP_NAV_ROOT_KEYS` from the 510-line -`AppNavInspector.tsx`, so every consumer of the URL helpers (including two small -utilities, `console/ai/artifactStudioPath.ts` and, transitively via -`useSurfaceDeepLink.ts`, the studio-design surface) pulled a React inspector -component into their static import graph for one array of root keys. - -`APP_NAV_ROOT_KEYS` (`['nav', 'navigation', 'tabs', 'items', 'menu']`) is a plain -string-literal array — it describes nav SHAPE, not the inspector — so it now lives in -`nav-selection.ts` instead. `AppNavInspector.tsx` never read the array itself, only -re-exported it (`export const APP_NAV_ROOT_KEYS = ROOT_KEYS;` as its last line), so -the fix is a straight move with no back-import needed on that side. - -Verified structurally rather than by reading the import line: a BFS over static -(non-type, non-dynamic) relative imports from `nav-selection.ts` and the other -non-test importers no longer reaches `AppNavInspector.tsx` (158 files visited, target -not found), where the same walk over the pre-fix tree found it in 6 files via -`nav-selection.ts -> AppNavInspector.tsx` directly. diff --git a/.changeset/node-gate-data-predicate-report-5687.md b/.changeset/node-gate-data-predicate-report-5687.md deleted file mode 100644 index 5465efc0e..000000000 --- a/.changeset/node-gate-data-predicate-report-5687.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`SchemaRenderer`'s node visibility gate now emits a dev-only warning when a -predicate reads `data.*` and the data-source adapter cannot answer that read, so -an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the -constant they authored instead of shipping it (objectui#5687). - -At the node tier `data` is the data-source **adapter** — the object `${data.total}` -in a props bag resolves against — and it has never been the row. A predicate -written with the deprecated `data.*` spelling therefore resolves `undefined == -'draft'`, which is a perfectly good `false`: the block is hidden on every row, and -because it does not throw, the unresolvable-predicate reporter added for -objectui#5454 never fired. Measured on this base, the same predicate written as a -`{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether -an author heard about the identical mistake depended on which dialect they happened -to write it in, which is the arbitrariness objectui#5454 existed to remove. - -**No verdict changes and no interpolation changes** (maintainer ruling, -2026-08-22, option A). The node tier keeps its documented `data` = adapter -semantics; objectui#5330's row binding does not extend here. The evaluator's answer -is returned exactly as computed and `${data.*}` interpolation is untouched — only -the silence moved. - -The report fires on a `data.*` read the bound adapter answers with `undefined`, not -on the spelling. A genuine adapter read stays silent (`data.total > 0` against an -adapter carrying `total`), a canonical `record.*` predicate stays silent, and a -correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per -node type + key + predicate source — the same module, Set and lifecycle as the -objectui#5454 reporter. - -This loudness is temporary by design: it dissolves when objectui#5330's deprecation -window for the `data.*` row spelling closes. diff --git a/.changeset/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/objectpivottable-i18n-resolver-refs-5625.md b/.changeset/objectpivottable-i18n-resolver-refs-5625.md deleted file mode 100644 index b5f4cc630..000000000 --- a/.changeset/objectpivottable-i18n-resolver-refs-5625.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -`ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel` -resolvers directly instead of holding them behind refs, so a pivot re-derives -its header and option labels when the resolver genuinely changes -(objectui#5625). - -The refs existed for a reason that no longer holds. `useSafeFieldLabel()` -returned a fresh object on every render outside an i18next provider, so a direct -dependency would have re-run the metadata-derivation effect on every render — -and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly -built objects, so each run scheduled the next one: an unbounded derive loop. -`ObjectPivotTable` worked around that locally with `fieldLabelRef` / -`fieldOptionLabelRef` plus a `useEffect` keeping them current. -`useObjectLabel`'s memo now holds with or without an i18next instance bound -(objectui#5564), so both resolvers have a stable identity on both paths and the -indirection buys nothing. - -It did cost something, and that is the user-visible half: a ref-hidden -dependency meant the derivation did NOT re-run when the resolver changed. A -pivot mounted before its `I18nProvider`, or rendered across a language switch, -kept showing its top-left header label and its select-option cell labels as -resolved by the old resolver — until some unrelated dependency (the data source, -the object name) happened to move. It now re-derives once on that transition and -renders in the active language. - -This is the same removal objectui#5587 made in `ObjectChart`, one package over. - -Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations -across forced re-renders both outside and inside a provider, checks that the two -derived state maps settle, and asserts the language-switch re-derivation. -Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the -no-provider case red (derivations in the dozens instead of 1), so the removal is -pinned to the fix that unlocked it rather than to a comment. diff --git a/.changeset/objectview-overlay-pair-completeness-5025.md b/.changeset/objectview-overlay-pair-completeness-5025.md deleted file mode 100644 index 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/objectview-sort-sink-4869.md b/.changeset/objectview-sort-sink-4869.md deleted file mode 100644 index 9408f9581..000000000 --- a/.changeset/objectview-sort-sink-4869.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/plugin-view': patch ---- - -**Bug fix:** a non-grid `object-view` (calendar / kanban / gallery / timeline) -whose sort came from the deprecated `table.defaultSort` no longer comes back -empty. `ObjectGridSchema.defaultSort` is declared a single `{ field, order }` -object, and `ObjectView`'s own fetch handed it to `$orderby` verbatim — where -the ObjectStack adapter reads it as an `$orderby` map and folds it with -`Object.entries`, so the request went out sorting by two columns literally -named `field` and `order`. The server answers `400 INVALID_SORT`, `ObjectView` -swallows the error, and the view rendered with no records — while the *same* -metadata sorted correctly as a grid, because `ObjectGrid` lowers that pair -before using it (objectui#4869, maintainer ruling 2026-08-22). - -`ObjectView` now performs the same legacy-to-canonical lowering `ObjectGrid` -already does (`sort ?? (defaultSort ? [defaultSort] : undefined)`) and routes -the whole chain — named view sort, `views` prop sort, `table.sort`, -`table.defaultSort` — through the shared `convertSortToQueryParams` sink. This -was the last object-bound read site sending an authored sort to `$orderby` -unlowered; every other block (gantt, map, calendar, timeline, -`record:line_items`) already did, so an adapter that implements `find` itself -now receives one normalized `Record<field, direction>` from all of them instead -of two different dialects. - -Precedence is unchanged, and both spellings of the pair keep working. The -shared sink was deliberately **not** widened to accept a bare `{ field, order }`: -its input slot also legitimately carries `$orderby`'s own map, in which -`{ field: 'desc' }` is a valid ordering by a column named `field`, so widening -it would make one shared function guess. - -Two visible shape changes on the wire, both semantically identical to before: a -string `table.sort` such as `'name desc'` now serializes as `-name` rather than -riding through as `name desc`, and a `SortConfig[]` arrives as a map rather than -as an array. diff --git a/.changeset/olive-donkeys-shave.md b/.changeset/olive-donkeys-shave.md deleted file mode 100644 index 6e765665f..000000000 --- a/.changeset/olive-donkeys-shave.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846). - -The flat branch now requests the `[]` marginal grouping — the same server-side -totals machinery the cross-tab has used since framework#1753 — and renders the -answer under the console's existing `dashboard.total` label. Totals are computed -by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/ -`max` reports the whole-set aggregate); nothing is recombined client-side. The -row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null -bucket, and never joins the drillable rows. - -No footer is rendered when the executor answers no `[]` grouping, and none is -requested for a table truncated by `options.limit`, whose visible rows a -whole-set total could not be reconciled against. diff --git a/.changeset/olive-donkeys-smile.md b/.changeset/olive-donkeys-smile.md deleted file mode 100644 index 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/one-origin-dev-configs-5745.md b/.changeset/one-origin-dev-configs-5745.md deleted file mode 100644 index a9ed98970..000000000 --- a/.changeset/one-origin-dev-configs-5745.md +++ /dev/null @@ -1,36 +0,0 @@ ---- ---- - -Dev/build configuration only — this publishes nothing, declared explicitly with an -empty frontmatter rather than left undeclared. No package `src/` is touched. The -files changed are `apps/console/.env.development`, `examples/console-starter/.env.*` -and `examples/console-starter/vite.config.ts`; `.env.development` is not part of -`@object-ui/console`'s published artifact (`files: ["dist"]`, built from -`.env.production`, which is untouched), and the example is private and on the -changeset `ignore` list. - -Converge the dev stacks on one origin: empty `VITE_SERVER_URL` and let the Vite -proxy do the split-host hop. - -Both dev env files pointed `VITE_SERVER_URL` at `http://localhost:3000` while the -page was served from `:5180` (console) and `:5173` (starter). Every client in these -apps coalesces an unset value to `''` and then builds a relative `/api/...` URL, so -an empty value routes same-origin through the dev proxy instead. `console-starter` -had no `server` block at all, so it gets the same one-stanza `/api` proxy -(`DEV_PROXY_TARGET` or `http://localhost:3000`) that `apps/console` already carries; -no port is pinned, because its README documents the example on Vite's default 5173. - -This is the ruled prerequisite for the `sameOriginOnly` action-runtime default. Under -that default a non-empty `VITE_SERVER_URL` makes every relative-target `type: 'api'` -action resolve cross-origin and be fetched bare — no `Authorization`, no -`X-Tenant-ID`, no `Accept-Language` — i.e. a 401 for the standard `pnpm dev` stack. - -`examples/console-starter/.env.production` is emptied too. Its committed -`https://demo.objectstack.ai` was not a deliberate choice for the example: it is the -same pre-convergence value the sibling console carried until 2026-05-24, when -`c351c9604` ("default published SPA to same-origin") cleared it there for CORS-blank --page reasons and touched only `apps/console/.env.production`. The starter was left -behind by that commit, and as a fork-ready scaffold its committed value is the -production origin every fork inherits. Split-origin deployments inject -`VITE_SERVER_URL` at build time, the way the console's own `.env.production` already -documents. diff --git a/.changeset/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 (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows -light/dark and whatever theme the deployment installs. The TSDoc now also names the -rule that reports a violation — `page-source-className-tailwind`, shipped in -`@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by -`os validate` as a warning on both tiers. - -No behaviour change, and the accepted `kind` set is untouched. - -`packages/components/src/renderers/layout/react-page.tsx` carries the same -correction on its two source comments (the injected-scope note and -`buildComponentScope`), and gains the styling note the file was missing. Those are -internal comments — they do not project into any `.d.ts` and change no export — so -they get no entry of their own; there is nothing an `@object-ui/components` -consumer could read in a CHANGELOG and act on. diff --git a/.changeset/page-source-tailwind-prose-retraction-5469.md b/.changeset/page-source-tailwind-prose-retraction-5469.md deleted file mode 100644 index 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 (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | -| `"react"` | Inline `style` objects. | - -Colors on both tiers come from the theme as `hsl(var(--token))`. - -Why each package has an entry — each was measured against its built artefact, not -assumed: - -- **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes - `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is - the paragraph that routes untrusted-author work to the `html` tier, and it - carried the retracted framing line-wrapped across `:17-18`. It also gains the - §Styling section it was missing — the absence is why the framing survived here. -- **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects - verbatim into the published `dist/types.d.ts`. -- **`@object-ui/components`**: the corrected header of - `src/renderers/basic/html-elements.tsx` projects verbatim into the published - `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm - comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a - function body) and is included here only because the same package already owes - an entry. - -No behaviour change: this is prose only. `CHANGELOG.md` occurrences are -deliberately untouched — immutable release history. diff --git a/.changeset/palette-canonical-discussion-5495.md b/.changeset/palette-canonical-discussion-5495.md deleted file mode 100644 index 18563f664..000000000 --- a/.changeset/palette-canonical-discussion-5495.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -**The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.** - -The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495. - -**The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered". - -One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one. diff --git a/.changeset/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/per-user-active-org-and-session-user-purge-5664.md b/.changeset/per-user-active-org-and-session-user-purge-5664.md deleted file mode 100644 index 45bdd142c..000000000 --- a/.changeset/per-user-active-org-and-session-user-purge-5664.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@object-ui/auth': patch -'@object-ui/console': patch ---- - -The active organization id is now stored per user, and a change of session user drops the -previous user's client state wholesale (objectui#5664). - -`auth-active-organization-id` was a single un-namespaced `localStorage` key while its -siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, -`flow-palette-recents:u:`). On a browser handed from one account to another — a shared -machine, a kiosk, a handover, a support session — the arriving user's console read the -PREVIOUS user's organization id. The header workspace chip rendered the previous user's -workspace for a user whose `organization/list` was empty, and the consequence past the -cosmetics is the one worth stating: the polluted org context suppressed -`RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so -a brand-new user on that browser silently never got the new-user flow at all. - -Nothing about row visibility rode on this. With the stale id the server answers -`403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero -environments; the damage was entirely in what the client believed about itself. - -Three changes, and the third is the one that closes the class rather than the instance: - -- The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention - its siblings already use. -- It can no longer be written un-namespaced at all. Where no session user is known yet the - value lives in memory for that page-load only — a namespacing that kept a bare-key - fallback would re-open the defect the first time a write happened before the user id - resolved. -- **A change of session user drops the previous user's client state wholesale.** This is an - allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so - the NEXT storage key someone adds without a `:u:` scope is covered before it is written. - Only device-scoped entries survive: the arriving session's own bearer token, the pointer - recording whose state the browser holds, and the UI theme. - -Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a -non-null `localStorage` read and falls back to the in-memory value, and the memory value is -nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge -too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- -sign-in path that never reloads the page. - -Existing browsers are not migrated. A value sitting under the retired bare key is -unattributable — nothing recorded whose org id it is — so migrating it is precisely the -defect it would be migrating away from, and it is deleted instead. A signed-in user loses -nothing durable: the active organization is a server-owned fact that -`AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no -active org is held, including the ADR-0081 single-membership repair. One boot re-supplies -it; users with no organization land on the guided first-run flow, which is the outcome this -card is about. - -`apps/console`'s pre-render auth preflight purges every spelling of the active-org key — -the retired bare one and each `:u:` scope — when it finds a dead bearer token, and -deliberately leaves the session-user pointer in place so the next sign-in can still tell -that the browser changed hands. diff --git a/.changeset/per-view-config-alias-fold-2890.md b/.changeset/per-view-config-alias-fold-2890.md deleted file mode 100644 index 1636fc20c..000000000 --- a/.changeset/per-view-config-alias-fold-2890.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`normalizeListViewSchema` now folds the four per-view-type config aliases phase 3 -carried over — `kanban.groupField` → `groupByField`, `kanban.cardFields` → -`columns`, `gallery.imageField` → `coverField`, `timeline.dateField` → -`startDateField` (objectui#2890). - -These are the pre-#2231 objectui spellings, kept declared alongside the spec keys -so stored view metadata would keep validating. They now fold at the same -component boundary as the A1–A5 vocabulary folds, in the same one-directional -shape: the canonical key wins when a config carries both, and the legacy key is -removed from the result so a missed read-site fails loudly instead of quietly -taking the legacy path. - -One rendering behaviour changes, and it is a correction of the same inverted -precedence the `densityMode` fold fixed: `ListView`'s kanban adapter resolves the -card field list as `cardFields || columns` — legacy over canonical — so a kanban -config carrying **both** rendered the legacy `cardFields` value and silently -ignored the spec-canonical `columns`. After the fold the authored `columns` is -what reaches it. Configs carrying only one of the two are unaffected, and every -other reader of these four keys was already canonical-first. - -`calendar.defaultView` is deliberately **not** folded: it aliases nothing and has -no spec counterpart, so it wants promotion upstream rather than a rename. diff --git a/.changeset/percent-figure-grid-citation-5608.md b/.changeset/percent-figure-grid-citation-5608.md deleted file mode 100644 index 7c6075e49..000000000 --- a/.changeset/percent-figure-grid-citation-5608.md +++ /dev/null @@ -1,34 +0,0 @@ ---- ---- - -Comments only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared (objectui#5608). - -Two live sites still carried the bare `27,581 of 1,200,013` figure with neither the grid -nor the measured surface named: the `DisplayNumberFormatOptions.style` doc comment and a -comment in the percentage-points test. Both were accurate. Neither was *reconcilable* — a -reader who had also seen `packages/fields`'s `27,577 of 1,200,003` met two counts that -look like a contradiction, and a figure a reader cannot reconcile is what makes a correct -comment get distrusted wholesale. That is the same defect class objectui#4596 was queued -for, and the wording here is lifted from its PR rather than phrased a third way — a third -phrasing for one fact is the defect one level up. - -Both counts stand unchanged. The disambiguator is the surface measured, so each site now -names it: objectui#4576's tie-dense grid (0.005 steps to 2,000, precisions 0/1/2) on -`formatDisplayNumber` / `formatMeasure`'s call shape, against objectui#4590's re-measure -of the same route through `formatPercent`. No value, assertion or behaviour moves. - -Why the empty form is the right declaration, stated from what was measured rather than -carried over from objectui#4596's PR — the two cases are mirror images. There, a function -BODY comment was changed, so the emitted `.js` moved and the `.d.ts` did not. Here the -changed source comment is the JSDoc on an exported interface member: the interface erases -at runtime, so the new prose reaches `dist/utils/number-display.d.ts` and is absent from -`dist/utils/number-display.js` (checked on a real build of the package). The second site -is a test file, excluded from the build program by `src/**/__tests__/**` and never in -`dist` at all. - -So a consumer-visible byte does move — the text shown on hover. That is documentation of -an existing measurement, not a change to the measurement, the type, or any behaviour: -nothing a consumer types against or calls is different, which is why this releases -nothing. Bytes moving is not the criterion; released behaviour and consumer-visible -surface are, and neither moves. diff --git a/.changeset/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 `<Input>`. A field-level `className` therefore lands -visibly on ordinary built-in controls, and a reader taking "exactly one" -literally concludes the opposite of what the code does (objectui#5131). - -The cell now describes the contract rather than the reader count: an undeclared -key still rides the props spread down to whichever component the field resolves -to, nothing in the contract promises that, and a registered widget honours it -only if it happens to spread its leftover props — the wording the docs site -already ships, so the two sources agree again. The advice in the row is -unchanged and was never wrong (`span` / `colSpan` for width, -`FormSchema.fieldContainerClass` for the grid), and the explicit -`section-divider` read is kept, now named as explicit. - -This is a documentation fix to a file `plugin-form` publishes to npm, which is -why it carries a version: the npm landing page only picks up the correction on a -release. No behaviour, export, type, or `dist` byte changes. diff --git a/.changeset/properties-template-visibility-diagnostic-5756.md b/.changeset/properties-template-visibility-diagnostic-5756.md deleted file mode 100644 index 5d050acf4..000000000 --- a/.changeset/properties-template-visibility-diagnostic-5756.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/react': patch ---- - -`SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated -spelling of the objectui#5454/#5687 diagnostics when it is written inside -`properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }` -(objectui#5756). - -The `properties.*` evaluation loop runs, and interpolates every `${…}` template it -finds, **before** the visibility gate ever sees the value — so by the time the -existing diagnostics ran, a template-spelled predicate had already collapsed into a -plain boolean and there was no predicate text left to inspect. The bare-string -spelling of the exact same gate (`properties: { visible: "data.status == 'draft'" -}`) was unaffected — nothing interpolates a string with no `${` in it — and was -already reported; only the template spelling was structurally invisible. - -Reached by moving the diagnostic check to inside the `properties` evaluation loop, -on the predicate's raw (pre-interpolation) text, gated on the key being one of the -six visibility keys the render chain consults (`visibleWhen` / `visible` / -`visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content` -interpolation, or any other non-visibility key, is untouched and stays silent. - -Reports only the key that actually **decides** the node's visibility, mirroring -objectui#5454's own leg semantics (its reporter is likewise only ever invoked on -the leg the chain's early-return sequence actually reaches): a `properties.visible` -template that a co-declared `visibleWhen` outranks is not reported for deciding -nothing. - -**No verdict changes and no interpolation changes.** The diagnostic call's return -value is discarded; the real verdict is still computed afterward, off the -post-evaluation, post-hoist schema, by the same code path as before this change. -Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data -predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate — -only the silence moved, one render-step earlier. diff --git a/.changeset/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/reanchor-adr-0124-d1-citations-5701.md b/.changeset/reanchor-adr-0124-d1-citations-5701.md deleted file mode 100644 index e176b1027..000000000 --- a/.changeset/reanchor-adr-0124-d1-citations-5701.md +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- - -Traceability only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. The change is comment text at eight live source sites; no -executable line moves, and `git diff -U0` carries zero non-comment added or removed lines. - -`server enforces, client is courtesy` was cited at those eight sites as the framework's -**`ADR-0057 D10`**. That decision reads *"Setup-nav surfacing follows the capability -(ADR-0029 K2); the object stays open"* — nav-entry tiering, not enforcement location. The -rule these sites actually invoke is decided by the framework's **`ADR-0124 D1`**, *"The -server is the enforcement point; client-side gating is a usability courtesy"* (Accepted -2026-08-18). #5699 fixed the repo-ambiguity half of this defect — whose ADR numbering the -citation meant — and deliberately left the anchor half; this is that half. - -The new anchor is derived from an authority, not chosen: the framework's own ADR-0057 -carries a note aimed at precisely this citation — *"If a citation of `ADR-0057 D10` -brought you here looking for that rule, ADR-0124 is where it is decided."* The substantive -claim at every site is unchanged; only the anchor moves. - -The disambiguating parenthetical #5699 shipped — *"framework numbering; this repo's own -ADR-0057 is an unrelated document"* — is retired along with the number it disambiguated. -It warned about a collision specific to `0057`; this repository has no ADR-0124 at all -(its own series stops at `0059`), and ADR-0124 records that a fresh, unambiguous number -was chosen so that citations of it would not need such a warning. The `the framework's …` -possessive stays at every site, so each one still says whose numbering it means. - -Left byte-untouched, deliberately: `packages/data-objectstack/src/appAccessProbe.test.ts` -cites the same decision for *"an app gated by an absent optional service"* — the -capability/service-gating family that decision genuinely does decide, so it is correct as -it stands. `docs/adr/0036-field-conditional-rules.md`, the wording all eight derive from, -carries the same misattribution and moves in its own PR: `docs/adr/**` is a governed -surface that stops at draft for human merge. diff --git a/.changeset/record-activity-scheduled-event-5840.md b/.changeset/record-activity-scheduled-event-5840.md deleted file mode 100644 index 011e1447b..000000000 --- a/.changeset/record-activity-scheduled-event-5840.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@object-ui/plugin-detail": patch ---- - -`record:activity` no longer discards scheduled activities. `sys_activity` rows with -`type: "scheduled"` now map to the `event` feed kind instead of being dropped before any -filter runs, so a not-yet-held meeting reaches the timeline and `types: ['event']` stops -being a permanently empty tab. Held meetings are unchanged: `completed` still maps to -`task` and still hides unless `showCompleted` is set, which is what keeps an upcoming -meeting visible by default while a finished one is not. - -The unknown-type default is unchanged and deliberately so — a row whose type nothing maps -is still dropped rather than bucketed into `system`, because rendering an unmeasured kind -as something it is not would be new wrong data rather than recovered data. What changes is -that the drop is no longer silent: an unmapped type now logs one `console.warn` naming it, -once per type. Types the map knows and deliberately excludes (`commented`, `mentioned`, -`login`, `logout`) stay silent, since those are decisions rather than gaps. diff --git a/.changeset/record-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-loading-frame-diagnostic-5776.md b/.changeset/record-alert-loading-frame-diagnostic-5776.md deleted file mode 100644 index e6b72b977..000000000 --- a/.changeset/record-alert-loading-frame-diagnostic-5776.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -`record:alert`'s `visible` predicate no longer logs a spurious `record is not -defined` evaluation-failure warning during the record-detail loading frame. - -The predicate is evaluated on every render (Rules of Hooks), including the -frame before `useRecordContext().data` has loaded. In that frame `record` is -unset, `usePredicateRecordContext` binds an empty context bag by design (no -`record` key at all), and a bare/`${…}` predicate referencing `record.*` -faulted with `record is not defined` — logged via `console.warn` on every -page load, even the correct, working ones, because the SAME predicate -resolved fine one frame later once `record` populated. The banner's own -visibility was never wrong (it already hides unconditionally while unloaded); -only the diagnostic was permanently misleading on the happy path. - -The predicate is now skipped while `record` hasn't loaded — the same -condition the banner's own "hide while unloaded" early return already used — -since its verdict in that frame was never consulted anyway. A predicate that -is genuinely broken (bad field, bad syntax) still faults, and still logs, on -every frame once `record` is populated (objectui#5776). diff --git a/.changeset/record-alert-row-binding-4807.md b/.changeset/record-alert-row-binding-4807.md deleted file mode 100644 index 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/record-picker-label-association-5771.md b/.changeset/record-picker-label-association-5771.md deleted file mode 100644 index df693b93c..000000000 --- a/.changeset/record-picker-label-association-5771.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `label` is now programmatically associated with its -`SelectTrigger` combobox — fixing a case where the caption the block's own -registration prose advertised (`'Caption rendered above the picker, in a -<label> element'`) named nothing at all (objectui#5771). - -Pre-fix, the render body was `{label && <label className="…">{label}</label>}` -against a `SelectTrigger` with no `id`: neither end of the association carried -any wiring. That is a step worse than the sibling gap objectui#5735 closed on -`element:text_input` — there the `label` half was already correctly wired and -only `description` was adrift; here the label element had no `htmlFor` and the -trigger had no `id`, so the picker had no accessible name unless a -`placeholder` happened to render text into the trigger's content. - -The fix follows the shape objectui#3341 already ruled on for the same defect -class (`InlineCreateRelated`'s `<label>`/`<Input>` pair) and objectui#5735 -just landed on the complement block: `htmlFor`/`id` against the -`SelectTrigger` — a `button` with `role="combobox"`, and therefore labelable, -so no `aria-labelledby` is needed (Radix sets none on the trigger) — wired -**when `schema.id` exists**, matching `element:text_input`'s precedent rather -than an always-on `useId()` fallback. Only the author can supply `schema.id`, -so a picker authored without one keeps its pre-fix, unassociated caption text -exactly as before; nothing about this change mints an id the author did not -provide. The `<label>` element itself is also swapped for the shared `Label` -(`@radix-ui/react-label`) primitive `element:text_input` already uses for the -same wiring, rather than a bare `<label>` with a `htmlFor` bolted on. - -The registration prose for `label` is corrected to describe what the code now -does (tied to the control by `htmlFor` when the node carries an `id`) instead -of a caption association the renderer never performed. diff --git a/.changeset/record-picker-label-placeholder-i18n-5637.md b/.changeset/record-picker-label-placeholder-i18n-5637.md deleted file mode 100644 index 58a25eaf3..000000000 --- a/.changeset/record-picker-label-placeholder-i18n-5637.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:record_picker`'s `label` and `placeholder` now resolve the inline -per-locale map their contract admits, and their published declarations say so -(objectui#5637). - -These are the two remaining keys of the trio objectui#5590 fixed one of. All -three members of `ElementRecordPickerPropsSchema` are the `I18nLabel` union -(`string | Record< string, string >`) — measured on the installed -`@objectstack/spec` 17.1.0 pin, where each resolves to -`optional -> union -> string | record` and -`safeParse({ object: 'account', <key>: { en, 'zh-CN' } })` succeeds. The -renderer honoured only the string arm on both, and the two keys failed in two -different ways: - -- `placeholder` was read raw and handed to `SelectValue`. React refuses a plain - object in that position rather than stringifying it, so the whole picker - subtree threw - `Objects are not valid as a React child (found: object with keys {en, zh-CN})` - — the same harm objectui#5590 removed from `emptyText`. -- `label` went through the file's local `toText`, whose object branch ends - `String(o.label ?? o.name ?? o.title ?? o.en ?? '')`. Reaching `o.en` - unconditionally is an English pick wearing locale resolution's clothes, so a - `zh-CN` viewer was shown the English entry — and a map that simply omits `en` - resolved to `''`, which the `{label && …}` render site drops, making the - picker's label element DISAPPEAR with nothing thrown and nothing logged. - -Both keys now resolve at their own read site through `pickLocalized`, the same -helper the settled `emptyText` shape uses. `toText` is deliberately unchanged: -it is shared with the row values (`toText(row?.[labelField])`), which are record -field values rather than `I18nLabel`, so teaching it locale resolution would -have changed a second, unrelated call site. The `placeholder` default is applied -before resolution, so an absent key still means "Select a record…" and an -authored empty string still renders empty. - -The two `ComponentMeta` entries, which held a single `'string'` arm precisely -because the renderer dropped the other one, now declare `['string', 'object']` -— declared in the change that makes the arm render, never before, which is the -order `ComponentInput.type` prescribes and the order `emptyText` set. - -KNOWN GAP, unchanged by this release: the sibling `label` read sites in -`renderers/layout/containers.tsx` compose -`translateLabel(pickLocalized(…), language)`, and that second helper is not -applied here — `translateLabel` and its `KNOWN_LABEL_DICT` are module-private to -that file. Only the locale-map resolution lands in this change; a plain-English -string `label` is still rendered verbatim in every language, exactly as before. diff --git a/.changeset/record-quick-actions-resultdialog-pin-5711.md b/.changeset/record-quick-actions-resultdialog-pin-5711.md deleted file mode 100644 index 351cbd3b8..000000000 --- a/.changeset/record-quick-actions-resultdialog-pin-5711.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Test-only change — no published behaviour changes. - -Adds a component-level test pinning that a `record:quick_actions` action's -declared `resultDialog` reaches the ambient `<ActionProvider>`'s shared -`ActionRunner` and opens the reveal dialog with the response value (success -toast suppressed), plus the negative leg — with no provider, the response is -silently discarded and the documented `console.warn` fires. Closes the gap -found while implementing objectstack#10681 (objectui#5711). diff --git a/.changeset/reference-rail-entry-spec-derived.md b/.changeset/reference-rail-entry-spec-derived.md deleted file mode 100644 index fd9b93bab..000000000 --- a/.changeset/reference-rail-entry-spec-derived.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@object-ui/plugin-detail": minor ---- - -**`ReferenceRailEntry` is now the spec's type, and the reference-rail `icon` key retires** (objectui#5494, maintainer ruling 2026-08-22). - -`ReferenceRailEntry` is owned by `@objectstack/spec` as of 17.1.0. The hand-written interface in `record-reference-rail.tsx` is replaced by a re-export of the spec's `ReferenceRailEntry` (derived from `ReferenceRailEntrySchema`, `$strict` over `{ objectName, relationshipField, title?, limit?, displayField? }`), and `buildDefaultPageSchema` no longer emits `icon` on the reference-rail entries it synthesizes. - -**Migration — if you write `icon` on `record:reference_rail` entries, remove it.** Be aware of what this does and does not change: - -- **Runtime validation does not move.** The spec's strict schema already refused `icon` at save — that refusal is unchanged. What was broken was objectui's declaration: the TS type advertised a key that could never be saved. -- **Nothing ever rendered `icon`.** No render path in `RecordReferenceRailRenderer` has ever read the key (measured back to the file's first commit, and independently by the spec's `ui-reference-rail-unknown-keys-refused` migration entry). An authored `icon` that survived in unsaved/preview metadata was already a silent no-op — after this change the TS type says so instead of suggesting otherwise. -- **The published TS surface narrows.** Code that imported `ReferenceRailEntry` from `@object-ui/plugin-detail` internals and set `icon` will now get a type error. That error is the contract speaking: remove the key. - -The reference-rail icon affordance is retired rather than proposed upstream: a stock scan of this repo, its examples and schema-catalog corpus, and the objectstack tree (examples, templates, docs) found no reachable authored usage of `icon` on reference-rail entries. Stored customer metadata is not reachable from this seat and was not scanned. diff --git a/.changeset/registerfunction-casefold-doc-5363.md b/.changeset/registerfunction-casefold-doc-5363.md deleted file mode 100644 index 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/remove-inert-showrefresh-5567.md b/.changeset/remove-inert-showrefresh-5567.md deleted file mode 100644 index 017478cc2..000000000 --- a/.changeset/remove-inert-showrefresh-5567.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/plugin-view': minor -'@object-ui/app-shell': patch ---- - -Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). - -The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. - -Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. - -`@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. diff --git a/.changeset/replay-dispatch-error-5695.md b/.changeset/replay-dispatch-error-5695.md deleted file mode 100644 index 8c014116b..000000000 --- a/.changeset/replay-dispatch-error-5695.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. diff --git a/.changeset/report-view-datasource-object-key-5116.md b/.changeset/report-view-datasource-object-key-5116.md deleted file mode 100644 index 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-codecov-upload-5436.md b/.changeset/retire-codecov-upload-5436.md deleted file mode 100644 index 66b7492a3..000000000 --- a/.changeset/retire-codecov-upload-5436.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -CI only — this publishes nothing, declared explicitly with an empty frontmatter rather -than left undeclared. - -`ci.yml`'s `Test (coverage)` job no longer uploads to Codecov. `CODECOV_TOKEN` was never -set on this repository and Codecov no longer accepts tokenless uploads, so the step failed -server-side (`Token length: 0`) on every push after objectui#5403 landed — a permanently -red lane that trains everyone to ignore red. Per the maintainer ruling of 2026-08-22 -(Option B), the upload is retired rather than the secret set: the merged report is now -published as the `coverage-report` build artifact (7 days, matching the shard blobs it is -derived from) alongside the existing step summary. - -The coverage GATE is untouched and was never Codecov's: the configured -`coverage.thresholds` are enforced by the merge step over the whole merged map, and the -shard legs still override them to zero so that merge step remains the only place the gate -runs. objectui#5403's loud verdict step is carried forward rather than deleted — it now -reports `shards=… blobs=… merge=… report=…` and is green only when all four shards passed, -all four blobs arrived, they merged, the thresholds were enforced over the result, and the -report was published. A threshold breach is annotated separately from a lane that never -delivered, because the two call for opposite actions. - -What is given up is the Codecov trend dashboard and its PR coverage comments. diff --git a/.changeset/retire-components-action-param-dialog-5685.md b/.changeset/retire-components-action-param-dialog-5685.md deleted file mode 100644 index 803fed535..000000000 --- a/.changeset/retire-components-action-param-dialog-5685.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/components': minor ---- - -Retire `ActionParamDialog`: the `custom` barrel's second action-param dialog is -removed, and the app-shell dialog is recorded as the surviving implementation -(objectui#5685, maintainer ruling of 2026-08-22). - -**Breaking for any out-of-repo host that imported it** (declared `minor` per the -repo's version-alignment rule — the major tracks `@objectstack`, never an -API-break count): `@object-ui/components` no longer exports `ActionParamDialog` -or `ActionParamDialogProps`. Measured at the branch point, the export had zero -production consumers — its only in-repo importers were its own five test files, -which retire with it, and no other repository in the organization imports the -symbol from this package. - -This file was the repo's SECOND implementation of the action-param surface, and -its audit trail is the reason it retires instead of being maintained: the last -close look (objectui#4758) found per-option `visibleWhen` not evaluated at all, -and the five hardcoded English strings this card originally recorded were the -next drift installment. A dormant second dialect of a governed surface is one -production import away from being live; removing it removes the whole drift -class. - -FROM → TO for an out-of-repo host: - -- `import { ActionParamDialog } from '@object-ui/components'` — no drop-in - replacement is published. The surviving implementation is - `@object-ui/app-shell`'s `ActionParamDialog` (`src/views/ActionParamDialog.tsx`), - rendered by app-shell's action runtime (`useConsoleActionRuntime`, - `RecordDetailView`) rather than exported standalone. A host that needs its own - param form builds on `@object-ui/fields`' shared field widgets - (`resolveFormWidgetType` / `getLazyFieldWidget`, ADR-0059) — the same seam the - surviving dialog renders through. - -The unreleased objectui#4758 changeset for this component (`select` options -through the shared option evaluator) is withdrawn in the same change: the -component retires before that fix ever ships, so the release notes carry the -removal rather than new behaviour of a surface this release does not contain. diff --git a/.changeset/retire-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-theme-switcher-preview-5647.md b/.changeset/retire-theme-switcher-preview-5647.md deleted file mode 100644 index 29633b09b..000000000 --- a/.changeset/retire-theme-switcher-preview-5647.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Retire `ThemeSwitcherSchema` (`type: 'theme-switcher'`) and -`ThemePreviewSchema` (`type: 'theme-preview'`) — the two remaining theme -component kinds, which no renderer implemented — together with -`ThemeUnionSchema`, the union that after objectui#5489 held only these two -members (objectui#5647). - -`packages/types/src/theme.ts` declared a theme-switcher control (`variant`, -`showMode`, `showThemes`, `lightIcon`, `darkIcon`) and a theme-preview panel -(`showColors`, `showTypography`, `showComponents`), and -`packages/types/src/zod/theme.zod.ts` published the matching Zod objects as -the two members of `ThemeUnionSchema` and therefore of `AnyComponentSchema`. -Nothing rendered either: neither literal appears at any -`ComponentRegistry.register(...)` / `registerLazy(...)` site in -`packages/*/src` (202 registered keys enumerated; positive control on the same -pipeline: `tooltip` → 1), nor in `PROTOCOL_COMPONENTS` / -`PALETTE_PLACEHOLDER_BLOCKS` -(`packages/components/src/renderers/placeholders.tsx`), and no fixture -declares either kind (control: `"type": "form"` → 81) — so a page declaring -one got the registry's "Unknown component type" panel (OBJUI-001), never a -switcher or a preview. Declared-but-unenforced, removed under the 2026-08-21 -maintainer ruling (option B) on objectstack#10485, extended to these siblings -by inheritance on identical evidence (objectui#5647). - -Removed from the published surface: the `ThemeSwitcherSchema` / -`ThemePreviewSchema` types (`@object-ui/types`), the matching Zod objects and -`ThemeUnionSchema` (`@object-ui/types/zod`), and the `ThemeSwitcherSchemaType` -/ `ThemePreviewSchemaType` inference aliases. `zod/theme.zod.ts` now exports -nothing and stands as the tombstone module. A schema spelling -`type: 'theme-switcher'` or `type: 'theme-preview'` is now REFUSED by -`AnyComponentSchema.safeParse` rather than accepted and then rendered as an -error panel, which is pinned by a test. - -**The theme system is unchanged.** `Theme` (the theme document vocabulary, -owned by `@object-ui/types` since objectui#5716), `ThemeEngine` -(`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are retained and -untouched — the rulings retain them explicitly. Author a theme as a document -handed to `ThemeProvider`; that path never went through the removed component -kinds. diff --git a/.changeset/retire-usebranding-hook-5368.md b/.changeset/retire-usebranding-hook-5368.md deleted file mode 100644 index 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 `<p>Rich <strong>text</strong></p>`, -and this repo's designer bridge already maps `richtext` onto its `html` type. The -display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose -sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops -raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the -cell body came out empty — with no error, no fallback and no console warning, so a -populated field read as an empty field and anyone auditing data through a grid -concluded the records were blank. Measured on the same stored bytes, a neighbouring -`html`-typed column rendered them correctly, which is what ruled out "the value never -arrived". - -`richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` -(script/style/iframe/object/embed blocks, inline event handlers and `javascript:` -URLs removed) and keeps everything a rich-text editor legitimately emits — headings, -paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at -once: the grid, the kanban card, the gallery, the related list, the dashboard record -panel and the record detail page all resolve their read-mode cells through this same -`getCellRenderer`. - -The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" -one type by moving every `markdown` cell's trust boundary, so `markdown` still drops -raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. diff --git a/.changeset/row-predicate-record-canon-5330.md b/.changeset/row-predicate-record-canon-5330.md deleted file mode 100644 index cfd76abde..000000000 --- a/.changeset/row-predicate-record-canon-5330.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/react': minor ---- - -Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now -warn once, and are unchanged otherwise. - -A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row -scope, a `record:alert`) has bound the row three ways since objectui#4075 — -`record.status`, bare `status`, and `data.status` — without any of them being -declared the contract. The maintainer ruled that question on 2026-08-20 -(objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for -the identical renderer-tolerance shape: **the canon is `record.*`**, and the -other two enter a deprecation window. - -The canon states the **server's** accept set, which was this card's first -measurement and turns out to be strictly narrower than the renderer's. Measured -against `@objectstack/formula@17.1.0`, the engine the server evaluates with: - -| spelling | server runtime | server authoring oracle | -|---|---|---| -| `record.status` | `{ ok: true, value: true }` | accepted | -| bare `status` | `Unknown variable: status` | refused | -| `data.status` | `Unknown variable: data` | **silently accepted** | - -`buildScope({ record })` mounts exactly `['record']` — `data` is never bound and -the row's fields are never flattened to top level. The three-way binding is a -client tolerance with no server counterpart, which is why the warning belongs on -this side. - -`data.*` is the dangerous one, and the reason the warning exists. `data` is in -`@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle -waves it through — that list is a deliberately generous "never faults" lint -baseline, not the runtime accept set. A `data.*` row predicate therefore passes -every authoring gate the platform has and then binds nothing at runtime: not an -error, a constant `false`. A `visible` that is constantly false is a button that -silently never appears — the objectui#4075 fail-closed signature. - -What ships: - -- `@object-ui/core` exports `detectNonCanonicalRowSpelling`, - `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and - `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, - which carries the canon statement and the measurement. -- Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and - `useCondition` (react, for bags bound by `usePredicateRecordContext`). -- Detection reuses the server's own oracles (`collectCelRootIdentifiers`, - `firstUndeclaredReference`) rather than a regex, so no second dialect - judgement is invented client-side. - -**No spelling is removed and no behaviour changes.** Every predicate that -resolved before resolves now — the ruling defers removal behind a stored-metadata -survey, and the warning is what makes that survey possible (ADR-0078: a -tolerance nothing ever reports can never be retired). - -The deprecation is scoped to the **runtime record layer**. `data` remains the -canonical root one layer over, in a metadata-editing form (ADR-0089 D3 -`CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. diff --git a/.changeset/runtime-config-server-side-pointer.md b/.changeset/runtime-config-server-side-pointer.md deleted file mode 100644 index 835e85199..000000000 --- a/.changeset/runtime-config-server-side-pointer.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Comment-only change to `app-shell`'s `runtime-config.ts`: the "Server-side" pointer block -now anchors on exported symbol + package (`RuntimeConfigPlugin` from -`@objectstack/cloud-connection`, `RuntimeConfigPlugin` from `@objectstack/objectos-runtime`, -`createStudioRuntimeConfigPlugin` from `@objectstack/service-cloud`) instead of file paths, -two of which had gone stale. No published behaviour changes. diff --git a/.changeset/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-parser-inline-whitespace-5661.md b/.changeset/sdui-parser-inline-whitespace-5661.md deleted file mode 100644 index 7affbea3d..000000000 --- a/.changeset/sdui-parser-inline-whitespace-5661.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/sdui-parser': patch ---- - -`kind:'html'` page sources keep the space between a text run and an adjacent -inline element: `A <strong>x</strong> page` now compiles to `A `/`<strong>`/` -page` and renders as `A x page` rather than `Axpage` (objectui#5661). - -The parser collapsed each text run's whitespace to a single space — correct, and -what HTML itself does — and then `.trim()`ed it, which is not: HTML collapses a -whitespace run to one space, it does not delete it. The deleted space was -precisely the separator between a run and its inline sibling, so every authored -sentence carrying emphasis or a link in the tier the guide recommends by default -rendered with its words run together. It was silent: the page rendered, the -structure was right, no diagnostic fired. - -The rule is deliberately mechanical rather than a block/inline taxonomy invented -for a schema tree that has none: keep one leading space when a sibling precedes -the run, and one trailing space when a sibling element follows it. The parent's -own start and end still drop their edge space, so `<p> hi </p>` is unchanged. - -Its one bounded cost: a whitespace-only run BETWEEN two siblings survives as a -single space, so a pretty-printed `<ul>` gains one `' '` string child per gap -between its `<li>`s — one space per gap, never the source's newline and -indentation, never at the container's own edges, and never inside an item's own -text. That bound is pinned by a test rather than left as a claim. diff --git a/.changeset/sdui-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md deleted file mode 100644 index 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 `<flex>`'s structured props plus JSON `style` -objects, the react tier with inline `style` objects, colours as -`hsl(var(--token))`), so it demonstrates what authors are told to write and now -follows the theme in light and dark. The two renderer-plumbing harnesses keep -their Tailwind and declare the exception in their headers; a test pins both -halves against the shipped `page-source-className-tailwind` rule. diff --git a/.changeset/secret-widget-spellings-5375.md b/.changeset/secret-widget-spellings-5375.md deleted file mode 100644 index 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/signout-purge-per-key-try-5777.md b/.changeset/signout-purge-per-key-try-5777.md deleted file mode 100644 index 8b5252c49..000000000 --- a/.changeset/signout-purge-per-key-try-5777.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's -`objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a -single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777). - -The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n` -aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed — -`AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are -the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198 -classifies a surviving entry as a cross-principal disclosure on a shared browser, not -mere staleness — so a partial sweep here is the sharper half of the same defect class -objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`). - -`Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on -its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is -not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()` -(objectui#5731) quarantines a key: this function does not own reads for the metadata -seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to -guard and nothing to quarantine — adding read-back verification would be a general -storage-error-handling refactor of the module, out of this card's scope. What is -mirrored is the reporting channel: a key whose `removeItem` throws is named in a -`console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is -discoverable instead of silent. - -Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata -key, asserting every other metadata key on both sides of it is still swept and unrelated -non-matching keys are untouched, plus a control that the warning fires only on an actual -failure. diff --git a/.changeset/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.<key>` the bound payload -does not carry, the warner names the missing key and then explained the cause: - -> Hidden (hidden: true) fields are stripped from detail payloads server-side, so a -> predicate gating on one may evaluate to a hide-by-default verdict. - -That cause is false, and it names a mechanism this repo does not own. `hidden` is a -UI concern — the framework spec describes it as "Hidden from default UI" -(`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against -the framework checkout rather than taken on trust: ObjectQL's own strip for the -`__search` companion documents that the `hidden` / `readonly` / `system` markers are -"None of them is a PROJECTION rule", which is precisely why a dedicated strip rule -had to be written for that one column; drivers answer a query with no `fields` using -`SELECT *`; and `metadata-protocol` enumerates what the read path does drop — -`internal: true` columns and the `__search` companion, and nothing else. The only two -read-side uses of `field.hidden` in the framework are auto-view/auto-form column -generation and companion-source eligibility, neither of which removes a key from a -record body. - -So an author who read this diagnostic went hunting for a `hidden` flag they would -either not find, or find on a field the payload demonstrably still returns — while -the real source of the sparseness (a projected or partial read) went unexamined. A -confidently wrong cause in a diagnostic is worse than no cause, because it is -actionable in the wrong direction. - -The replacement states the fact this surface can actually see and the consequence it -does own: the page bound a payload without those keys, a projected or partial read -will not carry them, and the predicate therefore fails closed and hides the action. -The measured half of the message — action name, missing fields, predicate source — -is unchanged, and nothing about what triggers the warning changed. - -Message text only. The same false claim also sat in this warner's own doc comments -and in the comments of the test that pins the message; both are corrected here, and -the docstring now carries an explicit note against re-attributing the cause to -`hidden: true`. No other call site was swept. diff --git a/.changeset/spec-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/spec-refresh-17-2-0-5668.md b/.changeset/spec-refresh-17-2-0-5668.md deleted file mode 100644 index 541883958..000000000 --- a/.changeset/spec-refresh-17-2-0-5668.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@object-ui/console': patch ---- - -Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains. - -**The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`. - -The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists. diff --git a/.changeset/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/studio-simplify-5813.md b/.changeset/studio-simplify-5813.md deleted file mode 100644 index c5860b40c..000000000 --- a/.changeset/studio-simplify-5813.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -Studio simplification, first cut (#5813): the Data pillar's object sub-tabs collapse to 记录/表单 with the five power panels (验证/钩子/操作/API/设置) behind one 「高级」 menu (the open panel's name wears the active pill, so the collapsed default never hides where you are); drafts now AUTO-save (debounced 1.5s, CEL-blocking gates the timer per the objectui#4306 rule, a failed save waits for the next edit) and the four 保存草稿 buttons are retired in favor of a quiet saving/last-saved hint; the 权限 pillar moves from the top-level row into a 「更多」 overflow whose items carry the same dirty-guard as the primary pillars — the /studio/:pkg/access route and every page behind it are untouched. diff --git a/.changeset/surface-context-send-1610.md b/.changeset/surface-context-send-1610.md deleted file mode 100644 index 0b92a2251..000000000 --- a/.changeset/surface-context-send-1610.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/app-shell': minor -'@object-ui/plugin-chatbot': patch -'@object-ui/i18n': patch ---- - -The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. diff --git a/.changeset/surface-deeplink-live-channel-5476.md b/.changeset/surface-deeplink-live-channel-5476.md deleted file mode 100644 index 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=<type>:<name>` deep-link that would have carried the author there -captures the URL exactly ONCE, at mount, and the sheet is opened over an -already-mounted pillar — so writing the param changed the URL and moved nothing. - -That mount-time capture is deliberate and stays exactly as it was: the mirror half -rewrites the param on every in-pillar selection, so a capture that followed the URL -would re-trigger its restore on each one. What was missing is a third half — a live -target delivered BESIDE the URL, which is what a producer already inside the pillar -needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity -(`{type, name}`), the host routes cross-pillar requests back through the URL (that -pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the -author declines over unsaved edits, and the mounted pillar applies the rest. - -Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next -rail reload would drag the author back off whatever they had since selected — the -regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` -pins a hand-picked object surviving a package switch, and -`surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it -still ignores every URL change after mount, and a live request never moves it. - -The sheet's other home is the Home / draft-preview bar, where the Studio object editor -is not a reachable destination at all. Reachability is answered structurally — the -producer hook returns `null` when no host published the channel — so off-Studio the -item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both -directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. - -Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its -signature, its return and its behaviour, and only the Data pillar subscribes to the -new channel. The channel is its own React-only module on purpose — importing the hook -into the sheet would have pulled `nav-selection` and the App-nav inspector into the -console's eager graph. diff --git a/.changeset/sweep-store-per-key-try-5763.md b/.changeset/sweep-store-per-key-try-5763.md deleted file mode 100644 index 615b71b68..000000000 --- a/.changeset/sweep-store-per-key-try-5763.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage` -state on a change of session user (objectui#5664 part 3) — now costs one key when a -single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763). - -The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key -`n` aborted the walk, so keys `n+1..end` were never swept, and the failure was -swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt` -believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the -next un-namespaced key — one nobody has written yet — cannot re-open the cross-user -pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys -survived depended on `Object.keys` iteration order rather than on anything bounded. The -previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their -permission-filtered app list, a cross-principal disclosure per objectui#5198) could all -land on the wrong side of the abort. - -`Object.keys(store)` — the reason a guard exists here at all — stays guarded on its -own; only the per-key guard is new, so one uncooperative key now costs exactly that key. - -Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is -NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its -one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a -quarantine are what keep a failed `clear()` from handing the value straight back. -`sweepStore` walks keys it does not own reads for — another package's recents cache, a -metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding -read-back verification for keys this function does not otherwise touch would be a -general storage-error-handling refactor of the module, which this card is scoped away -from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is -named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is -discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in -path, inside an `AuthProvider` effect) still cannot act on the failure and must not -throw either. - -A working `localStorage`/`sessionStorage` behaves exactly as before: every -non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist -(`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected. diff --git a/.changeset/switch-org-declared-reresolve-5750.md b/.changeset/switch-org-declared-reresolve-5750.md deleted file mode 100644 index 0a7646141..000000000 --- a/.changeset/switch-org-declared-reresolve-5750.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -`switchOrganization` now re-resolves identity for the organization it just switched -to by its own explicit decision, instead of depending on an accidental -`TokenStorage` side effect to notice the switch (objectui#5750). - -`AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity -re-resolved across a switch only because `POST /organization/set-active` happens to -return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from -the UNSIGNED `session.token` spelling `getSession()` normally stores — so -`TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription -calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That -signed spelling is deterministic on the raw session token, not on the organization: -two switches with no `get-session` landing in between produce the identical signed -value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity -is left answering for whichever organization the FIRST switch targeted even though -`activeOrganization` already reads as the new one. - -Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage" -link on an org card, plus its own "Back to organizations" button) — ordinary -client-side navigation with no full-page reload and nothing debouncing repeat -switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not -reachable paths for this: both force `window.location.href` immediately after a -successful switch, and the resulting fresh `AuthProvider` mount always performs an -authoritative `loadSession()` regardless of how the race above resolved. - -`switchOrganization` now tracks the organization it last resolved to itself and -re-resolves explicitly whenever a switch's target differs from that, while -suppressing the (now redundant) rotation notification for its own `set-active` -call — so the common single-switch path still spends exactly one `get-session`, not -two. A generation guard discards a still-in-flight, now-superseded switch's answer -rather than let it clobber a later switch's fresher one. diff --git a/.changeset/switch-org-identity-reresolve-5719.md b/.changeset/switch-org-identity-reresolve-5719.md deleted file mode 100644 index 48c7be059..000000000 --- a/.changeset/switch-org-identity-reresolve-5719.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -Tests only — this publishes nothing, declared explicitly with an empty frontmatter -rather than left undeclared. No package `src/` behaviour moves; the only files added are -`packages/auth/src/__tests__/switchOrgIdentityReresolve-5719.test.tsx` and this changeset. - -Pins that console identity **re-resolves across an organization switch**, and pins the -mechanism that actually provides it. - -objectui#5719 observed that `AuthProvider.switchOrganization` never calls `loadSession()`, -while the framework derives `user.positions[]` inside `customSession` from -`session.activeOrganizationId` — so leg 3 of `useWorkspaceAdminStatus` would keep answering -for the PREVIOUS organization, and because that hook is `leg1 || leg2 || leg3` with -`isResolved` short-circuited to true on any positive leg, a stale `org_owner` reads as a -CONFIRMED admin in the new organization. The card was filed as an observation because its -last step could not be settled from this repo. - -Measured against better-auth 1.6.28 driven standalone with the organization and bearer -plugins the framework always enables: `POST /organization/set-active` **does** hand back a -`set-auth-token`, so the objectui#4467 rotation subscription already re-resolves identity -and there is no defect. The mechanism, however, is an accident worth naming: the switch does -not mint a new session — `updateSession` writes only `activeOrganizationId` — it hands back -the **signed** `token.signature` spelling, while `createAuthClient.getSession()` stores the -**unsigned** `session.token`. Two spellings of one session, and `TokenStorage.set` notifies -on a value change. - -Nothing declared that. Normalising either lane onto a single spelling — a reasonable-looking -tidy-up — would silently stop identity re-resolving across an org switch, with the -over-permissive consequence #5719 described and no test anywhere to catch it. These cases -are that test, including the owner-of-both control (so the pin cannot pass on code that -merely stopped reporting admin) and a counterfactual that fixes what the guarantee rests on. - -No explicit `refreshSession()` was added: the rotation path already spends exactly one -`get-session` per switch, so an explicit call would make every org switch cost two. diff --git a/.changeset/table-renderer-declared-column-contract-5350.md b/.changeset/table-renderer-declared-column-contract-5350.md deleted file mode 100644 index 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/text-input-description-aria-describedby-5735.md b/.changeset/text-input-description-aria-describedby-5735.md deleted file mode 100644 index af1dc6abe..000000000 --- a/.changeset/text-input-description-aria-describedby-5735.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:text_input` now ties its authored `description` to the field with -`aria-describedby`, so assistive tech announces the helper text as the input's -accessible description instead of leaving it as an unassociated paragraph -beside the field (objectui#5735). - -Before this the paragraph and the input were siblings with no programmatic -relationship: a screen reader moving to the field announced the label and the -value and never the helper text. The `label` half of the same block was already -wired (`htmlFor` against the input's `id`), which is what made the gap specific -to `description` rather than a general absence of a11y wiring — and the -identical key authored on a field INSIDE `renderers/form/form.tsx` has been -announced all along, so one authoring key behaved two ways depending on which -container the author reached for. It no longer does. - -The paragraph's id is minted per instance with `React.useId()` — the same source -`FormItem` mints the form renderer's description id from — and deliberately not -derived from `schema.id`. The two associations in this block need ids on -opposite ends: `htmlFor` names the INPUT, whose id only the author can supply, -so that wiring still holds only when they gave the node an `id`; `aria-describedby` -names the PARAGRAPH, which the renderer owns, so the description association -holds unconditionally and cannot collide when two nodes share an authored id. -The attribute is emitted only when a paragraph is actually rendered — an absent -or empty `description` leaves the input with no `aria-describedby` rather than a -dangling reference. - -The key's published `ComponentInput` description, which documented this gap in -so many words, is rewritten in the same change. Its closing advice — prefer -`label` for an instruction a user must not miss — is kept rather than deleted, -on a new basis: a description is announced after the field's name and screen -readers gate description text behind verbosity settings a user can turn down, so -it remains the half of the announcement most likely to go unheard. diff --git a/.changeset/text-input-i18n-label-arms-5717.md b/.changeset/text-input-i18n-label-arms-5717.md deleted file mode 100644 index f8cbd403a..000000000 --- a/.changeset/text-input-i18n-label-arms-5717.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`element:text_input` now DECLARES the inline-translation arm on `label`, -`placeholder` and `description`, so the manifest gate stops reporting -`type-mismatch` on a locale map its own renderer has always resolved correctly -(objectui#5717). - -`@objectstack/spec` types all three keys as the `I18nLabel` union -(`string | Record<string, string>`) — measured on the installed 17.1.0 pin, per -key, from the schema's own verdicts — and `text-input.tsx` has resolved all -three through `pickLocalized` at their read sites since it was written. Only the -`ComponentMeta` entries stayed at a single `'string'` arm. Driven through the -same `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the -save gate use, an author writing `{ en: 'Owner', 'zh-CN': '负责人' }` got three -warnings on a write that renders correctly in the viewer's language: - -``` -<element:text_input> prop "label" expected a string -<element:text_input> prop "placeholder" expected a string -<element:text_input> prop "description" expected a string -``` - -That is the INVERSE of the direction the rest of this family moved in. -`element:record_picker.emptyText` (objectui#5590) and that block's `label` / -`placeholder` (objectui#5637) each held one arm for exactly as long as their -render site dropped the map, and gained the object arm in the change that taught -the render site to resolve it — never declare an arm the renderer drops. Here -the render site was never behind, so the same rule's other half applies: -withholding an arm the renderer resolves is the false declaration in the other -direction, and noise on legal writes trains authors (AI authors included) to -dismiss the `unknown-prop` and `type-mismatch` reports that are real. - -Per-key, not blanket. `defaultValue` on this same block keeps `['string', -'number']` — its contract has no object arm, measured the same way — and the -console specimen file keeps asserting that an `I18N_MAP` there still reports -`type-mismatch`. That control is what makes this a widening of three keys rather -than of a component. - -All three keys also gain a `description` written from what the renderer does -rather than from restating the contract: where each one lands in the rendered -output (a `<label>` above the field, the native `placeholder` attribute inside -it, a `<p>` below it), when each is omitted, and the locale fallback chain the -read site follows. No renderer behaviour changed. diff --git a/.changeset/theme-clientvalidation-dead-entry-5715.md b/.changeset/theme-clientvalidation-dead-entry-5715.md deleted file mode 100644 index 0f5648934..000000000 --- a/.changeset/theme-clientvalidation-dead-entry-5715.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Removed the dead `theme:` entry from `clientValidation.ts`'s `LOADERS` table, which -read `ThemeSchema` off `@objectstack/spec/ui` — a symbol the spec retired upstream -(objectstack#10485 / PR objectstack#10695, which deleted the whole `ui/theme.zod.ts` -module). `theme` was never a registered metadata type, so metadata-admin never asked -for it (objectui#5715). - -This is dead-code hygiene, not a bug fix, and it changes nothing an author can -observe. Two measurements say why, and both cut against the more dramatic reading: - -- The entry is not broken *today*. objectui's own `@objectstack/spec` pin (17.1.0) - still publishes `ThemeSchema` as a working strict Zod schema — measured by - importing the `ui` subpath and by ablation, where restoring the entry made - `validateMetadataDraft('theme', ...)` actively reject a draft. The skew is with - objectstack's `main`, not with what objectui installs, which is exactly why no - gate here ever went red. -- It could never have crashed. `getSchemaForType` duck-checks the loader's result - (`typeof schema.safeParse === 'function'`) and wraps the call in `try/catch`, so an - `undefined` export degrades to a silent no-op validator, never a throw. After a - future spec bump past the retirement that is the branch it would have taken. - -The one thing that was genuinely off: `hasClientValidator` answers from key presence -alone, so it returned `true` for `theme` while the validator behind it would have -judged nothing. Per its own docblock that combination makes `ResourceEditPage` treat -live client issues as the error source and suppress the server's `_diagnostics` — a -stored item rendering as clean. Unreachable in practice, but it is the reason the -entry is worth removing rather than leaving inert. - -The line is replaced by an absence note in the shape the file already uses for -`workflow` and `approval`, because re-adding this entry from the spec would -type-check and go green against the current pin. A metadata-admin theme editing -surface remains a separate capability decision. diff --git a/.changeset/theme-schema-doc-rewrite-5648.md b/.changeset/theme-schema-doc-rewrite-5648.md deleted file mode 100644 index 52c05730c..000000000 --- a/.changeset/theme-schema-doc-rewrite-5648.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Docs-only: rewrite `content/docs/core/theme-schema.mdx` around the retained theme -system (`Theme` from `@object-ui/types`, `ThemeEngine`, `ThemeProvider`) and delete -the page's three `check-doc-component-types.mjs` exemption entries (objectui#5648). -No published package changes. diff --git a/.changeset/theme-types-localized-5716.md b/.changeset/theme-types-localized-5716.md deleted file mode 100644 index 85b3fede0..000000000 --- a/.changeset/theme-types-localized-5716.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/providers': minor ---- - -Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. - -Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): - -- `Typography` — the shape lives on as the inline `Theme['typography']` member. -- `BorderRadius` — lives on as inline `Theme['borderRadius']`. -- `Shadow` — lives on as inline `Theme['shadows']`. -- `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. - -Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). - -The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. - -`@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). diff --git a/.changeset/theme-zod-retired-spec-reexports.md b/.changeset/theme-zod-retired-spec-reexports.md deleted file mode 100644 index 68023be60..000000000 --- a/.changeset/theme-zod-retired-spec-reexports.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Remove the six retired `@objectstack/spec/ui` theme-schema re-exports from `@object-ui/types/zod` — `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`, `ThemeDefinitionSchema` (the spec's `ThemeSchema`) — plus their `…SchemaType` inference helpers, and the `theme` / `mode` props of `ThemePreviewSchema` (zod and interface) that consumed them. - -objectstack#10485 (ADR-0049 enforce-or-remove, PR objectstack#10695) retired the spec's whole `ui/theme.zod.ts` module, and the maintainer's ruling on objectstack#10856 (Options A + C) has objectui remove the dangling imports first so the Console Pin Gate can build objectui against the framework tree; restoring the spec exports (Option B) was explicitly not taken. Breaking in effect for anyone importing those six names from `@object-ui/types/zod`: there is no replacement — the validators retired upstream with no successor. The theme TYPE surface (`Theme`, `ThemeMode`, `ColorPalette`, … re-exported from `@object-ui/types`) and the ThemeEngine/ThemeProvider runtime are unchanged. diff --git a/.changeset/tidy-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/tidy-jars-confirm.md b/.changeset/tidy-jars-confirm.md deleted file mode 100644 index d0e53b646..000000000 --- a/.changeset/tidy-jars-confirm.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Comment-only change in `@object-ui/core`: `ConfirmationHandler`'s declaration now -names the out-of-runner producer of its `options` bag, so a runner-scoped liveness -sweep no longer reads the parameter as declared-but-never-produced. No behaviour, -no type surface and no published output changes. diff --git a/.changeset/timeline-shared-safe-field-label-5623.md b/.changeset/timeline-shared-safe-field-label-5623.md deleted file mode 100644 index 58af0449f..000000000 --- a/.changeset/timeline-shared-safe-field-label-5623.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Internal dedup: `ObjectTimeline` now uses the shared `useSafeFieldLabel` from -`@object-ui/react` instead of its own local `useSafeObjectLabel` wrapper / -`OBJECT_LABEL_FALLBACK` (objectui#5623). No exported symbol changed and the -timeline's `fieldOptionLabel` call site behaves identically on both the -bound and unbound i18n paths — no published behaviour changes. diff --git a/.changeset/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/unified-pending-drafts-5801.md b/.changeset/unified-pending-drafts-5801.md deleted file mode 100644 index 126a5442a..000000000 --- a/.changeset/unified-pending-drafts-5801.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -One pending-drafts source of truth (#5801): `usePendingDrafts` / `fetchPendingDrafts` replace the five hand-rolled `GET /api/v1/meta/_drafts` copies behind the home banner, Studio topbar 变更/待发布 buttons, chat pending-drafts bar, chat draft-card resolver, and the preview watermark bar. Every successful publish path (`usePublishAllDrafts`, Studio `doPublish`, the chat bar) now emits the assistant bus's metadata-refresh pulse, and a chat turn that staged or replayed drafts emits it on completion — so a publish or an agent-staged draft anywhere converges every surface at once, and the home banner no longer needs its post-publish `window.location.reload()`. An errored drafts read now uniformly reports UNKNOWN (null), never a fake zero. diff --git a/.changeset/uniqueness-deprecation-jsdoc-4765.md b/.changeset/uniqueness-deprecation-jsdoc-4765.md deleted file mode 100644 index 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-overlay-write-patch-only-5233.md b/.changeset/view-overlay-write-patch-only-5233.md deleted file mode 100644 index 427d2ae8a..000000000 --- a/.changeset/view-overlay-write-patch-only-5233.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A view personalization overlay now stores **only the patch** — a column drag no -longer freezes the source view's filter into it (objectui#5233). - -`ObjectView`'s `persistViewPatch` wrote `{ ...baseViewDef, ...patch }`, so an -overlay written by a mere sort/density/column change copied the view's *current -effective* `filter` — along with its `columns`, `label`, `type`, `isDefault` — -into the stored row. The display merge is `{ ...source, ...override }`, so that -copy then outranked the source view forever: an admin edited the view's filter -and every user who had once resized a column silently kept the old filter, with -nothing anywhere reporting it. - -The write now goes through the exported seam `buildPersistedViewBody`. For a -**system view's overlay** the body is the patch plus `viewKind` when the active -tab carries one — identity, not content, and the same line the read side already -draws (`VIEW_OVERLAY_IDENTITY_KEYS`); `object`, `name` and the overlay marker are -stamped by the adapter. A **saved view's own row** is deliberately still written -whole: for it the body *is* the view, there is no source underneath it for a -copied key to shadow, and the write is a whole-document PUT — narrowing it would -delete the user's view rather than narrow it. - -This is the write half of the maintainer's 2026-08-12 ruling (objectstack#7494). -It was blocked until `columnState` was admitted to the view-metadata surface as -an explicitly runtime-only overlay key (objectstack#9933, released in -`@objectstack/spec` 17.1.0) — before that a `columnState`-only patch was refused -`422 INVALID_METADATA`, and the fat copy was the only thing supplying a -recognized key. The read half (`narrowPersonalizationOverlay`) shipped earlier -and stays: rows written before this land are still tolerated on read, and because -the write replaces the whole document, the next toolbar toggle also strips such a -row at rest. No migration. diff --git a/.changeset/view-switcher-chart-gantt-icons-5586.md b/.changeset/view-switcher-chart-gantt-icons-5586.md deleted file mode 100644 index 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/viewcolumn-identity-canonical-read-5344.md b/.changeset/viewcolumn-identity-canonical-read-5344.md deleted file mode 100644 index 4a7fcd06e..000000000 --- a/.changeset/viewcolumn-identity-canonical-read-5344.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/app-shell': minor ---- - -The metadata designer's View column inspector now reads a column's identity — -field key and label — in the ObjectStack canonical spelling only: `field` and -`label`. The legacy TanStack aliases `accessorKey` / `header` are no longer -consulted (objectui#5344). - -Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both -legacy keys by name (`unrecognized_keys`), so a column carrying them has no -field key the spec recognises. The inspector nevertheless read them and -displayed `accessorKey`'s value in the field-key box, presenting a spec-refused -key as though it were a valid column identity — the same consumer-side -tolerance alias `ObjectGrid` retired in objectui#5068, surviving one layer up in -the authoring tool, which is the surface that is supposed to teach the correct -shape. - -**What an author sees:** a stored column shaped `{ accessorKey, header }` now -shows an EMPTY field key and an empty header, and is re-authored rather than -silently carried. Canonical `{ field, label }` columns and bare-string columns -are untouched. - -**What is deliberately NOT changed:** the writeback. `patchIdentity` still -re-emits whichever spelling it was handed, so no stored document is rewritten -by the act of editing it. Normalising on write was ruled out once the -maintainer confirmed there is no population of legacy stored documents to -migrate; such a column stays unsaveable against the spec both before and after -an edit, exactly as it did before this change. What this removes is the -invisibility of that state, not the state itself. diff --git a/.changeset/visiblewhen-record-binding-5454.md b/.changeset/visiblewhen-record-binding-5454.md deleted file mode 100644 index 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.<var>`"*. `SchemaRenderer` bound no `record` at all. Its -evaluator was built from the ambient predicate scope, `data: dataSource` (the -connector **adapter**, not the row) and `page: pageVariables`; the row lives in -`RecordContext`, which that evaluator never read. - -Because the surface is fail-soft, a `record.*` predicate did not misfire — it -resolved to **shown**. Both polarities of the same predicate returned the same -verdict, so a visibility gate silently did not gate, on every block on every -record page. Measured on `record:alert`, `record:path`, `page:card` and -`element:text`. - -Three changes, all in `SchemaRenderer`'s evaluation memo: - -- **`record` is bound**, as the `record` root only — the three roots the - describe promises and nothing more. Not as bare fields, and never over - `data`, which is what `${data.*}` in a props bag resolves against. Bound - conditionally, so "no row" binds nothing rather than shadowing a `record` a - host supplied through the ambient scope. -- **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` - onto the node, so a node carrying `properties.visible` short-circuited the - declared node predicate — the one key the spec tells authors to write was the - one key that could be silently ignored. The two deprecated aliases - (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into - `visibleWhen` at parse, so a spec-parsed page never reaches them. -- **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this - predicate is broken" and "this predicate said yes" with the same word. The - verdict is unchanged on every path — `evaluateCondition` already returned - `true` for every unevaluable predicate, including the non-negated `hidden` / - `hiddenOn` legs where that `true` means HIDE — so only the silence moved. - -**Behaviour change, stated plainly:** a shipped page whose node-level -`record.*` predicate was previously inert now evaluates. A block that was -permanently visible may begin to hide — which is the point, but it is a verdict -change, not a no-op. `properties.visible` is unaffected in verdict: an -in-tree census found **zero** node-level `record.*` predicates on page -components, so nothing in this repository changes verdict. diff --git a/.changeset/vitest-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 ef00feb7f..db1b13d78 100644 --- a/QUICK_REFERENCE.md +++ b/QUICK_REFERENCE.md @@ -121,7 +121,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..1a0d7b343 100644 --- a/apps/console/CHANGELOG.md +++ b/apps/console/CHANGELOG.md @@ -1,5 +1,685 @@ # @object-ui/console +## 17.7.0 + +### Minor Changes + +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. + +### Patch Changes + +- 9b9af8d: The Approvals Inbox stops remounting every row on every render. + + `ApprovalsInboxPage` declared `RequestCell`, `RecordCell` and `InlineActions` + inside its own component body. React identifies a component by the identity of + its function, so each render produced three brand-new component *types* and React + unmounted and remounted every row's subtree instead of updating it — and the page + holds its clock in state and ticks it every 60s, so this fired on a timer whether + or not anyone was touching the page (objectui#5348). + + Two consequences were reproduced against `origin/main` before the fix, in + `apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx`: + + - **Transient subtree state is discarded.** Focus placed on a row's Approve + button moved to `<body>` on the next clock tick. + - **Input is silently swallowed.** A pointer sequence that spans a re-render — + press, tick, release — left the confirmation dialog unopened: the captured node + had been replaced, so React's delegated listener never saw the click. This is + the failure objectui#5211 hit and worked around at its call site + (`Unable to find role="alertdialog"`). + + The three cells are now at module scope beside `StatusBadge`, which was moved + there for the same reason and already carries the explanation. Everything they + closed over is passed in: `RequestCell` and `InlineActions` take the page's + scoped translator, and `RecordCell` takes `href: string | null` — one prop rather + than two, so the objectui#5211 readable/unreadable decision and the URL cannot be + handed in disagreeing with each other. + + The verification asserts the consequence, not the placement. A test that checks + the three functions now sit at module scope stays green for a refactor that moves + them and introduces a fourth inline component beside them; these cases compare + DOM-node identity for all three cells across a clock tick, which no remount can + pass, and re-drive the swallowed click. + + That guard is load-bearing because lint cannot supply one here. + `react-hooks/static-components` exists for exactly this class and is `error` in + this repo via the plugin's recommended set, yet it reports nothing on this page: + measured on `origin/main`, an arrow-form inner component injected into + `ApprovalsInboxPage` and used in JSX produced **zero** reports, while the same + shape in a ten-line file produced two. The rule's analysis bails out on this + component, which is how three of them shipped. +- 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. +- 07cd685: The console's form routes no longer seed a RUNTIME `defaultValue` into the + control, so the server-side resolution the declaration asks for actually happens + (objectui#5727). + + `readPrefill` in `apps/console/src/components/FormPage.tsx` seeded every declared + default unconditionally. A `defaultValue` may be a literal, or an *instruction* + the server resolves per insert — a `DEFAULT_VALUE_TOKENS` token (`NOW()`, + `current_user`) or a CEL Expression envelope. Seeding one of those literally put + the text `NOW()` into a datetime input on both `/forms/:name` and the public + `/f/:slug` route, and submitting it sent that string as the field's value — + which is neither absent nor null, so `ObjectQL.applyFieldDefaults` never resolved + the declared default and the column stored the token text instead of a timestamp. + + The seed is now guarded by `isRuntimeDefault` from `@object-ui/core` — the same + published classifier `@object-ui/plugin-form`'s `schemaDefaults.ts` guards its + seeding with, and the one this renderer already reads once removed (through + `isServerOwnedValue`) for the create-mode `required` carve-out. A runtime default + leaves the key ABSENT rather than empty, because absent is precisely the case the + engine resolves. + + Nothing else about the prefill precedence moves: a literal default still seeds, a + stored record value still wins over a default, and an explicit `prefill_<field>=` + param still wins over both — including for a field whose default is a runtime + token, since a value a producer supplies is not a declaration awaiting + resolution. +- b63a9a3: The console's standalone form renderer now evaluates conditional field visibility. + + `apps/console/src/components/FormPage.tsx` is a **second, independent form renderer** + — its own `buildSections`, its own JSX — and it serves both the public + `/f/:slug` route and the internal `/forms/:name` route. It read neither spelling of + the FormView field visibility predicate: a repo-wide grep for a `visibleWhen` / + `visibleOn` *read* inside that file returned zero. So a field an author conditioned on + `record.priority == 'urgent'` — legal, spec-strict metadata that `@objectstack/spec` + normalises to `visibleWhen` (ADR-0089), and that the metadata-admin designer both + authors and honours — rendered unconditionally on both routes. Fail-open and silent: + the author saw the field always, with no diagnostic. + + objectui#2212 recorded this exact symptom and PR #2214 fixed it — in a **different + chain**: `ModalForm` → `resolveFormViewLayout` → `@object-ui/plugin-form` + `sectionFields.ts` → `@object-ui/components` `renderers/form/form.tsx`. `FormPage.tsx` + is on that chain at no point, and #2212's regression pin lives with the chain it fixed, + so nothing in the suite could see this copy. One contract, two implementations, each + only ever checked against itself. + + The wiring is **#2212's ruling applied verbatim** rather than a second predicate + semantics invented for this renderer, because two form renderers disagreeing about what + `visibleWhen` *means* would be a worse defect than one renderer ignoring it. The + predicate goes through the canonical engine — `evalFieldPredicate` (`@object-ui/core`, + `evaluator/fieldRules.ts`) — so the accepted wire shapes (bare CEL string and + `{ dialect, source }`), the bound scope (`record.*` = the live input values, `previous.*` + = the stored record an edit form started from), and the fail-open-but-loud behaviour on + an unevaluable predicate are the shared ones by construction. Resolution is + canonical-first, `visibleWhen ?? visibleOn`, matching both sibling readers: + `sectionFields.ts` and app-shell's `readVisibility`. + + Two things deliberately did **not** change. A field hidden by its predicate still + submits its value — conditional visibility is a rendering rule in both renderers, and + making it a submit-payload rule would be a new contract decided once for both, not + invented in the second one. And `FormPage` is **not** folded onto the plugin-form chain: + the second-renderer question is real, but it belongs with the #5596 convergence track, + not with a predicate that is dead today. + + `FormPage.visibleWhen.test.tsx` is the regression pin, and it lives next to *this* + renderer on purpose — a pin that cannot see the second copy is how the first gap + survived. With the fix reverted and the pin in place the suite reports + `11 failed | 1 passed (12)`; the one green is the control that has to be green (a field + with no predicate still renders), without which every "the field is absent" assertion + would be equally satisfied by a renderer that draws nothing at all. +- fb934fb: The console's `/docs` portal is code-split for real: its four pages leave the eager closure instead of only pretending to. + + `AppContent.tsx` lazy-imports `DocsLayout` / `DocsSlug` / `DocPage` for the + app-scoped `/apps/:packageId/docs` tree (ADR-0048). `App.tsx` imported the same + three statically for the platform portal at `/docs` (ADR-0046 section 6), so all + of them sat in the eager graph regardless and the `import()` moved nothing — + three `INEFFECTIVE_DYNAMIC_IMPORT` warnings on every `vite build` + (objectui#5467). A static import on either side silently defeats the split for + both, and the only signal is a build warning that fails nothing. + + `App.tsx` now reaches all four docs pages through `lazy()` behind `Suspense`, + matching the pattern `AppContent.tsx` already uses. `DocsIndex` joins them even + though it carried no warning: `AppContent` renders `AppDocsIndex` at that slot, + so nothing imported `DocsIndex` dynamically, but left static it alone would keep + `DocShell`, `use-book-data` and `book-nav` eager and the portal would only + half-leave the closure. + + Measured on this branch with the `dist/eager-closure.json` gauge added by + objectui#5324, both builds exiting 0: + + | | before | after | + |---|---|---| + | `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 46 | 44 | + | eager closure, gzipped | 3,881,609 B | 3,870,058 B | + | eager chunks | 58 | 52 | + + Six chunks leave the eager closure: `plugin-markdown` (4,212 B gz), + `CreateViewDialog` (3,617 B), `use-book-data` (1,966 B), `DocShell` (476 B), + `componentRegistry` (99 B), and `src` (129,555 B), the last of which rolldown + folds into the entry chunk rather than dropping — which is why the entry chunk + grows from 25,910 to 154,378 B gzipped while the closure as a whole shrinks by + 11,551 B. The entry stays far under that budget's 350 KB line, and the eager + closure is the number a page load actually pays. + + What does NOT move is `vendor-markdown`, 164,708 B gzipped and the reason this + looked like a bigger win than it is. Three eager chunks import it statically, + and only one of them was this portal: `plugin-chatbot` reaches it directly, and + `packages/fields`' `MarkdownContent` — lazy in source — is folded into the + eagerly imported `ui-components` chunk by the `advancedChunks` group that claims + every `packages/fields` module. That is objectui#5325's mechanism, not this + card's, and it is why the saving here is 0.30% rather than 4%. +- 8549453: The console's pre-boot branding script now resolves its server origin from + `VITE_SERVER_URL` — the same variable every module-side consumer reads — instead of + `window.__CONSOLE_SERVER_URL`, a global nothing in this repository ever set + (objectui#5660). + + The two callers of `GET /api/v1/runtime/config` share one in-flight request, and + that sharing keys on the FULL URL. So the origin was half of the contract: the + inline script in `index.html` read one spelling and `src/main.tsx` read another, and + whenever they disagreed the two never found each other. A same-origin production + build hid it completely — both spellings collapse to `''` — so the split surfaced + only in a dev pointing the console at a separate server, where it cost two requests + to two different servers and let pre-boot branding paint from the wrong one. + + The pre-boot fetch is kept, not deleted: it is the request the module side JOINS. + `sharedGetJson()` can only hand an earlier request to a later caller, and this + script is the earlier one by construction — it runs during HTML parse, before the + bundle is fetched, while `initRuntimeConfig()` is awaited before + `createRoot().render()`. Deleting it would not remove a request; it would move the + single remaining one later, onto the critical path to first paint, and leave the + page's empty `<title>` and favicon unbranded until React mounts. + + Vite substitutes its HTML env token only when the variable is set and leaves it + verbatim otherwise, so the unset case is read as same-origin `''` rather than + allowed to reach the URL as a path segment. +- 7493bff: `registerStudioComponents.tsx` no longer claims a code split it never had: `studio:builder` imports `BuilderLanding` directly instead of through a `lazy()` that deferred nothing. + + The registration wrapped `import('@object-ui/app-shell')` in `lazy()` behind a + `Suspense` fallback — naming the same barrel the line above it imports + statically for `registerAppComponent`, and the same barrel `App.tsx` pulls + `BuilderLanding` from to render the standalone `/studio` landing full-screen. + Either reason alone makes the `import()` unable to move a module into another + chunk (objectui#5486). + + **This moves no modules and is not a bundle improvement.** `BuilderLanding` was + already in the eager graph via `App.tsx` and still is. Measured on + `dist/eager-closure.json`, both builds exiting 0: the eager closure holds the + same 52 chunks with the same names, and the only difference is 130 B gzipped + (413 B raw) off the entry chunk — the deleted `lazy()`, `Suspense` and fallback + text themselves, 0.003% of a 3,875 KB closure. Nothing leaves the closure, + because nothing could. + + What it does fix is honesty. The old code told every reader the builder was + deferred, and it emitted an `INEFFECTIVE_DYNAMIC_IMPORT` warning on every + console build — the console's count of those drops from 44 to 43, with the 43 + remaining ones all belonging to the `packages/fields` barrel (objectui#5325). + A permanent warning that fails nothing is how a team learns to skim past build + warnings, and a decorative `lazy()` is how the next reader learns something + false about the chunk graph. + + The `lazy()` shape is not the mistake. The sibling `registerAccountComponents.tsx` + lazy-imports `./pages/system/ProfilePage`, a specifier nothing else pulls in + statically, and is genuinely deferred; it is untouched. Making the *builder* + genuinely lazy would mean taking `App.tsx` off the static import too, changing + how `/studio` mounts, and it only pays if app-shell's own graph cleaves behind + the barrel — a separate measured card, not folded in here. +- d8afbe5: `FormPage` — the console's own form renderer, serving both the public `/f/:slug` + route and the internal `/forms/:name` one — now honours the three conditional-rule + surfaces it still dropped after objectui#5594: section-level `visibleWhen` / + `visibleOn`, and the object-level field rules `visibleWhen` / `readonlyWhen` / + `requiredWhen` (objectui#5627). + + This is the second form renderer in the repo, and it honoured exactly one of the + four surfaces the sibling chain does. A section an author conditioned away rendered + in full — heading and every control — on both routes including the anonymous one. + The object-level half was worse than fail-open hiding: `readonly` was whatever the + static flag said, so a field a `readonlyWhen` should have locked stayed editable and + paired with the server's fail-closed unbound-scope behaviour into "the user edits, + the save reports success, and the value never lands". + + Both halves evaluate through the SHARED machinery rather than a fourth consumer-side + copy of the rule semantics: `@object-ui/core`'s `resolveFieldRuleState` for the three + field rules — which brings the settled rulings with it, including the `serverOwnedValue` + carve-out that keeps a create form from requiring a producer-owned control (#4069 / + #4085) — and its `evalFieldPredicate` for the section predicate, with the canonical-first + `visibleWhen ?? visibleOn` read every sibling reader spells. + + Visibility stays a RENDERING rule at both granularities: a hidden section's fields + still submit their values, exactly as a hidden field's have since #5594. +- cdda37a: `buildSections` now honours a FormView field's `maxLength` override instead of always + taking the object's ceiling (objectui#5595). + + The function merges a form's field overrides with the target object's field definitions, + and its own docstring states the rule: *"Field-level FormField overrides take precedence + over object defaults."* Every key in the loop is built that way — `override.label ?? + def.label`, `override.required ?? def.required`, `override.placeholder ?? def.placeholder` + — except one, which read `def.maxLength` unconditionally. So an author who set a tighter + per-form limit (a short public intake form over a column whose object-level ceiling is + generous) got the generous one. + + The failure was silent in the worst direction: no diagnostic, no warning, and the form + still submits, so the symptom is a value the author believed the input refused being + accepted. It is load-bearing rather than cosmetic — the merged row reaches the DOM at two + `maxLength={field.maxLength}` sites, the `textarea` arm and the default `input type="text"` + arm. + + `override.maxLength ?? def.maxLength` — `??` rather than `||`, matching the sibling keys, + so an explicitly declared `0` stays a value the author wrote rather than falling through + to the column's ceiling. This narrows only what the input allows; the object's storage + ceiling still decides at submit time, so nothing that was accepted before is now rejected + anywhere but at the keyboard. + + Why it survived: the console's local `FormFieldSpec` did not declare `maxLength` at all + until objectui#5542, so no one typing a spec in this app could write the override in the + first place, and the inert merge branch was never exercised. #5542 converged that type + onto the shared app-shell declaration, which does declare the key — making the gap + expressible, and therefore findable. + + The pin `#5542` left behind — `expect(row.maxLength).toBeUndefined()` in + `FormPage.fieldSpec.test.ts`, which recorded the old answer explicitly rather than + assuming it — is **inverted** to `toBe(40)` rather than deleted. It was the pre-registered + evidence for this fix, and it is what made the gap findable in the first place, so it + keeps its place and names the honoured answer. +- 343c598: The active organization id is now stored per user, and a change of session user drops the + previous user's client state wholesale (objectui#5664). + + `auth-active-organization-id` was a single un-namespaced `localStorage` key while its + siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, + `flow-palette-recents:u:`). On a browser handed from one account to another — a shared + machine, a kiosk, a handover, a support session — the arriving user's console read the + PREVIOUS user's organization id. The header workspace chip rendered the previous user's + workspace for a user whose `organization/list` was empty, and the consequence past the + cosmetics is the one worth stating: the polluted org context suppressed + `RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so + a brand-new user on that browser silently never got the new-user flow at all. + + Nothing about row visibility rode on this. With the stale id the server answers + `403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero + environments; the damage was entirely in what the client believed about itself. + + Three changes, and the third is the one that closes the class rather than the instance: + + - The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention + its siblings already use. + - It can no longer be written un-namespaced at all. Where no session user is known yet the + value lives in memory for that page-load only — a namespacing that kept a bare-key + fallback would re-open the defect the first time a write happened before the user id + resolved. + - **A change of session user drops the previous user's client state wholesale.** This is an + allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so + the NEXT storage key someone adds without a `:u:` scope is covered before it is written. + Only device-scoped entries survive: the arriving session's own bearer token, the pointer + recording whose state the browser holds, and the UI theme. + + Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a + non-null `localStorage` read and falls back to the in-memory value, and the memory value is + nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge + too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- + sign-in path that never reloads the page. + + Existing browsers are not migrated. A value sitting under the retired bare key is + unattributable — nothing recorded whose org id it is — so migrating it is precisely the + defect it would be migrating away from, and it is deleted instead. A signed-in user loses + nothing durable: the active organization is a server-owned fact that + `AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no + active org is held, including the ADR-0081 single-membership repair. One boot re-supplies + it; users with no organization land on the guided first-run flow, which is the outcome this + card is about. + + `apps/console`'s pre-render auth preflight purges every spelling of the active-org key — + the retired bare one and each `:u:` scope — when it finds a dead bearer token, and + deliberately leaves the session-user pointer in place so the next sign-in can still tell + that the browser changed hands. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- e067173: Refreshes the lockfile so every `@objectstack/*` package resolves at `17.2.0` — `spec`, `client`, `core`, `formula`, `lint` and `sdui-parser` move in lockstep (a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history), and no `17.1.0` resolution remains. + + **The docs-site and console builds stop pulling a Postgres connection-string parser toward the browser bundle.** `@objectstack/spec@17.1.0` imported `pg-connection-string` at the top level of `dist/index.mjs` with no `browser` export condition, so `apps/site`'s production build failed with `Module not found: Can't resolve 'fs'` on every route that reaches `@object-ui/components` from a client component — red on `main` since 2026-08-22 (objectui#5668). `17.2.0` ships the objectstack#11072 fix: `.`, `./data`, `./system`, `./kernel` and `./cloud` now carry `browser` conditions pointing at schema-free `dist/browser/**` bundles, and the site build is back to `Tasks: 29 successful, 29 total`. + + The refresh is lockfile-only — every manifest already declared `^17.0.0`, which admits `17.2.0`, so no dependency range changed. No shipped source moves: the two in-repo adaptations are a drift-guard test and a CI gate, both forced by `17.2.0` retiring the spec's theme module (objectstack#10485) exactly as the objectui#5716 localization predicted — its `Theme`/`ThemeMode`/`ColorPalette` ALLOW entries in `check:spec-symbols` went stale and were deleted, and the parity test now pins the vacancy (the spec re-publishing a theme name is a loud collision) instead of a spec leg that no longer exists. +- 7d0143c: The Console now gates the `/studio/*` routes on the `studio.access` ENTRY + capability, not just on the backend's refusal of the writes behind them + (objectui#5519). + + `/_console/studio/` rendered the full Studio pillar builder — Data / + Automations / Interfaces / Access, with Publish and Save draft — to any + authenticated principal who typed the URL, on deployments where the Studio nav + tile is deliberately absent and every metadata write is refused. A plain tenant + user was walked through the entire "new package" form and only refused at + submit (403). The lockdown criterion for that deployment shape is two-part — UI + entry hidden AND API refused — and only the API half was met; what stood on + this side was a write-level gate where an entry-level one belongs. + + The whole `/studio` subtree now hangs off one route element that reads + `systemPermissions[]` from `GET /api/v1/auth/me/permissions` (the endpoint this + app already consumes) and admits only a principal whose LOADED set carries + `studio.access` — the capability declared as "Enter the Studio metadata-design + surfaces", which a tenant org owner does not hold by design. Everyone else is + sent to `/home` without the builder ever mounting. + + The fail direction is deliberately inverted from this app's other capability + gates: those fail OPEN on an unknown answer because their bad outcome is a + holder losing a button, whereas a route gate's bad outcome is a non-holder + seeing the builder. So the loading window renders the console splash (never the + builder), an outright fetch failure renders the retryable error splash, and a + `200` that carries no `systemPermissions` at all is refused rather than waved + through. The server-side refusals are untouched. +- Updated dependencies [8d58f46] +- Updated dependencies [0b1326d] +- Updated dependencies [305205a] + - @object-ui/sdui-parser@17.7.0 + - @object-ui/react-runtime@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..25e2bc13d 100644 --- a/packages/app-shell/CHANGELOG.md +++ b/packages/app-shell/CHANGELOG.md @@ -1,5 +1,1426 @@ # @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. +- 6bd343d: The built-moment transition (#5799): a build conversation that has produced a WHOLE-APP build auto-routes to `/studio/<pkg>/interfaces` — on live completion and on reopening the conversation — after idempotently re-keying the thread under the `app:<pkg>:build` cache key the Studio dock resolves, so the workbench's right rail continues the SAME conversation. Cold start keeps the full-page surface; the dock's 以完整页面打开 door carries a sticky per-conversation opt-out so the one sanctioned way back is never bounced straight to Studio. +- 1939c96: 设计⇄运行 on the Interfaces canvas (#5800): a two-state switch in the canvas header flips the SAME renderer between design (selection + inspector + design overlays) and an interactive runtime (click 新建, enter records) — ADR-0080's design=run pivot made visible; selection context survives the round trip. The topbar's 打开应用 teleport is retired (run mode is the in-workbench way to try the app), and the topbar's app detection now matches the pillar's (draft-app fallback, re-resolved on draft saves and the metadata-refresh pulse) so a deep-link to /access can no longer claim the package has no app while /data shows one. +- c40f3b8: A screen flow's resume result reaches the user — on both outcomes (objectui#5417). + + A dogfood walkthrough reported that a refused `resume` and a successful one + "render identically: the dialog closes and the page is unchanged", leaving no + gesture that distinguishes "created" from "rejected". Re-measured against `main` + before any change, one half of that was already fixed — `interpretFlowResponse` + reads the ADR-0112 envelope, and `FlowRunner`'s `toast.error` has carried its + prose since the `400 FLOW_FAILED` classification landed in `17.6.0`, five minors + after the version the report was measured on. There was no interpreter bug and + no un-consolidated fourth call site. Three gaps in the RUNNER's disposition were + real, and they are what changed: + + - **A terminal failure no longer closes the dialog.** The reason it closed is + unchanged and is not reversed: on a `FLOW_FAILED` the engine has already + consumed the suspension, so a resubmit can only reach "No suspended run" and + must not be offered. Closing was one way to withhold that dead retry and the + expensive one — the user had just typed a form they could no longer see, and + the engine's sentence names a value that left the screen with it. The dialog + now stays open with the submit affordance withdrawn: the flat footer swaps + Submit for Close, and an `object-form` step drops its Save (which also stops a + second click from duplicating the record it had already persisted). + - **The refusal has a second, non-expiring carrier.** The toast stays — it is + viewport-fixed, so it still reaches a user scrolled past a tall step's header + — and an inline destructive `Alert` (`role="alert"`) now holds the same + sentence inside the dialog, beside the values that produced it. A retryable + refusal (`INVALID_SCREEN_INPUT`, transport, 5xx) keeps Submit live as before, + and its banner clears as soon as the user starts editing. + - **A successful run invalidates what the flow WROTE, not just what the user is + looking at.** Both hosts answered `onComplete` with + `notifyDataChanged({ objectName: <this page's object> })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- f4becce: Console Home stops offering the metadata-authoring front door on a deployment + whose own runtime config says authoring is not offered there (objectui#5521). + + The "Build an app" cover card is now withheld when + `GET /api/v1/runtime/config` reports `features.aiStudio: false`. On the composed + hosted-SaaS shape that card led a plain tenant into the full authoring flow + behind a runtime whose `/api/v1/meta/*` answers `403` and whose ToolRegistry + holds zero authoring handlers — the entry was offered and the refusal arrived at + submit. The lockdown criterion for that shape is two-part, UI entry hidden AND + API refused; only the backend half was green. + + - The card is **hidden, not dimmed**, because that is the flag's own declared + meaning on both sides of the wire: `RuntimeFeatures.aiStudio` documents "when + false, the SPA hides the AI authoring affordances", and the serving plugin + documents "set false to force-hide the authoring UI". + - `features.marketplace` keeps the different presentation objectui#5504 gave it + — a dimmed card plus a visible localized reason. That flag means a route is + reachable; this one means force-hide. "Start with a template" is untouched: + installing a marketplace package is not AI metadata authoring and answers to + its own flags. + - No reason line is rendered in its place. `home.build.noCapability` says the + *account* lacks "Manage Metadata"; on a runtime with no authoring at all the + surface is absent for everyone, and pointing a viewer at a permission that + would not help them is the misdirection objectui#5557 is about. + - Unknown fails **OPEN** (`!== false`), the doctrine `isMarketplaceEnabled()` + already encodes: a runtime predating the flag, or one whose config fetch + failed, keeps the card exactly as visible as before. + + No new authorable config key, no new server surface, and no new copy — the flag + was already being served and already reaches the browser. +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. +- 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. +- 8b1a8f3: **The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.** + + The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495. + + **The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered". + + One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one. +- 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. +- 19479a3: Studio simplification, first cut (#5813): the Data pillar's object sub-tabs collapse to 记录/表单 with the five power panels (验证/钩子/操作/API/设置) behind one 「高级」 menu (the open panel's name wears the active pill, so the collapsed default never hides where you are); drafts now AUTO-save (debounced 1.5s, CEL-blocking gates the timer per the objectui#4306 rule, a failed save waits for the next edit) and the four 保存草稿 buttons are retired in favor of a quiet saving/last-saved hint; the 权限 pillar moves from the top-level row into a 「更多」 overflow whose items carry the same dirty-guard as the primary pillars — the /studio/:pkg/access route and every page behind it are untouched. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- 4697234: One pending-drafts source of truth (#5801): `usePendingDrafts` / `fetchPendingDrafts` replace the five hand-rolled `GET /api/v1/meta/_drafts` copies behind the home banner, Studio topbar 变更/待发布 buttons, chat pending-drafts bar, chat draft-card resolver, and the preview watermark bar. Every successful publish path (`usePublishAllDrafts`, Studio `doPublish`, the chat bar) now emits the assistant bus's metadata-refresh pulse, and a chat turn that staged or replayed drafts emits it on completion — so a publish or an agent-staged draft anywhere converges every surface at once, and the home banner no longer needs its post-publish `window.location.reload()`. An errored drafts read now uniformly reports UNKNOWN (null), never a fake zero. +- 0e00da3: The metadata designer's View column inspector now reads a column's identity — + field key and label — in the ObjectStack canonical spelling only: `field` and + `label`. The legacy TanStack aliases `accessorKey` / `header` are no longer + consulted (objectui#5344). + + Why this is a behaviour change and not a tidy-up: `ListColumn` refuses both + legacy keys by name (`unrecognized_keys`), so a column carrying them has no + field key the spec recognises. The inspector nevertheless read them and + displayed `accessorKey`'s value in the field-key box, presenting a spec-refused + key as though it were a valid column identity — the same consumer-side + tolerance alias `ObjectGrid` retired in objectui#5068, surviving one layer up in + the authoring tool, which is the surface that is supposed to teach the correct + shape. + + **What an author sees:** a stored column shaped `{ accessorKey, header }` now + shows an EMPTY field key and an empty header, and is re-authored rather than + silently carried. Canonical `{ field, label }` columns and bare-string columns + are untouched. + + **What is deliberately NOT changed:** the writeback. `patchIdentity` still + re-emits whichever spelling it was handed, so no stored document is rewritten + by the act of editing it. Normalising on write was ruled out once the + maintainer confirmed there is no population of legacy stored documents to + migrate; such a column stays unsaveable against the spec both before and after + an edit, exactly as it did before this change. What this removes is the + invisibility of that state, not the state itself. + +### Patch Changes + +- 1edbc2f: **Behaviour change:** a `master_detail` action param with no reference target now + receives the same "paste a record id" placeholder and help text a targetless + `lookup` param has had since objectui#3405. Previously it degraded to a plain + text input exactly like `lookup` did, but silently — the user was shown an + unexplained empty box that wanted a bare record id. + + The cause was two hand-maintained answers to one question. `paramToField()` + performs the degradation over RESOLVED widget keys (`lookup`, `master_detail`), + while `ActionParamDialog` decided who gets the hints with its own literal over + RAW param spellings (`'lookup' || 'reference'`). Neither set contained the + other: `master_detail` degraded with no hints, and `reference` was a copy of an + alias-table row the adapter folds to `lookup` before it tests membership. + + `paramToField` now exports `paramDegradesWithoutTarget(param)` and both the + adapter's own fallback branch and the dialog's two hint readers ask it, so "who + degrades" and "who gets hints after degrading" are one member set answered once + (objectui#5654; same convergence shape as objectui#4770 / #4790 / #4815). + + Params that already had the hints keep them, including the `reference` spelling; + a param that renders as text without having degraded (an unknown type) still + gets no picker hints. +- 649f4a5: **Behaviour change:** the console action runtime (`useConsoleActionRuntime`) now + builds its authenticated fetch with `sameOriginOnly: true`, matching the + `provider: 'api'` data-source lane (`ConsoleShell`). A metadata `type: 'api'` + action whose resolved target is a different origin than the page is fetched + through the bare global fetch: the platform Bearer token, `X-Tenant-ID`, and + `Accept-Language` are no longer attached (objectui#5702, maintainer ruling + 2026-08-22). Previously the Bearer rode to any off-origin target whose URL + contained `/api/`, and `X-Tenant-ID` rode to every off-origin target + unconditionally. + + Same-origin actions — including every relative target in a same-origin + deployment — are unchanged, and off-origin requests still execute (pass-through, + not a refusal). An off-origin integration that legitimately needs the platform + bearer declares itself explicitly or proxies same-origin. + + Note for split-host setups: `apiHandler` prefixes relative action targets with + `VITE_SERVER_URL`. When that is set to an origin different from the page's, + those action requests are off-origin and no longer carry credentials. The + committed dev and starter configurations are unaffected: since objectui#5745 + they ship `VITE_SERVER_URL` empty and reach a split-host backend through the + Vite `/api` dev proxy, so their action requests are same-origin. +- 169f6d2: `AiChatPage`'s public share-link base now resolves through the one console-mount + resolver instead of a private copy of it (objectui#4482). + + The page built `publicShareBase` itself — read the injected `<base href>`, take its + pathname, trim trailing slashes, concatenate `${origin}${base}/s` — which was the third + independent implementation of the mount resolution `resolveConsoleUrl` centralizes. + objectui#4472 had just deleted the other two on that rule; this was the surviving + sibling. Its output was correct, so nothing a user hits was broken and nothing a user + hits changes: measured over the base-href matrix, the deleted builder and + `resolveConsoleUrl('s')` return identical URLs for every shape the console is served in + — `/_console/` (the only href the framework CLI injects), `/` root mounts, `./` portable + builds, nested mounts, and no `<base>` at all. + + The `/s` resolution now lives beside its three siblings as `resolvePublicShareBase()`, + which keeps the one thing a bare `resolveConsoleUrl('s')` call would drop: with no DOM + it returns `undefined` rather than a URL built from an origin that does not exist, so + `ShareDialog` applies its own fallback. It deliberately takes no `baseURI` argument — + the mount is only ever carried by the injected `<base href>`, and a resolver with no + other input cannot be pinned by a test that steers something production never reads. + + `resolvePublicShareBase.browser.test.tsx` pins the resolved base against a real injected + `<base>` element for each deployment shape, plus a structural case asserting no other + app-shell file reads the `<base>` tag — so a fourth copy fails a test rather than + waiting for mount semantics to change under it. +- aa3b810: `features.aiStudio` is now read through one `isAiStudioEnabled()` accessor instead + of being spelled inline at two call sites (objectui#5577). + + `features.marketplace` already had a documented accessor whose docblock is where the + fail-open doctrine is written down — *"Fails OPEN (`!== false`): a runtime predating + `/api/v1/runtime/config`, or one whose config fetch failed, keeps the default `true`"*, + plus the "never infer this from the shape of a failure" warning. `features.aiStudio` + had no such sibling: `ChatDock` read `getRuntimeConfig().features.aiStudio !== false` + and `HomePage` read `getRuntimeConfig().features?.aiStudio !== false`, so one doctrine + had two spellings and neither reader could cite it. + + The two spellings were not equivalent. `ChatDock`'s omitted the optional chain, and + against a runtime-config snapshot whose `features` is absent that read is a TypeError + rather than a fail-open — the exact shape that crashed 29 tests across four suites in + PR #5575 before it was corrected. Measured here: no live path can currently deliver + such a snapshot to `ChatDock` (the module's singleton constructs `features` on every + write and exports no setter, and no suite mounts the dock's default body under a + partial stand-in), so this closes a reachable-by-construction crash rather than a live + one — and it closes it at the source by leaving no inline read to get wrong. + + `isAiStudioEnabled()` is an internal module export, matching `isMarketplaceEnabled()`: + neither is re-exported from `src/index.ts`, so the package's published `exports` surface + is unchanged. +- 029e2fb: The console's `<PageHeader>` spells its secondary line `subtitle`, the same key the other `PageHeader` in this repo uses (objectui#4761). + + This repository has two components named `PageHeader`. `@object-ui/layout`'s is + the renderer for the authored `page:header` / `page-header` node and converged + on `subtitle` in objectui#3789, because `subtitle` is the key + `@objectstack/spec/ui`'s `PageHeaderProps` declares. `@object-ui/app-shell`'s — + the console's own title row, drawn by `ObjectView` and `ObjectDataPage` — + spelled the very same concept `description` and had no `subtitle` at all. Both + rendered correctly; the defect was one concept carrying two key names one + package apart, the objectstack#4115 shape moved up a layer. An author reading + one component to learn the other was being taught a key the contract does not + have. + + **Not a breaking change, measured rather than assumed.** The convergence is a + plain rename with no alias, because this component is not on the published + surface: + + | gauge | result | + |---|---| + | exports of `dist/index.d.ts`, through the TypeScript checker | 226 symbols; `PageHeader` and `PageHeaderComponentProps` are not among them (controls: `AppShell` reachable, a nonsense name not) | + | `exports` map | declares exactly `.` and `./styles.css` | + | Node resolving `@object-ui/app-shell/layout`, `…/dist/layout/PageHeader.js`, `…/src/layout/PageHeader.js` | `ERR_PACKAGE_PATH_NOT_EXPORTED` for all three, while the declared entry resolves | + | in-repo call sites | 2, both inside this package (`ObjectView.tsx`, `ObjectDataPage.tsx`) | + | emitted declarations that change | `dist/layout/PageHeader.d.ts` only — `dist/index.d.ts` and `dist/layout/index.d.ts` are byte-identical across the change (`8c886251…`, `f9f4862b…`, both legs) | + + No supported specifier reaches the prop, so there was nothing to keep + compatible, and a renderer-side `description` alias would have been exactly the + second dialect AGENTS.md #0.1 forbids — the layout side had just finished + retiring one. Out-of-repo consumers cannot be enumerated from this repository; + what can be, and is, is the set of import paths through which one could have + reached this component, which is empty. + + Rendered output is unchanged: same element, same classes, same position. The + patch tier is a declaration that the tarball moved, not a claim that a consumer + must act. + + `packages/app-shell/src/layout/__tests__/PageHeader.subtitle.test.tsx` is the + pin the card asked for. It asserts the subtitle on the DOM a reader gets (a + `<p>`, in the title block, after the `<h1>`), that `description` now draws + nothing and is rejected by the compiler, and — the assertion that actually goes + red if either side drifts again — that both packages' `PageHeaderComponentProps` + declare `subtitle`. +- ac73c24: The approval step progress bar is a vertical stepper, so long flows stop + clipping their tail steps. + + Both occurrences were a single non-wrapping flex row whose steps were each + `shrink-0`. A flex row's min-content width is the sum of its non-shrinkable + items, so the bar's intrinsic width grew without bound with step count and + label length. On a live 17.1.0 project a real 6-step flow with ordinary CJK + step names measured **1070px inside a 527px container** (objectui#5554). + + The two hosts failed differently, and neither failure was recoverable by the + reader: + + - **`ApprovalsInboxPage`** (the inbox detail drawer) — the bar itself was not + scrollable, so the nearest scroller was the drawer *panel*. Reaching steps + 4-6 meant dragging the drawer's own horizontal scrollbar, which pushed the + record card, the activity timeline and the action buttons off-screen and left + a near-blank panel. + - **`RecordApprovalsPanel`** (the record page's approvals panel) — this one + carried `overflow-x-auto`, so it scrolled itself rather than its container. + Better, but the tail steps still sat behind a scroll gesture with no visible + affordance. + + In both, readers took the clipped bar for the end of the data; the reporting + customer acceptance tester said so verbatim. Widening the window does not help: + the drawer is fixed-width, and clipping was identical at 1440x900 and 1920x1000. + + Both now render as a column: one row per step, a badge-and-rail gutter, and a + label that may wrap. Width is capped by the container at every step count and + every label length, which also suits both hosts' tall-and-narrow aspect. The + rail segment below each step keeps the tint rule the horizontal connector used + — it is coloured by the step it leads *into*. + + **Always vertical, with no step-count or measured-width threshold**, because + the overflow is driven by intrinsic content width (labels x count), not by + count alone: three 16-character CJK labels already crowd a 527px drawer, so any + count threshold picks a cutoff that is wrong for some real flow, and a measured + one reintroduces a viewport-dependent branch. The card's requirement is a fix + that cannot break at an untested viewport or flow length, and a layout with no + breakpoint and no measurement is the form that satisfies it. Horizontal-with- + scroll was ruled out for both occurrences: it leaves steps behind a gesture. + + Pinned in `ApprovalsInboxPage.stepProgressVertical.test.tsx` and + `RecordApprovalsPanel.stepProgressVertical.test.tsx`. "The stepper renders" is + green against the broken code too — every step was always in the DOM, and the + clipping was layout — so the suites assert the property the defect names + instead: no row is `shrink-0`, every label is `min-w-0` and none is + `whitespace-nowrap`, nothing in the subtree is an `overflow-x` scroller, and no + axis, overflow or width-pinning class carries a breakpoint prefix (so there is + no viewport with untested behaviour). The reported failing regime is exercised + directly with the reporter's own six CJK labels, and a 2/5/6/12-step sweep pins + that the layout classes are byte-identical across all four, so no count + threshold can put some other flow length back on the old path. + + The two steppers are kept identical by hand rather than extracted to a shared + component: they live in different packages, and deduplicating them is a + refactor with its own surface. Filed separately. +- 7e89836: fix(approvals): derive approver identities from `positions`, not the retired `user.roles` (objectui#5424) + + Framework ADR-0090 D3 renamed the session's `roles` key to `positions` with no + deprecation window, and the protocol-17 session face emits no `roles` key at + all. Three client sites still read it: + + - **`sharedUserFeeds.approverIdentities`** — the bell badge, the bell's + Approvals tab and Home's To-do card. It read nothing else, so it sent **no + `role:` identity at all**: an approval addressed to a position rather than to + a person matched nothing and vanished from all three surfaces, silently. + - **`approvalsApi.buildApproverIdentities`** — "My Pending" and the + Approve/Reject enablement. It also splits the scalar `user.role`, so it + degraded rather than dying: it still yielded `role:user` while dropping every + business position name (`manager`, `finance_approver`, …). + - **`AppContent`'s expression user** — forwarded a `roles` key that was always + `undefined` into every CEL predicate context. Removed; `positions` and + `isPlatformAdmin` were already forwarded correctly beside it. + + The retired spelling is **not** kept as a fallback — pairing the two is what + ADR-0090 D3 forbids, and `packages/auth/src/types.ts` says so on the + declaration. + + `AuthGuard`'s `requiredRoles` gate (the fourth surviving reader) is deliberately + untouched: it is a semantics decision, not a rename, and is deferred to a + maintainer ruling. +- 77c4ed3: `AppSidebar` is now `@deprecated` — use `UnifiedSidebar` instead. + + A census (objectui#5720) found `AppSidebar` has no in-repo mount point + (`ConsoleLayout` renders `UnifiedSidebar`, not this component) and no + downstream consumer visible anywhere across this org's GitHub-visible + repositories. It stays exported — from the package barrel and the published + `dist/index.d.ts` — because `@object-ui/app-shell` is a public npm package + (`publishConfig.access: "public"`) and an external consumer outside this org + is structurally invisible to that census; that is why it is deprecated + rather than deleted outright. No behavior change in this release — the + component still renders exactly as before. Its admin nav cluster is a + near-duplicate of `UnifiedSidebar`'s and has already drifted from it (it + gates only `sys-marketplace` on the workspace-admin flag, where + `UnifiedSidebar` gates the whole cluster); that divergence is not being + reconciled, since the component is scheduled for removal rather than kept + in parity — see objectui#5817 for the removal plan. + + Migration: replace any `AppSidebar` usage with `UnifiedSidebar` from the same + package. +- 3a58149: A cloud-connection bind failure now reads in the user's language whichever clock + noticed it (objectui#5054). + + One abandoned device approval could be noticed by either of two clocks, and the + Cloud Connection panel had a different answer for each. When the panel's own + `expires_in` deadline fired first it rendered `cloudConnection.errors.expired` — + translated in all ten packs. When the SERVER noticed first, `/bind/poll` answered + HTTP 400 with `message: 'Device authorization failed: expired_token'`; `getJson` + threw a bare `Error` carrying only that sentence, and the catch rendered it + verbatim. Same user, same failure, two languages, decided by which clock got + there first — visible on a zh console as the same abandoned approval reading + Chinese or English depending on whether the tab sat open past `expires_in`. + + `getJson` now carries the envelope's `declaredCode` and `code` across its throw, + and a single closed map turns the two RFC 8628 outcomes a user can actually cause + into console copy: `expired_token` → the existing `cloudConnection.errors.expired`, + `access_denied` → a new `cloudConnection.errors.accessDenied` added to all ten + locale packs. `declaredCode` is read first, because ADR-0112 keeps the upstream + spelling there — `code` is `DEVICE_CODE_FAILED` for both. + + Every other code is unchanged: `invalid_grant`, and anything upstream invents + next, still render the wire `message`, which stays the single source of truth for + failures this console has no copy for. No API, export or resolver was widened. +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. +- 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. +- 4334341: `MarketplacePackagePage`'s third fetch effect — `listLocalInstalls` — is now gated on + `marketplaceEnabled` and `isAdmin`, the same two predicates the page's other two fetch + effects already check, in addition to (not instead of) its existing + `features.installLocal` check (objectui#5620). + + `listLocalInstalls`'s only consumer is `localInstalls.find(...)` in the content branch, + which is unreachable whenever the page has already returned `MarketplaceDisabled` (no + marketplace on this runtime) or `MarketplaceAccessDenied` (a refused viewer) — both + decided ahead of the content branch since objectui#5533 and objectui#5583. Before this + fix, a runtime with `features.installLocal: true` still fired the request — and + discarded its answer — on a marketplace-off runtime and for a non-admin, the same + wasted-round-trip class objectui#5533 established the fix for on this page, on the flag + that card was not about. + + `features.installLocal` remains its own axis: a runtime can mount a local kernel install + path with no marketplace proxy at all, so the fix adds the two predicates as a + conjunction rather than replacing the existing check. The request still fires exactly + when it did before AND the viewer would actually see its answer. +- 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. +- 4ef29f0: Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). + + The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. + + Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. + + `@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. +- ad404e0: Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. +- 0935a43: Console builds no longer carry a live Sentry DSN, and `sendDefaultPii` is now opt-in + (objectui#5522). + + `@object-ui/console` publishes a pre-built SPA, so ONE artifact — built once from + `apps/console/.env.production` — is what the hosted SaaS console and the on-premises / + air-gapped EE images all embed. Vite inlines every `VITE_*` from that file into the + bundle as a frozen object literal, so the DSN committed there was a live third-party + telemetry endpoint compiled into artifacts that land inside customer networks. It could + not be switched off afterwards either: the `VITE_SENTRY_ENABLED` kill switch is read off + that same frozen literal, so on a shipped bundle it is `undefined` forever and editing + env vars on the deployed host does nothing. An air-gapped deployment was measured + sending 14 envelopes per session to sentry.io with IP + User-Agent PII, unstoppable by + the customer. + + - `apps/console/.env.production` no longer defines `VITE_SENTRY_DSN`, + `VITE_SENTRY_ENVIRONMENT` or `VITE_SENTRY_SEND_DEFAULT_PII`. A build with no DSN never + imports `@sentry/react`, so the `vendor-sentry` chunk is not even fetched. + - `sendDefaultPii` changed from opt-out (`!== 'false'`) to **opt-in** (`=== 'true'`), so + IP address and User-Agent are never the inherited default of a build that did not ask + for them. + - The gate now fails **closed**: an absent, empty or whitespace-only DSN means do not + send. The direction is deliberately inverted from the usual — an unreported error is + recoverable, PII leaving an air-gapped deployment is not. + + **Action required for deployments that want error reporting** (the hosted SaaS/demo + console): inject `VITE_SENTRY_DSN` from your build environment, the same way + `VITE_SERVER_URL` is already injected, plus `VITE_SENTRY_SEND_DEFAULT_PII=true` if you + still want IP/User-Agent on events. Nothing else changes for builds that opt in. +- add10d8: Studio's metadata authoring path stops greeting the author with errors they did not cause, English help text in a Chinese console, and a toast on top of the publish button. + + Three defects measured on a 17.1.0 dogfood walkthrough (objectui#5416), all on + the first surfaces a new author sees. + + **Validation no longer runs on mount.** `新建软件包` opened with both required + fields already red: the create draft is `{ version, type }`, so + `ManifestSchema.safeParse` reported `name` and `id` missing on the very first + render, and the dialog then jumped a line height per field as each error + cleared. `SchemaForm` now defers the error *line* until the row has been + touched — first focusout anywhere in it, or the field's own first edit — for + create forms only. The rule itself did not move: `issues` still reaches the host + unchanged, so the submit button is gated on exactly the same validation, and + edit/view forms still report from mount, where the issues describe stored values + rather than something half-typed. + + **The package form's help text is translated.** The labels came from the + metadata-admin i18n bundle and the help line under each one came straight from + `ManifestSchema`'s English `.describe()`, so a zh console rendered 显示名称 over + "Human-readable package name". `getPackageForm` now reads each field's help + through the same bundle as its label, via a new `tOptional` that returns + `undefined` rather than echoing the key back. Only zh entries exist: an en-US + console finds nothing and keeps falling through to the spec's own sentence, so + the English keeps exactly one producer — `@objectstack/spec` in the framework + repo — and this repo never holds a copy of it to drift. + + **The publish panel's primary button opens clear of the toast stack.** The + console mounts its toaster bottom-right and `DraftChangesPanel` is a + `side="right"` sheet with an `mt-auto` footer, so a save toast raised on the way + there (`对象「…」已存为草稿`, 4s default) sat directly on 全部发布 until it timed + out. Opening the panel now clears the stack the surface the author just left had + raised. Repositioning the toaster was measured and rejected: the draft preview + bar is `sticky top-0` and carries its own publish actions, `NotificationSnackbar` + anchors bottom-centre and the nav rail owns the left edge, so a move only + relocates the same collision onto a different primary control. + + Not fixed here, and not fixable here: the other strings the card names are + produced outside this repo. `Owning Business Unit` (`packages/spec`), + `Search Index` (`packages/objectql`) and the `Revise Window` flow node's name + and description (`packages/plugins/plugin-approvals`) all come from the + framework, which owns their translation catalogue; patching them in the console + would create a second source of truth that diverges at the next framework + release. +- 6cc62e2: Studio Interfaces: the nav-leaf binding reads the canonical target key only + + `resolveSurface` fell back to the bare spellings `page` / `object` / + `dashboard` / `report`, and carried a `case 'view'`. Every + `NavigationItemSchema` member is a `strictObject`, none of those bare + spellings is in any variant's shape or in `NAV_ITEM_ALIASES`, and `view` is + not one of the union's nine members — so all of them are keys `AppSchema` + answers with `unrecognized_keys`, and every one of those branches could only + fire on an app that cannot be saved. The Studio nav item inspector's object + picker likewise read `node.object ?? node.objectName`, preferring the rejected + spelling over the canonical one; it now reads the canonical key first. + + No shape that parses today stops parsing: this narrows the designer back to + what the contract already declares. +- d15a92d: Studio's pre-publish security block can now take you to the object it names. + + The pending-changes sheet reports what the publish door would refuse — as + `object/crmext_visit`, with the rule's fix-it hint and "Fix it on the object under + Settings → Record sharing". Naming it was the half that shipped; reaching it was not. + The `?surface=<type>:<name>` deep-link that would have carried the author there + captures the URL exactly ONCE, at mount, and the sheet is opened over an + already-mounted pillar — so writing the param changed the URL and moved nothing. + + That mount-time capture is deliberate and stays exactly as it was: the mirror half + rewrites the param on every in-pillar selection, so a capture that followed the URL + would re-trigger its restore on each one. What was missing is a third half — a live + target delivered BESIDE the URL, which is what a producer already inside the pillar + needs. `surfaceDeepLinkChannel` adds it: producers ask for a surface by identity + (`{type, name}`), the host routes cross-pillar requests back through the URL (that + pillar is unmounted, so its capture is the right mechanism) and vetoes the ones the + author declines over unsaved edits, and the mounted pillar applies the rest. + + Applied AT MOST ONCE, by a monotonic id. A standing request re-resolved on the next + rail reload would drag the author back off whatever they had since selected — the + regression the mount-time ref exists to prevent — so `DataPillar.surfaceRequest.test` + pins a hand-picked object surviving a package switch, and + `surfaceDeepLinkChannel.test` pins the capture itself as an unchanged control: it + still ignores every URL change after mount, and a live request never moves it. + + The sheet's other home is the Home / draft-preview bar, where the Studio object editor + is not a reachable destination at all. Reachability is answered structurally — the + producer hook returns `null` when no host published the channel — so off-Studio the + item name stays the prose #5418 shipped rather than becoming a link to nowhere. Both + directions are assertions in `DraftChangesPanel.securityLink.test`, not a comment. + + Nothing the other three pillars observe changed: `useSurfaceDeepLink` keeps its + signature, its return and its behaviour, and only the Data pillar subscribes to the + new channel. The channel is its own React-only module on purpose — importing the hook + into the sheet would have pulled `nav-selection` and the App-nav inspector into the + console's eager graph. +- 3ece13e: Removed the dead `theme:` entry from `clientValidation.ts`'s `LOADERS` table, which + read `ThemeSchema` off `@objectstack/spec/ui` — a symbol the spec retired upstream + (objectstack#10485 / PR objectstack#10695, which deleted the whole `ui/theme.zod.ts` + module). `theme` was never a registered metadata type, so metadata-admin never asked + for it (objectui#5715). + + This is dead-code hygiene, not a bug fix, and it changes nothing an author can + observe. Two measurements say why, and both cut against the more dramatic reading: + + - The entry is not broken *today*. objectui's own `@objectstack/spec` pin (17.1.0) + still publishes `ThemeSchema` as a working strict Zod schema — measured by + importing the `ui` subpath and by ablation, where restoring the entry made + `validateMetadataDraft('theme', ...)` actively reject a draft. The skew is with + objectstack's `main`, not with what objectui installs, which is exactly why no + gate here ever went red. + - It could never have crashed. `getSchemaForType` duck-checks the loader's result + (`typeof schema.safeParse === 'function'`) and wraps the call in `try/catch`, so an + `undefined` export degrades to a silent no-op validator, never a throw. After a + future spec bump past the retirement that is the branch it would have taken. + + The one thing that was genuinely off: `hasClientValidator` answers from key presence + alone, so it returned `true` for `theme` while the validator behind it would have + judged nothing. Per its own docblock that combination makes `ResourceEditPage` treat + live client issues as the error source and suppress the server's `_diagnostics` — a + stored item rendering as clean. Unreachable in practice, but it is the reason the + entry is worth removing rather than leaving inert. + + The line is replaced by an absence note in the shape the file already uses for + `workflow` and `approval`, because re-adding this entry from the spec would + type-check and go green against the current pin. A metadata-admin theme editing + surface remains a separate capability decision. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- 286dd8d: A view personalization overlay now stores **only the patch** — a column drag no + longer freezes the source view's filter into it (objectui#5233). + + `ObjectView`'s `persistViewPatch` wrote `{ ...baseViewDef, ...patch }`, so an + overlay written by a mere sort/density/column change copied the view's *current + effective* `filter` — along with its `columns`, `label`, `type`, `isDefault` — + into the stored row. The display merge is `{ ...source, ...override }`, so that + copy then outranked the source view forever: an admin edited the view's filter + and every user who had once resized a column silently kept the old filter, with + nothing anywhere reporting it. + + The write now goes through the exported seam `buildPersistedViewBody`. For a + **system view's overlay** the body is the patch plus `viewKind` when the active + tab carries one — identity, not content, and the same line the read side already + draws (`VIEW_OVERLAY_IDENTITY_KEYS`); `object`, `name` and the overlay marker are + stamped by the adapter. A **saved view's own row** is deliberately still written + whole: for it the body *is* the view, there is no source underneath it for a + copied key to shadow, and the write is a whole-document PUT — narrowing it would + delete the user's view rather than narrow it. + + This is the write half of the maintainer's 2026-08-12 ruling (objectstack#7494). + It was blocked until `columnState` was admitted to the view-metadata surface as + an explicitly runtime-only overlay key (objectstack#9933, released in + `@objectstack/spec` 17.1.0) — before that a `columnState`-only patch was refused + `422 INVALID_METADATA`, and the fat copy was the only thing supplying a + recognized key. The read half (`narrowPersonalizationOverlay`) shipped earlier + and stays: rows written before this land are still tolerated on read, and because + the write replaces the whole document, the next toolbar toggle also strips such a + row at rest. No migration. +- Updated dependencies [ebfc3c2] +- Updated dependencies [9a5d669] +- Updated dependencies [b2e85a9] +- Updated dependencies [c7cd2b6] +- Updated dependencies [77f846a] +- Updated dependencies [686d5d9] +- Updated dependencies [76ae729] +- Updated dependencies [f47d94c] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [894d103] +- Updated dependencies [a31adc6] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [2e471a6] +- Updated dependencies [3d79455] +- Updated dependencies [cc5de5c] +- Updated dependencies [84d2e98] +- Updated dependencies [f24195a] +- Updated dependencies [56f4e34] +- Updated dependencies [1d9100e] +- Updated dependencies [62c0fa1] +- Updated dependencies [4db5989] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- 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 [3ccd9e8] +- Updated dependencies [c8da8b3] +- Updated dependencies [d7bd274] +- 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 [9850c6e] +- Updated dependencies [6c5ee71] +- Updated dependencies [84d70bb] +- Updated dependencies [cfc2c7a] +- Updated dependencies [9e725e0] +- Updated dependencies [6f017e9] +- Updated dependencies [f1c27f0] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [343c598] +- Updated dependencies [af3861f] +- Updated dependencies [0ccbdc1] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [4ef29f0] +- Updated dependencies [ad404e0] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [2dd9443] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [ff2d547] +- Updated dependencies [36918bf] +- Updated dependencies [490f482] +- Updated dependencies [934a532] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- Updated dependencies [cfcff30] +- Updated dependencies [3c73d99] +- Updated dependencies [6b348d2] +- Updated dependencies [c86185e] +- Updated dependencies [fb96ecb] +- Updated dependencies [4d73b07] + - @object-ui/auth@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/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-charts@17.7.0 + - @object-ui/plugin-chatbot@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-list@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/providers@17.7.0 + - @object-ui/plugin-kanban@17.7.0 + - @object-ui/collaboration@17.7.0 + - @object-ui/plugin-report@17.7.0 + - @object-ui/plugin-editor@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..69ee0eb72 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,335 @@ # @object-ui/auth +## 17.7.0 + +### Minor Changes + +- 686d5d9: `AuthGuard`'s `requiredRoles` now matches against `user.positions` — the one published spelling (framework ADR-0090 D3) — keeping the `user.role` scalar fallback only for sessions that carry no `positions` key at all (guest / legacy identities). This restores position-holder admission on protocol-17 deployments (where the retired `roles` key is never emitted) and closes the matching over-admission: a coarse scalar `role` no longer passes a gate whose position the server says the user lacks. Preview mode now emits `positions: [role]` instead of the retired `roles` key — it was the last producer — so preview identities are shaped like protocol-17 sessions for every `positions` consumer. The hand-copied `roles?: string[]` mirror key is removed from the client `AuthUser` type; breaking only for TypeScript consumers that compiled against `AuthUser['roles']` (read `positions` instead — no runtime payload ever carried `roles` at protocol 17). Maintainer ruling 2026-08-22 on objectui#5424. +- c8da8b3: Stop showing an access-denied screen to a real administrator while their + adminship is still resolving. + + `useIsWorkspaceAdmin()` returned a bare `boolean`, so "the inputs have not + arrived yet" and "resolved: not an admin" were the same answer. One of its three + sources — the active organization member row — is fetched some round trips after + the session (`listOrganizations` → `getActiveOrganization` → `getActiveMember`), + so an administrator whose adminship lives only in that row rendered at least + once as a non-admin, and every gate downstream acted on it: the two marketplace + surfaces painted `MarketplaceAccessDenied`, the console chrome dropped and + re-added its admin nav entry, and `AppContent` fired a `<Navigate to="/home" + replace>` that the later flip could not undo. + + **Breaking (published API, hence `minor` per this repo's version policy):** + `useIsWorkspaceAdmin(): boolean` is replaced by + `useWorkspaceAdminStatus(): { isAdmin: boolean; isResolved: boolean }`. The old + name is removed rather than kept alongside, so a call site that ignores the + third state fails to compile instead of silently refusing an administrator. + + -const isAdmin = useIsWorkspaceAdmin(); + +const { isAdmin, isResolved } = useWorkspaceAdminStatus(); + + `AuthProvider` gains `isMembershipResolved` on its context — the organization / + member pipeline has reached a terminal state — because `organizations`, + `activeOrganization` and `activeMember` read `[]` / `null` / `null` both before + the pipeline starts and after it finds nothing. + + No extra wait for administrators: `isResolved` is true the instant `isAdmin` is, + so an admin the session already identifies through `positions[]` never waits on + the member row. + +### Patch Changes + +- ebfc3c2: `ActiveOrganizationStorage.clear()` now verifies that the persisted key is actually + gone and, when it is not, both reports the failure and stops `get()` answering from + the surviving value — instead of swallowing the failed removal (objectui#5731). + + `clear()` nulled `_memoryValue` and then removed the persisted key inside a + `try`/`catch` that discarded any failure. Since objectui#5703 `get()` prefers a + NON-NULL `localStorage` read and only falls through to `_memoryValue`, so the two + halves of `clear()` were not equally strong: nulling memory always sticks, while a + removal that did not stick left the key readable and the read order preferred it. + Sign-out is one of `clear()`'s five callers, so the failure mode was "sign-out does + not stick", and it was silent — the cleared organization went back on the wire as + `X-Tenant-ID` on every subsequent request. + + The removal is now judged by a READ-BACK rather than by catching the throw, which is + both narrower and wider in the right directions. Wider: a wrapped or proxied + `localStorage` whose `removeItem` is a silent no-op never throws and leaves identical + residue, and is now covered. Narrower: SSR and the partitioned-iframe browser where + every operation throws have nothing readable to resurrect, were already safe, and are + not reported as failures. + + A key whose removal could not be verified is quarantined in memory for the rest of the + page-load: `get()` skips the persisted branch for it and answers from `_memoryValue`, + which `clear()` has just nulled and which a later `set()` refills with the value that + write was meant to persist. The quarantine is released as soon as a removal on that key + sticks. An unstamped `X-Tenant-ID` is a documented state of the edge contract + (objectui#5279); a re-stamped signed-out organization is not. + + The failure is not thrown and not returned. All five call sites — sign-out's + `purgeSignedOutClientCaches`, `switchOrganization`, `deleteOrganization`, + `leaveOrganization`, and the session-user purge that runs on the SIGN-IN path — arrive + after the transition they follow up on has already happened, and none can act on a + storage failure; a `boolean` every caller ignores would read as handled when it is not. + So the invariant is restored inside `clear()` and the failure is reported to the + console. + + A working `localStorage` behaves exactly as before: the removal sticks, nothing is + quarantined, nothing is reported, and a non-null persisted read is still authoritative. + `set()`'s swallowed write failure is deliberately untouched — that swallow is + objectui#5703's memory fallback, and it is the correct kind, because the memory copy + upholds `set()`'s postcondition where nulling memory could not uphold `clear()`'s. +- 9a5d669: `ActiveOrganizationStorage.get()` now prefers a non-null `localStorage` read and falls + back to the in-memory value otherwise, instead of returning the `localStorage` read + unconditionally — so the fallback is reachable in the browser state it was written for + (objectui#5703). + + `set()` already swallowed a failed `localStorage.setItem` into `_memoryValue`, but + `get()` only consulted `_memoryValue` when the READ itself threw. There is a real + browser state where the read does not throw and the fallback is nonetheless the only + copy: `localStorage` present and readable but rejecting writes — Safari private + browsing, and any quota-exhausted origin, where `setItem` throws `QuotaExceededError`. + In that state the active org was stored and could not be read back, measured as + `_memoryValue = org-42` alongside `get() = null`. + + The cost was silent and lasted the whole session rather than the documented first-boot + window: `createAuthenticatedFetch` reads `get()`, so `X-Tenant-ID` was never stamped on + any request. Per the edge contract documented on objectui#5279 that header is a routing + hint a reader falls through on — the framework scopes from the session — so no row + visibility rode on this; what was missing is the tenant-routing input, on every request. + `switchOrganization` also appeared to succeed while the client-side stamp never + followed. + + Sign-out is unaffected, and that is the half worth stating: the new fallback fires + exactly when the `localStorage` read is null, which is the state `clear()` leaves + behind. It answers `null` there because `clear()` nulls `_memoryValue` too. That + property is now pinned by test rather than relied upon, so a future `clear()` that only + removed the persisted key fails a test instead of quietly re-stamping a cleared org. + + A non-null persisted read still wins over the memory value, so a working `localStorage` + behaves exactly as before. +- 32ef595: Restore platform-admin detection for permission-set-derived administrators. + + `useIsWorkspaceAdmin` decides Setup app + Studio visibility, App Marketplace + gating and the "Build an app" CTAs. Its third source read `user.roles`, a key + the protocol-17 session face no longer emits (framework ADR-0090 D3 renamed it + to `positions`). An administrator whose adminship comes from the + `admin_full_access` permission set — the single-tenant deployment shape, where + there is no organization member row and the server deliberately no longer + overwrites `user.role` — matched none of the three sources and read as **not an + administrator**: Setup and Studio simply disappeared for them. + + The hook now reads `user.positions[]`, the one spelling the session publishes. + Detection is restored for that path and unchanged everywhere else: an active + member row with an admin role, a stored `user.role` admin scalar, and + preview/no-auth mode all behave exactly as before, and nobody who was not an + administrator becomes one — pinned by four negative cases alongside the + positive one. + + Also corrects the now-stale documentation that described the removed spelling: + the hook's own docblock, the `roles?: string[]` declaration on the client + `AuthUser` (kept for one remaining compile-time reader; see objectui#5424), and + two comments in `@object-ui/app-shell`'s Home page. No behaviour change from the + comment corrections. +- 343c598: The active organization id is now stored per user, and a change of session user drops the + previous user's client state wholesale (objectui#5664). + + `auth-active-organization-id` was a single un-namespaced `localStorage` key while its + siblings were already user-scoped (`objectui-recent-items:u:`, `objectui-favorites:u:`, + `flow-palette-recents:u:`). On a browser handed from one account to another — a shared + machine, a kiosk, a handover, a support session — the arriving user's console read the + PREVIOUS user's organization id. The header workspace chip rendered the previous user's + workspace for a user whose `organization/list` was empty, and the consequence past the + cosmetics is the one worth stating: the polluted org context suppressed + `RequireOrganization`'s routing into the guided "Create your workspace" first-run flow, so + a brand-new user on that browser silently never got the new-user flow at all. + + Nothing about row visibility rode on this. With the stale id the server answers + `403 USER_IS_NOT_A_MEMBER` on `get-full-organization` and `set-active`, and lists zero + environments; the damage was entirely in what the client believed about itself. + + Three changes, and the third is the one that closes the class rather than the instance: + + - The key is per-user (`auth-active-organization-id:u:$userId`), matching the convention + its siblings already use. + - It can no longer be written un-namespaced at all. Where no session user is known yet the + value lives in memory for that page-load only — a namespacing that kept a bare-key + fallback would re-open the defect the first time a write happened before the user id + resolved. + - **A change of session user drops the previous user's client state wholesale.** This is an + allowlist sweep of both `localStorage` and `sessionStorage`, not a list of known keys, so + the NEXT storage key someone adds without a `:u:` scope is covered before it is written. + Only device-scoped entries survive: the arriving session's own bearer token, the pointer + recording whose state the browser holds, and the UI theme. + + Both properties objectui#5703 established are preserved and still pinned: `get()` prefers a + non-null `localStorage` read and falls back to the in-memory value, and the memory value is + nulled BEFORE storage is touched — by `clear()` as before, and now by the user-change purge + too, so the outgoing user's org id cannot outlive their persisted key on the sign-out-then- + sign-in path that never reloads the page. + + Existing browsers are not migrated. A value sitting under the retired bare key is + unattributable — nothing recorded whose org id it is — so migrating it is precisely the + defect it would be migrating away from, and it is deleted instead. A signed-in user loses + nothing durable: the active organization is a server-owned fact that + `AuthProvider.refreshOrganizations` re-asks for whenever the list is non-empty and no + active org is held, including the ADR-0081 single-membership repair. One boot re-supplies + it; users with no organization land on the guided first-run flow, which is the outcome this + card is about. + + `apps/console`'s pre-render auth preflight purges every spelling of the active-org key — + the retired bare one and each `:u:` scope — when it finds a dead bearer token, and + deliberately leaves the session-user pointer in place so the next sign-in can still tell + that the browser changed hands. +- 2dd9443: `purgeSignedOutClientCaches()` — the sweep that drops the signed-out user's + `objectui:metadata:*` seed cache on sign-out (objectui#5198) — now costs one key when a + single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5777). + + The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key `n` + aborted the walk, so keys `n+1..end` were never swept, and the failure was swallowed — + `AuthProvider`'s `signOut` believed the purge had completed. The entries this sweeps are + the previous principal's org-scoped, PERMISSION-FILTERED app list — objectui#5198 + classifies a surviving entry as a cross-principal disclosure on a shared browser, not + mere staleness — so a partial sweep here is the sharper half of the same defect class + objectui#5763 fixed on the sign-in path (`sweepStore` in `ActiveOrganizationStorage.ts`). + + `Object.keys(sessionStorage)` — the reason a guard exists here at all — stays guarded on + its own; only the per-key guard is new. Same as `sweepStore`, a failed removal here is + not verified by read-back and not quarantined the way `ActiveOrganizationStorage.clear()` + (objectui#5731) quarantines a key: this function does not own reads for the metadata + seed cache (`MetadataProvider` in `@object-ui/app-shell` does), so there is no `get()` to + guard and nothing to quarantine — adding read-back verification would be a general + storage-error-handling refactor of the module, out of this card's scope. What is + mirrored is the reporting channel: a key whose `removeItem` throws is named in a + `console.warn`, the same channel `sweepStore` and `clear()` use, so a partial sweep is + discoverable instead of silent. + + Adds a partial-failure test: a `sessionStorage` whose `removeItem` throws on one metadata + key, asserting every other metadata key on both sides of it is still swept and unrelated + non-matching keys are untouched, plus a control that the warning fires only on an actual + failure. +- ff2d547: `sweepStore()` — the walk that drops the previous user's `localStorage`/`sessionStorage` + state on a change of session user (objectui#5664 part 3) — now costs one key when a + single `removeItem` throws, instead of aborting the rest of the sweep (objectui#5763). + + The `try` wrapped the WHOLE loop, not each removal. A `removeItem` that threw on key + `n` aborted the walk, so keys `n+1..end` were never swept, and the failure was + swallowed — `purgePreviousUserClientState()` returned normally and `SessionUserScope.adopt` + believed the sign-in purge had completed. This is an ALLOWLIST sweep precisely so the + next un-namespaced key — one nobody has written yet — cannot re-open the cross-user + pollution class #5664 fixed; a partial sweep is a partial allowlist, and which keys + survived depended on `Object.keys` iteration order rather than on anything bounded. The + previous user's org id, recents, favourites, or a `sessionStorage` metadata seed (their + permission-filtered app list, a cross-principal disclosure per objectui#5198) could all + land on the wrong side of the abort. + + `Object.keys(store)` — the reason a guard exists here at all — stays guarded on its + own; only the per-key guard is new, so one uncooperative key now costs exactly that key. + + Unlike `ActiveOrganizationStorage.clear()` (objectui#5731), a failed removal here is + NOT verified by read-back and NOT quarantined: `clear()` owns every future read of its + one key through `ActiveOrganizationStorage.get()`, so a "still readable" verdict and a + quarantine are what keep a failed `clear()` from handing the value straight back. + `sweepStore` walks keys it does not own reads for — another package's recents cache, a + metadata seed — so there is no `get()` here to guard and nothing to quarantine; adding + read-back verification for keys this function does not otherwise touch would be a + general storage-error-handling refactor of the module, which this card is scoped away + from. What IS mirrored is the reporting channel: a key whose `removeItem` throws is + named in a `console.warn`, the same channel `clear()` uses, so a partial sweep is + discoverable instead of silent. The caller (`SessionUserScope.adopt`, on the sign-in + path, inside an `AuthProvider` effect) still cannot act on the failure and must not + throw either. + + A working `localStorage`/`sessionStorage` behaves exactly as before: every + non-device-scoped key is removed, nothing is reported, and the device-scoped allowlist + (`auth-session-token`, `auth-session-user-id`, `vite-ui-theme`) is unaffected. +- 36918bf: `switchOrganization` now re-resolves identity for the organization it just switched + to by its own explicit decision, instead of depending on an accidental + `TokenStorage` side effect to notice the switch (objectui#5750). + + `AuthProvider.switchOrganization` has never called `loadSession()` itself. Identity + re-resolved across a switch only because `POST /organization/set-active` happens to + return the SIGNED `token.signature` spelling in `set-auth-token`, which differs from + the UNSIGNED `session.token` spelling `getSession()` normally stores — so + `TokenStorage.set` reads the flip as a rotation and the objectui#4467 subscription + calls `loadSession()` for it (measured and pinned in objectui#5749/#5719). That + signed spelling is deterministic on the raw session token, not on the organization: + two switches with no `get-session` landing in between produce the identical signed + value, so the SECOND `TokenStorage.set` sees no change, never notifies, and identity + is left answering for whichever organization the FIRST switch targeted even though + `activeOrganization` already reads as the new one. + + Reachable in the console via `OrganizationLayout`'s slug-driven effect (the "Manage" + link on an org card, plus its own "Back to organizations" button) — ordinary + client-side navigation with no full-page reload and nothing debouncing repeat + switches. `WorkspaceSwitcher` and `OrganizationsPage`'s own card click were not + reachable paths for this: both force `window.location.href` immediately after a + successful switch, and the resulting fresh `AuthProvider` mount always performs an + authoritative `loadSession()` regardless of how the race above resolved. + + `switchOrganization` now tracks the organization it last resolved to itself and + re-resolves explicitly whenever a switch's target differs from that, while + suppressing the (now redundant) rotation notification for its own `set-active` + call — so the common single-switch path still spends exactly one `get-session`, not + two. A generation guard discards a still-in-flight, now-superseded switch's answer + rather than let it clobber a later switch's fresher one. +- 934a532: Document the `X-Tenant-ID` edge contract that `createAuthenticatedFetch` stamps, and the + unstamped-first-request window in which it is not sent (objectui#5279). Documentation + only — no behaviour changes. + + The header had no written contract anywhere, and the shape of the missing information was + actively misleading: its only non-CORS consumer lives in the **cloud** repository, so a + search confined to this repo and the framework (`objectstack`) returns zero readers and + reads as "nothing consumes this stamp". #5279 was filed on exactly that reading, and was + held until a cloud-side reading came back non-empty. Without the contract written down, + the next person to grep reaches the same false conclusion and deletes a live routing + input. + + `packages/auth/README.md` gains "The `X-Tenant-ID` edge contract": what the header means + (a routing hint carrying the better-auth `activeOrganizationId` — not an identity claim, + not an authorization input, not what scopes rows), who stamps it and under exactly which + condition, who reads it, and what a reader may and may not assume. The framework half is + stated as a negative with its pin — `resolveAuthzContext` takes `tenantId` from the + API-key principal or `session.activeOrganizationId` and from no header — alongside + `plugin-sharing`'s record that trusting `x-tenant-id` as identity *was* a vulnerability. + The configuration half is quoted from the contract this package can actually resolve, + `TenantRoutingConfigSchema` in `@objectstack/spec/cloud`, where `X-Tenant-ID` is the + default of a configurable `tenantHeaderName` and `header` ranks second of six + identification sources behind `subdomain`. + + The unstamped-first-request gap gets its own section: `ActiveOrganizationStorage` is + filled only after `AuthProvider`'s async `getSession` -> `listOrganizations` -> + `getActiveOrganization` chain resolves, so early-boot requests carry no tenant header at + all. What a reader observes is documented as **absent, never present-and-empty**, with the + five situations that open the window and the instruction to fall through to the next + identification source rather than fail closed. The gap is recorded, deliberately not + closed: the cloud readers observe today's behaviour, so changing when the header first + appears is its own decision. + + Three cases in `createAuthenticatedFetch.test.tsx` pin the statements the prose makes + about the wire — no active organization means no header at all, the stamp is not gated on + `/api/` the way `Authorization` is, and the active organization overwrites a caller-set + `X-Tenant-ID` — so the documentation cannot drift away from the behaviour unnoticed. +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..54d1e698d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,120 @@ # @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 [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/react@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 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..61e8d68c8 100644 --- a/packages/collaboration/CHANGELOG.md +++ b/packages/collaboration/CHANGELOG.md @@ -1,5 +1,43 @@ # @object-ui/collaboration +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [20e317c] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [38a9568] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..d47903186 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,789 @@ # @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<FlexSchema, 'type'>`, whose spacing key is `gap`. + `spacing` was declared by nothing: not the TypeScript interface, not the zod + mirror, not the renderer's own `inputs` registration. `stack.tsx` read it anyway, + as `schema.gap ?? (schema as any).spacing ?? 2` — and the `as any` is the whole + story, since it existed to get past the type system saying the key was not there. + A lenient consumer leg does not stay in the consumer: it becomes a second + de-facto contract that producers write to, and 135 nodes across 39 files of the + shipped schema catalog did exactly that. Every one of them rendered correctly, so + nothing ever pointed at it, while the examples went on teaching the key to every + author who copied them. + + The trap it was one edit away from springing: `flex` — semantically a `stack` + with a `direction` — never read `spacing`, so re-typing any of those nodes would + have dropped the spacing to the default silently. Fixed at the producer + (AGENTS.md #0.1): those nodes now author `gap`, carrying the same value, and the + alias is deleted rather than legalised into `StackSchema`, where it would only + have been a second name for `gap`. + + **If you author `spacing` on a `stack`**, rename it to `gap`; the value and the + rendering are unchanged. A `stack` still carrying `spacing` now renders the + default gap, exactly as a `flex` always did. + + Also in the same sweep, and visible only in the published example catalog rather + than in any package API: 140 catalog nodes that were already `flex` / `stack` / + `container` stopped hand-writing their own declared props in `className` + (`items-center` → `align`, `justify-between` → `justify`, `gap-2` → `gap`, + `flex-wrap` → `wrap`, `p-4` → a container's `padding`) — 231 tokens in all + (objectui#4891). Breakpoint-prefixed overrides and everything decorative stay in + `className`, because the props are not responsive. Both facts are ratcheted in + `examples/schema-catalog/test/layout-props-conversion.test.tsx`. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- fa429cf: The register-meta key `defaultChildren` is retired (objectui#5051). + + It was declared in four places, produced in eleven, and read in **none**. The designer's + drop path builds a new node from its twin key only — `PageDesigner.tsx`, + `props: paletteItem?.defaultProps ?? {}` — with no `children:` line, so a palette item + that declared `defaultChildren` dropped an **empty** node and the declared children never + materialised. Nothing rendered the wrong thing; an entire declaration surface was simply + inert, which is the declared-but-unenforced shape ADR-0049 targets. Per the maintainer + ruling of 2026-08-19, the key is removed rather than wired up; if designer + default-children UX is ever product-wanted it returns as its own designed card. + + **If you author plugins against the published register-meta table, drop the key.** It is + gone from `skills/objectui/guides/plugin-development.md`, which had been teaching it. A + meta that still declares it stays *valid*: `ComponentMetaSchema` is a plain `z.object`, + and measured on zod 4.4.3 that STRIPS unknown keys rather than rejecting them — so the + key is silently dropped from the parse output instead of failing validation. TypeScript + authors get the loud signal instead: all three `ComponentMeta` declarations + (`@object-ui/types` `base.ts` and `plugin-scope.ts`, `@object-ui/core` `Registry.ts`) no + longer offer it, so re-declaring it is now a compile error. + + **No runtime behaviour changes in either direction.** No code path read the key before + this change, and the eleven producers that set it (`sidebar.tsx` x10, `span.tsx`) were + feeding a reader that did not exist. Dropping a `span` or any of the ten sidebar types + into the designer produces exactly the node it produced yesterday. + + Two suites keep it retired, one per package: `packages/types` pins the zod twin (the key + is absent from the parse output, with a surviving sibling asserted present through the + same parse as the control) plus the two TS twins with `@ts-expect-error`, and + `packages/core` pins the registration surface the eleven producers were written against. + Both are compile-time-enforced through each package's chained `tsconfig.test.json`. +- 3ccd9e8: Split the static `table` column type off the rich shared `TableColumn` + (objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = + enforced holds per renderer. + + `TableColumn` is unchanged and remains the rich shape `data-table`, + `CRUDSchema` and detail-view relations honour. The static `table` renderer's + `TableSchema.columns` now declares the new narrow `StaticTableColumn` + (`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the + keys that renderer reads). The eleven keys the static renderer never read are + retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on + `TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / + `filterable` / `resizable` / `editable` / `cell` on its columns. + + Breaking for authored metadata that wrote those keys on a `type: 'table'` + node: they were silently inert before and are now refused loudly — a tsc + error on the interface (`?: never`) and a parse rejection naming the key in + `@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: + nodes that wanted the interactive behaviour move to `type: 'data-table'` + (whose columns keep the rich `TableColumn`); right-aligned columns on the + static table use `cellClassName: 'text-right'`; alternate-row styling uses + Tailwind on `className`. +- 72ffc34: Retire `ActionParamDialog`: the `custom` barrel's second action-param dialog is + removed, and the app-shell dialog is recorded as the surviving implementation + (objectui#5685, maintainer ruling of 2026-08-22). + + **Breaking for any out-of-repo host that imported it** (declared `minor` per the + repo's version-alignment rule — the major tracks `@objectstack`, never an + API-break count): `@object-ui/components` no longer exports `ActionParamDialog` + or `ActionParamDialogProps`. Measured at the branch point, the export had zero + production consumers — its only in-repo importers were its own five test files, + which retire with it, and no other repository in the organization imports the + symbol from this package. + + This file was the repo's SECOND implementation of the action-param surface, and + its audit trail is the reason it retires instead of being maintained: the last + close look (objectui#4758) found per-option `visibleWhen` not evaluated at all, + and the five hardcoded English strings this card originally recorded were the + next drift installment. A dormant second dialect of a governed surface is one + production import away from being live; removing it removes the whole drift + class. + + FROM → TO for an out-of-repo host: + + - `import { ActionParamDialog } from '@object-ui/components'` — no drop-in + replacement is published. The surviving implementation is + `@object-ui/app-shell`'s `ActionParamDialog` (`src/views/ActionParamDialog.tsx`), + rendered by app-shell's action runtime (`useConsoleActionRuntime`, + `RecordDetailView`) rather than exported standalone. A host that needs its own + param form builds on `@object-ui/fields`' shared field widgets + (`resolveFormWidgetType` / `getLazyFieldWidget`, ADR-0059) — the same seam the + surviving dialog renders through. + + The unreleased objectui#4758 changeset for this component (`select` options + through the shared option evaluator) is withdrawn in the same change: the + component retires before that fix ever ships, so the release notes carry the + removal rather than new behaviour of a surface this release does not contain. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 490f482: The static `table` renderer reads only the declared `TableColumn` contract, and its published reference page teaches that spelling. + + `renderers/complex/table.tsx` resolved a heading as `col.header || col.label` and a + cell as `row[col.accessorKey || col.name]`. Neither `label` nor `name` is declared + on `TableColumn`, which declares `header` and `accessorKey` — both required + (`packages/types/src/data-display.ts`). This was the fourth site of the + column-alias family, after `data-table`, `ObjectDataTable` and `ObjectGrid` + (objectui#5350). + + Both aliases are retired. The ruling recorded on objectui#5120 (2026-08-20) is the + family direction — *retire the consumer-side alias; unify the producers* — and it + names this site: the declared `header`/`accessorKey` contract wins. + + What makes this site different from its three siblings is that the alias was not + merely tolerated, it was **published**. `content/docs/api/schema-reference.md` + §TableSchema shipped a copyable `{ "name": "id", "label": "#" }` example and a + property row reading *"Column definitions with `name`, `label`, …"*, while + `packages/types` declared the opposite pair. Docs and type disagreed about one + type they both call `TableColumn`, each internally consistent. Retiring the alias + without correcting the page would have turned a documented, working example into a + silently broken one, so both halves land together: the page now authors + `accessorKey`/`header`. The same row also advertised a `render` property that + `TableColumn` has never declared — the renderer's hook is `cell` — and that claim + is dropped rather than re-spelled. + + The failure mode of a now-unresolvable column is worth stating, because it is + quiet: the column keeps its slot and its neighbours are unaffected, the header or + the cells simply render empty, and nothing throws. This renderer keys its cells by + index rather than by accessor, so unlike `data-table` it does not even produce + React's generic missing-key warning — a retired-spelling column is fully silent. + Whether that silence should become an authoring diagnostic is objectui#5349's + question; no diagnostic is added here. + + The two `columns.map` callbacks are typed `TableColumn` instead of `any`, so + re-introducing an undeclared alias on this renderer is now a type error rather + than a reviewer's catch. + +### Patch Changes + +- fe76ece: The typings both packages publish now carry an explicit extension on every relative specifier, so a consumer on `moduleResolution: nodenext` can follow them. + + `vite-plugin-dts` emits one declaration file per source file, and TypeScript + copies a module specifier into the declaration verbatim. `export * from './ui'` + therefore shipped extensionless in `dist/index.d.ts` — 21 such re-exports in + `@object-ui/components`, 7 in `@object-ui/layout`, 128 across the two emitted + trees. Node16/NodeNext resolution does not extension-search a relative + specifier, so the compiler could follow none of the hops and every symbol they + carried read as absent from the package: + + ``` + error TS2305: Module '"@object-ui/components"' has no exported member 'Badge'. + ``` + + Measured on `@object-ui/app-shell`, the largest consumer and the one that pulls + in both packages: 880 TS2305 across 162 files (864 from `components`, 16 from + `layout`), plus 215 TS7006 as fallout from the imports that stopped resolving. + On `@object-ui/fields`, 178 TS2305 and 57 TS7006. Both are zero now. + + The emitted `.js` never had the defect — rolldown resolves the same specifier + away — which is why `pnpm check:esm-specifiers`, whose verdict is about + specifier-preserving `.js` builds, correctly never scanned either package. The + fix is therefore in the declaration EMIT (`scripts/vite-dts-explicit-extensions.ts`, + shared by both `vite.config.ts` files), not in the sources: the same source line + produces a clean `.js` and a broken `.d.ts`, so no source edit can express the + difference. The rewriter resolves each specifier against the source tree the + output mirrors — a file hop becomes `./x.js`, a directory hop `./x/index.js` — + throws on anything it cannot resolve, and after the build re-parses the emitted + declarations to assert every relative specifier both carries an extension and + names a file the build really emitted. + + `packages/fields` takes the `nodenext` pin as a result — the same two lines + `packages/react` has carried since objectui#4538 — so the property is enforced by + the compiler on the consumer side rather than by review. `packages/app-shell` + does not: it type-checks clean without the pin and still shows 23 errors with it, + none of them from these two packages. That residue is filed separately. +- 58770f3: **Behaviour change:** the eighteen renderers whose host element is a form + control no longer forward their whole prop bag to it. They route it through a + form-control DOM declaration — the same `pickDomProps` mechanism `grid`, `flex`, + `stack`, `container` and `text` were converged on — so an authored schema key + becomes an HTML attribute only if the contract declares it one (objectui#5632, + the `BARE_SPREAD_MINUS_NAME` slice of objectui#5574). + + The eighteen: `action:button`, `action:icon`, `ui:button`, `ui:checkbox`, + `ui:combobox`, `ui:date-picker`, `ui:email`, `ui:file-upload`, `ui:input`, + `ui:input-otp`, `ui:password`, `ui:radio-group`, `ui:sidebar-menu-button`, + `ui:slider`, `ui:sonner`, `ui:switch`, `ui:textarea`, `ui:toggle`. + + What this stops reaching the DOM: the renderer's own declared props, consumed + off `schema` to render the control AND spread onto the element a second time as + attributes HTML does not define, plus the authored node's SDUI metadata and the + flattened `props` container. Measured across `examples/schema-catalog`, rendered + through the real `SchemaRenderer`: 284 illegitimate attributes over 287 + form-control nodes — `button[label]` 140, `button[icon]` 25, `input[inputtype]` + 23, `input[label]` 19, `toggle[label]` 14, `radio-group[options]` 8, + `date-picker[placeholder]` 7, `file-upload[label]` 7, `file-upload[buttontext]` + 7, `checkbox[label]` 6, `textarea[label]` 6, `toggle[arialabel]` 6, + `switch[label]` 4, `password[label]` 3, `email[label]` 2, `radio-group[direction]` + 2, and six singletons. The same probe reads 0 after, with `grid`'s 26 nodes at 0 + both times as the control and an unchanged node census across the two runs. + + `name` and `disabled` are DELIBERATELY still forwarded. Both are legal on a form + control and neither was ever part of the leak — the ledgered shape for this + group is thirteen attributes, not fourteen, precisely because HTML defines + `name` on these hosts. The whitelist this group uses is therefore the shared + SDUI one PLUS those two, not the bare `toDomProps` the container renderers take: + that would have stripped the form-serialization key off every control and + silently re-enabled every disabled one, and no gate in the repo would have gone + red for it. + + Nothing an author writes renders differently: every leaked key was already being + read off `schema` and applied. `id`, `role`, `tabIndex`, `className`, `style`, + event handlers and the open `data-*` / `aria-*` families are unchanged. + + Anything that read one of the leaked attributes off the DOM — a CSS attribute + selector such as `[label="Save"]`, or a test asserting `inputtype` on a rendered + `ui:input` — must read the schema instead. No `@object-ui` code did; this is + called out because the attributes were externally visible while they lasted. +- 485f096: The form renderer now keeps a `defaultValues` reset off `onChange` and off the + `form_change` `onAction` for **every** caller — including one that memoizes the + callback (objectui#5235). + + "A record landing is not a user edit" was already this file's documented, + pinned behaviour, but two of the three channels delivered it by accident of + React's effect ordering: every layout DESTROY runs before any layout CREATE, so + a caller passing a fresh callback each render had its value subscription torn + down before the reset and re-established after. The guarantee was therefore + delivered by the callback's *identity changing*. Wrap the same callback in + `React.useCallback` — taught everywhere as a semantically neutral performance + optimization — and the identity stays put, the effect never re-runs, the + subscription survives the reset, and the whole loaded record comes back to the + host as if the user had typed it: the false "the user edited this" signal + objectui#2968 was filed about, in a form no type, doc or call site warned about. + + The reset now states what those two channels report, the way `onDirtyChange` + already did (it computes its payload against the freshly installed baseline and + calls the host outright). Callers passing inline arrows see byte-identical + behaviour; callers who memoize stop receiving a phantom edit. + + Not a contract change: whether a value channel *should* report a programmatic + reset stays open in objectui#5235. This only removes the answer's dependence on + caller identity. +- b655a9d: The `ui:grid` renderer now forwards to the DOM by whitelist, so schema keys no longer + land on the rendered `<div>` as invalid HTML attributes (objectui#4787). + + `grid.tsx` ended in a bare `{...gridProps}` spread that removed only `data-obj-*` and + `style`, so everything else `SchemaRenderer` hands a registered component reached the + element. Measured on a canary node, eight attributes leaked: + `columns="4"`, `gap="4"`, `mdcolumns="2"`, `smcolumns="2"`, `name="grid_node"`, + `props="[object Object]"`, `colorvariant="x"` (the flattened `props` container) and an + unknown authored `zzcanary="leak"`. A responsive `columns` object rendered as + `columns="[object Object]"`. Layout was unaffected, so every catalog grid example + rendered with them — the reason this went unnoticed. + + The spread now goes through `toDomProps` from `@object-ui/core`, the same whitelist + objectui#3291 established in `packages/fields` and objectui#4425 phase 2 promoted to the + SDUI widget contract. Keys that are *declared* DOM-safe survive — `id`, `className`, + `role`, `tabIndex`, plus the open `data-*` and `aria-*` families, which is how the + designer's `data-obj-id` / `data-obj-type` still arrive — and `style` continues to be + forwarded by name. Nothing an author can add to a grid node reaches the DOM implicitly + any more, including keys `GridSchema` does not have yet; enumerating today's keys to + strip would have re-rotted on the next schema addition. + + No authored input changes and no layout changes: the grid's own vocabulary was always + read off `schema`, never off these props. +- d7bd274: **Behaviour change:** the `flex`, `stack`, `container` and `text` renderers no + longer forward their whole prop bag to the host element. They route it through + `toDomProps` — the same whitelist `grid` was converged on — so an authored + schema key becomes an HTML attribute only if the SDUI DOM contract declares it + one (objectui#5574). + + What this stops reaching the DOM: the renderer's own declared props, which were + consumed off `schema` to build the class list AND spread onto the element a + second time as attributes HTML does not define. Measured across + `examples/schema-catalog`, rendered through the real `SchemaRenderer`: 1194 + illegitimate attributes over 1141 nodes — `text[content]` 522, `flex[align]` + 198, `flex[gap]` 193, `stack[gap]` 153, `flex[justify]` 98, `container[padding]` + 14, `container[maxwidth]` 6, `flex[direction]` 5, `stack[align]` 4, + `text[value]` 1. The same probe reads 0 after, with `grid`'s 26 nodes at 0 both + times as the control. + + Nothing an author writes renders differently: every leaked key was already being + read off `schema` and applied as a class, so the markup loses attributes that + never had meaning and keeps the styling that did. `id`, `role`, `tabIndex`, + `className`, `style`, event handlers and the open `data-*` / `aria-*` families + are unchanged — including `data-obj-id` / `data-obj-type`, which now arrive + through the `data-*` family rather than by hand. + + Anything that read one of the leaked attributes off the DOM — a CSS attribute + selector such as `[gap="4"]`, or a test asserting `align` on a rendered `flex` — + must read the schema or the class instead. No `@object-ui` code did; this is + called out because the attributes were externally visible while they lasted. +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. +- fa140b8: `element:record_picker`'s `emptyText` now resolves the inline per-locale map its + contract has admitted since rc.6, and its published declaration says so + (objectui#5590). + + `@objectstack/spec` widened this key to the `I18nLabel` union + (`string | Record< string, string >`) at 17.0.0-rc.6, and the installed 17.0.0 GA + still carries it — measured, not assumed: + `ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })` + succeeds. The renderer honoured only the string arm, handing the map straight to a + text node. React refuses a plain object in a child position rather than stringifying + it, so an author writing the map form the contract accepts did not get a mis-rendered + empty state — the whole picker subtree threw + `Objects are not valid as a React child (found: object with keys {en, zh-CN})`. + + The read site now resolves through `pickLocalized`, the objectui-side helper the + sibling text-node sites already read through (`element:text.content`, + `element:button.label`, `page:card.title`), which spells a miss as `''` rather than + the spec resolver's `undefined`. The default is applied before resolution, so + `emptyText` absent still means "No records" and an authored empty string still + renders empty. + + The `ComponentMeta` entry, which held a single `'string'` arm precisely because the + renderer dropped the other one, now declares `['string', 'object']`. That narrowing + was correct for exactly as long as it was true: with the map arm reaching the screen + resolved, withholding it would be the false declaration in the other direction — the + manifest gate reporting `type-mismatch` on a legal write the same input's own + `description` teaches the author to make. The `apps/console` specimen that pinned the + narrow arm named this release condition in its own words ("keeps its single `'string'` + arm until the render site catches up") and is flipped here, keeping its controls. + + Three comments in the renderer deferred this gap to objectui#4163, which closed as + completed on 2026-08-15 while the gap was still open; the file now carries no + reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this very + key as its worked example of an arm deliberately withheld, and is corrected in the + same change so the example stays true. +- 71cba28: `element:record_picker`'s `label` is now programmatically associated with its + `SelectTrigger` combobox — fixing a case where the caption the block's own + registration prose advertised (`'Caption rendered above the picker, in a + <label> element'`) named nothing at all (objectui#5771). + + Pre-fix, the render body was `{label && <label className="…">{label}</label>}` + against a `SelectTrigger` with no `id`: neither end of the association carried + any wiring. That is a step worse than the sibling gap objectui#5735 closed on + `element:text_input` — there the `label` half was already correctly wired and + only `description` was adrift; here the label element had no `htmlFor` and the + trigger had no `id`, so the picker had no accessible name unless a + `placeholder` happened to render text into the trigger's content. + + The fix follows the shape objectui#3341 already ruled on for the same defect + class (`InlineCreateRelated`'s `<label>`/`<Input>` pair) and objectui#5735 + just landed on the complement block: `htmlFor`/`id` against the + `SelectTrigger` — a `button` with `role="combobox"`, and therefore labelable, + so no `aria-labelledby` is needed (Radix sets none on the trigger) — wired + **when `schema.id` exists**, matching `element:text_input`'s precedent rather + than an always-on `useId()` fallback. Only the author can supply `schema.id`, + so a picker authored without one keeps its pre-fix, unassociated caption text + exactly as before; nothing about this change mints an id the author did not + provide. The `<label>` element itself is also swapped for the shared `Label` + (`@radix-ui/react-label`) primitive `element:text_input` already uses for the + same wiring, rather than a bare `<label>` with a `htmlFor` bolted on. + + The registration prose for `label` is corrected to describe what the code now + does (tied to the control by `htmlFor` when the node carries an `id`) instead + of a caption association the renderer never performed. +- 190fbd0: `element:record_picker`'s `label` and `placeholder` now resolve the inline + per-locale map their contract admits, and their published declarations say so + (objectui#5637). + + These are the two remaining keys of the trio objectui#5590 fixed one of. All + three members of `ElementRecordPickerPropsSchema` are the `I18nLabel` union + (`string | Record< string, string >`) — measured on the installed + `@objectstack/spec` 17.1.0 pin, where each resolves to + `optional -> union -> string | record` and + `safeParse({ object: 'account', <key>: { en, 'zh-CN' } })` succeeds. The + renderer honoured only the string arm on both, and the two keys failed in two + different ways: + + - `placeholder` was read raw and handed to `SelectValue`. React refuses a plain + object in that position rather than stringifying it, so the whole picker + subtree threw + `Objects are not valid as a React child (found: object with keys {en, zh-CN})` + — the same harm objectui#5590 removed from `emptyText`. + - `label` went through the file's local `toText`, whose object branch ends + `String(o.label ?? o.name ?? o.title ?? o.en ?? '')`. Reaching `o.en` + unconditionally is an English pick wearing locale resolution's clothes, so a + `zh-CN` viewer was shown the English entry — and a map that simply omits `en` + resolved to `''`, which the `{label && …}` render site drops, making the + picker's label element DISAPPEAR with nothing thrown and nothing logged. + + Both keys now resolve at their own read site through `pickLocalized`, the same + helper the settled `emptyText` shape uses. `toText` is deliberately unchanged: + it is shared with the row values (`toText(row?.[labelField])`), which are record + field values rather than `I18nLabel`, so teaching it locale resolution would + have changed a second, unrelated call site. The `placeholder` default is applied + before resolution, so an absent key still means "Select a record…" and an + authored empty string still renders empty. + + The two `ComponentMeta` entries, which held a single `'string'` arm precisely + because the renderer dropped the other one, now declare `['string', 'object']` + — declared in the change that makes the arm render, never before, which is the + order `ComponentInput.type` prescribes and the order `emptyText` set. + + KNOWN GAP, unchanged by this release: the sibling `label` read sites in + `renderers/layout/containers.tsx` compose + `translateLabel(pickLocalized(…), language)`, and that second helper is not + applied here — `translateLabel` and its `KNOWN_LABEL_DICT` are module-private to + that file. Only the locale-map resolution lands in this change; a plain-English + string `label` is still rendered verbatim in every language, exactly as before. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- 5a07e67: The `[page:header]` sparse-predicate warning no longer blames `hidden: true` — it + states what it actually measured (objectui#5399). + + When an action's `visible` predicate references a `record.<key>` the bound payload + does not carry, the warner names the missing key and then explained the cause: + + > Hidden (hidden: true) fields are stripped from detail payloads server-side, so a + > predicate gating on one may evaluate to a hide-by-default verdict. + + That cause is false, and it names a mechanism this repo does not own. `hidden` is a + UI concern — the framework spec describes it as "Hidden from default UI" + (`packages/spec/src/data/field.zod.ts`) — not a projection rule. Confirmed against + the framework checkout rather than taken on trust: ObjectQL's own strip for the + `__search` companion documents that the `hidden` / `readonly` / `system` markers are + "None of them is a PROJECTION rule", which is precisely why a dedicated strip rule + had to be written for that one column; drivers answer a query with no `fields` using + `SELECT *`; and `metadata-protocol` enumerates what the read path does drop — + `internal: true` columns and the `__search` companion, and nothing else. The only two + read-side uses of `field.hidden` in the framework are auto-view/auto-form column + generation and companion-source eligibility, neither of which removes a key from a + record body. + + So an author who read this diagnostic went hunting for a `hidden` flag they would + either not find, or find on a field the payload demonstrably still returns — while + the real source of the sparseness (a projected or partial read) went unexamined. A + confidently wrong cause in a diagnostic is worse than no cause, because it is + actionable in the wrong direction. + + The replacement states the fact this surface can actually see and the consequence it + does own: the page bound a payload without those keys, a projected or partial read + will not carry them, and the predicate therefore fails closed and hides the action. + The measured half of the message — action name, missing fields, predicate source — + is unchanged, and nothing about what triggers the warning changed. + + Message text only. The same false claim also sat in this warner's own doc comments + and in the comments of the test that pins the message; both are corrected here, and + the docstring now carries an explicit note against re-attributing the cause to + `hidden: true`. No other call site was swept. +- 27308c5: `element:text_input` now ties its authored `description` to the field with + `aria-describedby`, so assistive tech announces the helper text as the input's + accessible description instead of leaving it as an unassociated paragraph + beside the field (objectui#5735). + + Before this the paragraph and the input were siblings with no programmatic + relationship: a screen reader moving to the field announced the label and the + value and never the helper text. The `label` half of the same block was already + wired (`htmlFor` against the input's `id`), which is what made the gap specific + to `description` rather than a general absence of a11y wiring — and the + identical key authored on a field INSIDE `renderers/form/form.tsx` has been + announced all along, so one authoring key behaved two ways depending on which + container the author reached for. It no longer does. + + The paragraph's id is minted per instance with `React.useId()` — the same source + `FormItem` mints the form renderer's description id from — and deliberately not + derived from `schema.id`. The two associations in this block need ids on + opposite ends: `htmlFor` names the INPUT, whose id only the author can supply, + so that wiring still holds only when they gave the node an `id`; `aria-describedby` + names the PARAGRAPH, which the renderer owns, so the description association + holds unconditionally and cannot collide when two nodes share an authored id. + The attribute is emitted only when a paragraph is actually rendered — an absent + or empty `description` leaves the input with no `aria-describedby` rather than a + dangling reference. + + The key's published `ComponentInput` description, which documented this gap in + so many words, is rewritten in the same change. Its closing advice — prefer + `label` for an instruction a user must not miss — is kept rather than deleted, + on a new basis: a description is announced after the field's name and screen + readers gate description text behind verbosity settings a user can turn down, so + it remains the half of the announcement most likely to go unheard. +- 8689166: `element:text_input` now DECLARES the inline-translation arm on `label`, + `placeholder` and `description`, so the manifest gate stops reporting + `type-mismatch` on a locale map its own renderer has always resolved correctly + (objectui#5717). + + `@objectstack/spec` types all three keys as the `I18nLabel` union + (`string | Record<string, string>`) — measured on the installed 17.1.0 pin, per + key, from the schema's own verdicts — and `text-input.tsx` has resolved all + three through `pickLocalized` at their read sites since it was written. Only the + `ComponentMeta` entries stayed at a single `'string'` arm. Driven through the + same `manifestFromConfigs` + `validateTree` pair the JSX-page compiler and the + save gate use, an author writing `{ en: 'Owner', 'zh-CN': '负责人' }` got three + warnings on a write that renders correctly in the viewer's language: + + ``` + <element:text_input> prop "label" expected a string + <element:text_input> prop "placeholder" expected a string + <element:text_input> prop "description" expected a string + ``` + + That is the INVERSE of the direction the rest of this family moved in. + `element:record_picker.emptyText` (objectui#5590) and that block's `label` / + `placeholder` (objectui#5637) each held one arm for exactly as long as their + render site dropped the map, and gained the object arm in the change that taught + the render site to resolve it — never declare an arm the renderer drops. Here + the render site was never behind, so the same rule's other half applies: + withholding an arm the renderer resolves is the false declaration in the other + direction, and noise on legal writes trains authors (AI authors included) to + dismiss the `unknown-prop` and `type-mismatch` reports that are real. + + Per-key, not blanket. `defaultValue` on this same block keeps `['string', + 'number']` — its contract has no object arm, measured the same way — and the + console specimen file keeps asserting that an `I18N_MAP` there still reports + `type-mismatch`. That control is what makes this a widening of three keys rather + than of a component. + + All three keys also gain a `description` written from what the renderer does + rather than from restating the contract: where each one lands in the rendered + output (a `<label>` above the field, the native `placeholder` attribute inside + it, a `<p>` below it), when each is omitted, and the locale fallback chain the + read site follows. No renderer behaviour changed. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [8d58f46] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [305205a] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/sdui-parser@17.7.0 + - @object-ui/react@17.7.0 + - @object-ui/react-runtime@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/components/package.json b/packages/components/package.json index 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..cb5812740 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,470 @@ # @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<TSchema>` with `schema: TSchema`, defaulting to + `BaseSchema`. `schema` therefore **narrows** back to the object form — core's + copy had silently widened when objectui#4608 made core's `SchemaNode` a + re-export of types' union — and the type gains a parameter. **Nothing imported + it**, on either side, re-verified repo-wide on the merged ref, so no call site + can observe either move; the narrowing is recorded here because it is a change + to a published type, not because a consumer is affected. + + A compile-time pin now holds the reconciliation from + `@object-ui/react` — the only position that resolves both packages through + `node_modules` — alongside the existing `SchemaNode` one. It is a test-only + addition and emits nothing, so `@object-ui/react` takes no bump of its own. +- 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`. +- d1ab06f: Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now + warn once, and are unchanged otherwise. + + A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row + scope, a `record:alert`) has bound the row three ways since objectui#4075 — + `record.status`, bare `status`, and `data.status` — without any of them being + declared the contract. The maintainer ruled that question on 2026-08-20 + (objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for + the identical renderer-tolerance shape: **the canon is `record.*`**, and the + other two enter a deprecation window. + + The canon states the **server's** accept set, which was this card's first + measurement and turns out to be strictly narrower than the renderer's. Measured + against `@objectstack/formula@17.1.0`, the engine the server evaluates with: + + | spelling | server runtime | server authoring oracle | + |---|---|---| + | `record.status` | `{ ok: true, value: true }` | accepted | + | bare `status` | `Unknown variable: status` | refused | + | `data.status` | `Unknown variable: data` | **silently accepted** | + + `buildScope({ record })` mounts exactly `['record']` — `data` is never bound and + the row's fields are never flattened to top level. The three-way binding is a + client tolerance with no server counterpart, which is why the warning belongs on + this side. + + `data.*` is the dangerous one, and the reason the warning exists. `data` is in + `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle + waves it through — that list is a deliberately generous "never faults" lint + baseline, not the runtime accept set. A `data.*` row predicate therefore passes + every authoring gate the platform has and then binds nothing at runtime: not an + error, a constant `false`. A `visible` that is constantly false is a button that + silently never appears — the objectui#4075 fail-closed signature. + + What ships: + + - `@object-ui/core` exports `detectNonCanonicalRowSpelling`, + `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and + `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, + which carries the canon statement and the measurement. + - Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and + `useCondition` (react, for bags bound by `usePredicateRecordContext`). + - Detection reuses the server's own oracles (`collectCelRootIdentifiers`, + `firstUndeclaredReference`) rather than a regex, so no second dialect + judgement is invented client-side. + + **No spelling is removed and no behaviour changes.** Every predicate that + resolved before resolves now — the ruling defers removal behind a stored-metadata + survey, and the warning is what makes that survey possible (ADR-0078: a + tolerance nothing ever reports can never be retired). + + The deprecation is scoped to the **runtime record layer**. `data` remains the + canonical root one layer over, in a metadata-editing form (ADR-0089 D3 + `CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. +- 91783c4: Three more secret-field spellings no longer render a secret in clear text on the form's unregistered-widget branch. + + Measured on `main` at `f2e11ae6f`, the real `form` renderer on the built-in path + (no `registerAllFields()`), before and after objectui#5322's fix: + + ``` + type registry hit rendered type + ui:password true text + secret false text + field:secret false text + ``` + + Two halves, per the maintainer ruling of 2026-08-20: + + - **`@object-ui/core` — an unresolvable namespaced widget id is now an authoring + ERROR.** A form field's widget id (`widget`, else `type`) may name the + `field:` namespace or a bare name; any other namespace resolves no field + widget (objectui#5254) and used to degrade silently to a plain text box. + `validateSchema` now reports `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE` and + `assertValidSchema` throws. Behaviour change: a schema that previously + validated with e.g. `type: 'ui:password'` is now invalid — inventing a + plausible-looking widget id fails loudly instead of rendering clear text. + `field:` ids stay valid whether or not the widget is registered, since + registration is a runtime fact an authoring-time validator cannot see. + - **`@object-ui/components` — the known secret types cover the remaining + spellings.** Bare `secret` and `ui:password` render the native masked input, + and `field:secret` is refused outright like `field:password`. Existing authors + need no migration. + + `ui:password` **is** registered — as an SDUI node renderer for a top-level + `{ type: 'email' }`-style node — so an author who checked whether it resolved + got a yes and still got a clear-text box on the field path. No producer emits + any of the three; all are reachable only through a hand-authored standalone + form schema, which is exactly the surface where the author is the producer and + no normalizer sits in between. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- 8ebd57f: Both `evaluateExpression` references in the `ExpressionEvaluator.registerFunction` + JSDoc are now qualified, so each resolves to the entity it means (objectui#5580). + + `ExpressionEvaluator.ts` declares two things spelled `evaluateExpression`: the method + on `ExpressionEvaluator` (bare expression, throws) and the module-level export + (context bag, fail-soft, delegating to `evaluate`). The `registerFunction` block + referred to both under the one spelling, four lines apart. + + The prose link was not merely ambiguous, it was bound wrong. Measured with + `checker.getSymbolAtLocation` on the pre-fix source, `{@link evaluateExpression}` + resolved to the module-level `FunctionDeclaration` — the fail-soft one — inside the + sentence that calls it *"the throwing sibling"*. The neighbouring `{@link evaluate}` + binds to the method, but only because no module-level `evaluate` exists to outrank + it, so the rule "an unqualified link resolves to the enclosing class's member" does + not hold here. The link is now `{@link ExpressionEvaluator.evaluateExpression}`, + which the checker resolves to the `MethodDeclaration`. + + The `@example`'s final line is the module-level export — its second parameter is a + context bag and the `${...}` wrapper only resolves on the `evaluate` path — but it sat + two lines below calls that establish `evaluator.` as the receiver, and a `.d.ts` hover + carries no import to disambiguate. It now names the module-level export and shows the + import it needs. + + This is prose only: the diff is confined to a block comment and no declaration moves. + It is scored `patch` rather than the empty-frontmatter form because the block is + emitted into what npm ships — measured, this edit moves both + `dist/evaluator/ExpressionEvaluator.d.ts` and `dist/evaluator/ExpressionEvaluator.js` + (this package builds with a bare `tsc`, which preserves comments in the JS emit), and + the ten changed lines in that JS are all comment lines. + + `registerFunction-jsdoc-links.test.ts` pins the binding against the checker rather + than asserting it in prose, since a `{@link}` that binds to the wrong entity is + indistinguishable in source from one that binds right. +- 7138bc1: The dev-mode unknown-key warning stops flagging `overrideNotice`, the console's + privileged-override safety copy (objectui#5611). + + `ActionRunner.execute` classifies the object it was HANDED, and a console host + hands it a DISPATCH, not a stored metadata row. `DeclaredActionsBar` composes + `overrideNotice` on that dispatch and two param-collection handlers read it — + yet the key inventory only mirrored AUTHORED surfaces, so the runner reported a + key two files read as one "no reader recognizes", and prescribed promoting it to + an explicit field on `ActionDef`. That prescription is the one shape the + 2026-08-22 maintainer ruling forbids for this key, so acting on the diagnostic + walked an author into a rejected design. A false warning on the product's own + privileged path — the branch that finalises an approval over approvers who have + not acted — is how a dev console gets muted. + + Adds an exported `HOST_DISPATCH_ACTION_KEYS` (sole member `overrideNotice`) to + `actions/actionKeys.ts` and unions it into `KNOWN_ACTION_KEYS`, which is the + fourth input to that set and the first one that is not an authored-surface + mirror. Measured before and after on the exact dispatch the bar composes: the + warning went from one call naming `overrideNotice` to none, `KNOWN_ACTION_KEYS` + grew by exactly one member, and an action carrying a real typo alongside it + still warns — naming `targt` only. + + The authored surface does not move. `overrideNotice` is still NOT declared on + `ActionDef` and still NOT in `ACTION_DEF_KEYS`; writing it in an action literal + remains a compile error, and the AST-derived pin over the interface is unchanged. + Membership in `KNOWN_ACTION_KEYS` widens what the WARNING tolerates, never what + an author may write — `actionKeys.pin.test.ts` now pins both halves, including + the new list's exact contents so a second member cannot arrive quietly. +- cef27e2: The value-fallback label prettifier `humanizeLabel` has one implementation instead of two byte-identical copies. + + `humanizeLabel` turns a stored value into a display string when nothing else + resolves it — an option with no declared label, an object name, a chart axis + member. It existed twice, byte for byte: once in `@object-ui/fields` (read by + `plugin-grid`, `plugin-gantt`, `plugin-detail` and by that package's own + renderers) and once as a deliberate local copy in `plugin-charts`' + `ObjectChart.tsx`, whose comment said it was there "to avoid a dependency on + `@object-ui/fields`". + + Two copies of one convention is a live hazard rather than tidiness: one + dashboard can hold a chart and a grid over the same stored value, so a change + landing on one copy alone would put that value on screen under two spellings at + once. The single implementation now lives in `@object-ui/core` — the shared + ancestor both packages already depend on, so the dependency the copy existed to + avoid is still avoided and no new edge is created, and core takes no React + (objectui#4389: core-canonical logic, plugins consume). Both former sites + re-export it, so `import { humanizeLabel } from '@object-ui/fields'` keeps + working unchanged. + + **Nothing rendered changes.** The surviving implementation is byte-identical to + both deleted copies, and each former call site is pinned by identity against the + core function — not by a copied output table that someone would have to remember + to edit in two places. + + The core module also writes down, for the first time, why this convention stays + distinct from `humanizeFieldKey` (the KEY fallback, in `@object-ui/plugin-dashboard`), + which additionally splits camelCase: + + ``` + input humanizeFieldKey humanizeLabel + needs_analysis Needs Analysis Needs Analysis + NeedsAnalysis Needs Analysis NeedsAnalysis <- differ + unitPrice Unit Price UnitPrice <- differ + BestCase Best Case BestCase <- differ + lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ + ``` + + A field KEY is authored in the codebase and carries a machine spelling, so + splitting camelCase recovers words its author meant. A stored VALUE is arbitrary + tenant data, where a mid-token capital is not reliably a word boundary and + splitting it rewrites what the tenant wrote (`McDonald` to `Mc Donald`). The two + conventions also do not nest — on the last row each leaves alone the separator + the other rewrites. Whether they should ever converge is a separate decision + that would move rendered output in four packages at once; it is deliberately not + made here. +- af3861f: `normalizeListViewSchema` now folds the four per-view-type config aliases phase 3 + carried over — `kanban.groupField` → `groupByField`, `kanban.cardFields` → + `columns`, `gallery.imageField` → `coverField`, `timeline.dateField` → + `startDateField` (objectui#2890). + + These are the pre-#2231 objectui spellings, kept declared alongside the spec keys + so stored view metadata would keep validating. They now fold at the same + component boundary as the A1–A5 vocabulary folds, in the same one-directional + shape: the canonical key wins when a config carries both, and the legacy key is + removed from the result so a missed read-site fails loudly instead of quietly + taking the legacy path. + + One rendering behaviour changes, and it is a correction of the same inverted + precedence the `densityMode` fold fixed: `ListView`'s kanban adapter resolves the + card field list as `cardFields || columns` — legacy over canonical — so a kanban + config carrying **both** rendered the legacy `cardFields` value and silently + ignored the spec-canonical `columns`. After the fold the authored `columns` is + what reaches it. Configs carrying only one of the two are unaffected, and every + other reader of these four keys was already canonical-first. + + `calendar.defaultView` is deliberately **not** folded: it aliases nothing and has + no spec counterpart, so it wants promotion upstream rather than a rename. +- 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 [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..721a4c9f8 100644 --- a/packages/data-objectstack/CHANGELOG.md +++ b/packages/data-objectstack/CHANGELOG.md @@ -1,5 +1,170 @@ # @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. +- c7cd2b6: `ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's + ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an + unauthenticated session stop being reported as a missing analytics capability + (objectui#5663). + + Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`: + the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and + the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The + mapping tested `res.status === 501 || res.status === 404` and called all of it + "the analytics capability is not installed", so every unknown dataset produced a + banner telling the operator to install `@objectstack/service-analytics` and mount + `AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on + four HotCRM Executive Overview widgets while the analytics service was installed + and answering — the real condition was an installed `app.objectstack.hotcrm` at + 1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of + the system from the remedy the banner named. + + Three conditions now get three answers, each keyed on the code the framework + declares for it: + + - `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and + `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing + `AnalyticsNotInstalledError` and its copy — one remedy, one message. + - `NOT_FOUND` (404, unknown `datasetName`) throws the new + `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the + dataset and pointing at the installed app's version rather than at the server. + - `UNAUTHENTICATED` (401, `enforceAuth`) throws the new + `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the + request was refused before it ran and therefore says nothing about the + capability. + + The banner also used to print the server's own message in a parenthetical while + contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not + found.` under a headline claiming a missing capability. That is now structurally + impossible rather than merely fixed: the headline is a pure function of `code` and + the parenthetical is a verbatim quote of `message`, both read off the same + response, and a test walks every branch asserting each message carries its own + headline and none of the others'. + + Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its + constructor signature (it gains an optional third `serverCode` argument and a + `serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including + the metadata-admin dataset preview — are unaffected. A 404 carrying a code this + client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now + keeps its server detail instead of being relabelled as a missing capability; a 404 + or 501 carrying no code at all is still read as the capability being absent, since + the route's own `NOT_FOUND` always ships a code. +- 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no + title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three + times (objectui#5024). + + `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four + states out by hand, 42 lines apart in one file, compared by no gate. They are now + one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s + `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies + were restating a schema rather than filling a gap. + + The user-visible half is the banner. Its title was three independent `&&` branches + with no fallback, while the switch that opens the banner is true for any non-`none` + value — so a lock state outside the four opened the box and left the headline + empty. That is reachable without a fifth state ever being added here: + `MetadataClient.layered()` casts the wire value through unchecked, so a newer + server reaches this banner as-is. Measured, not assumed — feeding `no-publish` + through the page rendered the padlock, the border and an empty title. The title is + now a keyed lookup with a loud fallback that names the unrecognised token, so a + fifth state fails `type-check` here and, if one arrives from a server anyway, the + operator reads a sentence instead of a blank box. +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [199d31b] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [af3861f] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..181a1cc14 100644 --- a/packages/fields/CHANGELOG.md +++ b/packages/fields/CHANGELOG.md @@ -1,5 +1,292 @@ # @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 `<p>Rich <strong>text</strong></p>`, + and this repo's designer bridge already maps `richtext` onto its `html` type. The + display registry nevertheless dispatched it to `MarkdownCellRenderer`, whose + sanitizing GFM pipeline runs react-markdown with no `rehype-raw` and therefore drops + raw HTML. Because a richtext value is *entirely* HTML, everything was dropped and the + cell body came out empty — with no error, no fallback and no console warning, so a + populated field read as an empty field and anyone auditing data through a grid + concluded the records were blank. Measured on the same stored bytes, a neighbouring + `html`-typed column rendered them correctly, which is what ruled out "the value never + arrived". + + `richtext` now resolves to `HtmlCellRenderer`, which sanitizes with `sanitizeHtml` + (script/style/iframe/object/embed blocks, inline event handlers and `javascript:` + URLs removed) and keeps everything a rich-text editor legitimately emits — headings, + paragraphs, emphasis, lists, links, quotes. One map entry fixes every read surface at + once: the grid, the kanban card, the gallery, the related list, the dashboard record + panel and the record detail page all resolve their read-mode cells through this same + `getCellRenderer`. + + The markdown pipeline is untouched. Passing raw HTML through it would have "fixed" + one type by moving every `markdown` cell's trust boundary, so `markdown` still drops + raw HTML — pinned alongside the fix, on the same bytes `richtext` must now render. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..a8fd83e78 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,321 @@ # @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: <this page's object> })`, so a flow that + created a quote from an Opportunity page never told the related list that + would now contain it — the record did not appear until a manual reload. The + runner cannot know which objects a flow touched, so it emits + `{ objectName: '*' }`: the same scope, for the same stated reason, that the + record page's manual ⟳ already uses. Everything mounted refetches in place + over the invalidation bus, with no remount. + + The runner's copy now goes through `@object-ui/i18n` instead of being hardcoded + English: a new `flowRunner` namespace (`title`, `submitting`, `saveAndContinue`, + `nextStep`, `completed`) in all ten packs, plus reuse of + `common.{loading,cancel,close,submit}` and `wizard.missingRequired`. The + server's own refusal sentence is still passed through untranslated — it is prose + the automation engine composed for a human, not copy with a key. +- 20e317c: Marketplace-less runtimes now say so instead of erroring: `OS_CLOUD_URL=off` is a + first-class disabled state, and the load-failure hint describes the control plane + the runtime was actually pointed at (objectui#5504). + + `apps/objectos-ee/deploy/.env.example` ships `OS_CLOUD_URL=off` as its factory + default, so a stock self-hosted stack has no marketplace at all. The Console still + recommended one: Home led with "Start with a template" and "Browse App + Marketplace", and the click landed on a red **Failed to load marketplace / Not + found** card whose hint claimed this runtime "points at the public ObjectStack + cloud by default" and advised setting `OS_CLOUD_URL`. Both claims were false for + exactly the deployment reading them — the operator had not left the default, and + the advice pointed back at the template that told them to set `off`. "Marketplace + disabled by configuration" is a configuration conclusion, not a load failure. + + - `isMarketplaceEnabled()` (`runtime-config`) reads the server's own + `features.marketplace`, which `RuntimeConfigPlugin` derives per request from the + serving app's route table (objectstack#8356). It is never inferred from the shape + of a failed request: a control plane that is merely DOWN leaves the flag `true`, + so an outage still renders as an outage. Unknown fails OPEN. + - The marketplace page renders an informational "App Marketplace is turned off" + state — muted, not `destructive` — and issues no request it knows will 404. + - Home's "Start with a template" cover greys out with a visible localized reason, + and the "Browse App Marketplace" shortcut is withheld, exactly as they already + are for the `manage_metadata` capability gate. + - `marketplace.load.failedHint` is replaced by `failedHintConfigured` (naming the + configured control plane) and `failedHintSameOrigin`. The "points at the public + cloud by default" sentence is gone: it was rendered unconditionally, including on + every runtime whose operator had overridden `OS_CLOUD_URL`. + + All ten locale packs carry the new keys. +- 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. +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. +- 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. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [e719ebd] +- Updated dependencies [fa429cf] +- Updated dependencies [8ebd57f] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [af3861f] +- Updated dependencies [f2158ec] +- Updated dependencies [6c6cee7] +- Updated dependencies [d1ab06f] +- 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..adcd3ea72 100644 --- a/packages/layout/CHANGELOG.md +++ b/packages/layout/CHANGELOG.md @@ -1,5 +1,101 @@ # @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 [b55a346] +- Updated dependencies [065bba7] +- 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 [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@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..016afd577 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,5 +1,30 @@ # @object-ui/mobile +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..db20084df 100644 --- a/packages/permissions/CHANGELOG.md +++ b/packages/permissions/CHANGELOG.md @@ -1,5 +1,34 @@ # @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 [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..fdfd5c192 100644 --- a/packages/plugin-ai/CHANGELOG.md +++ b/packages/plugin-ai/CHANGELOG.md @@ -1,5 +1,65 @@ # @object-ui/plugin-ai +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- 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 [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@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..c11dc4b6c 100644 --- a/packages/plugin-calendar/CHANGELOG.md +++ b/packages/plugin-calendar/CHANGELOG.md @@ -1,5 +1,159 @@ # @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. +- f47d94c: `@object-ui/plugin-calendar` now exports the `CalendarView` component's runtime event + type as **`CalendarViewEvent`**, and keeps `CalendarEvent` as a **`@deprecated` alias** + of it. **Non-breaking:** the alias is a working re-export denoting the same type, so + code importing `CalendarEvent` from this package keeps compiling unchanged — nothing is + removed and no behaviour changes. + + Why: `@object-ui/types` exports its own `CalendarEvent`, the AUTHORING event + (`id: string`, `start` / `end` accept ISO strings with `end` required, plus + `description`), while this package's was the runtime event (`id: string | number`, + `start: Date`, `end?: Date`). Neither is assignable to the other, and IDE auto-import + chose between the two identical names essentially at random — the wrong pick surfaced as + a remote `TS2322` about `Date` rather than as a wrong import, which is how this package's + own README example stayed uncompilable through an earlier import-path fix. The authoring + type keeps the canonical `CalendarEvent` name; the runtime type gets the self-describing + one (objectui#5044, following the `ObjectCalendarProps` -> `ObjectCalendarComponentProps` + rename in objectui#4650). + + Write `CalendarViewEvent` in new code. +- 065bba7: `CalendarViewSchema` (TS interface and zod mirror) converges on the registered + `calendar-view` renderer's measured read set (objectui#5667, maintainer ruling + option A — the renderer is authoritative). + + **Breaking for consumers of the published type** (deliberate; per-repo policy + breaking changes ship as `minor` — the fixed group's `major` tracks + `@objectstack`): + + - Nine inert keys are retired: `events` (the interface's only required key, + which the renderer deliberately drops — objectui#4433), `defaultView`, + `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + `onEventUpdate`, `onDateChange`. None had a read site on the authored-node + path and no measured app authors them (ADR-0049 enforce-or-remove). + - The type now declares what the renderer actually reads: `data`, `titleField`, + `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, + `currentDate`, `allowCreate`, `className`, plus the two host-only function + hatches it forwards (`onEventClick`, `onViewChange`). + - Practical radius, measured: `BaseSchema` carries an index signature and the + zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys + neither fail to compile nor get rejected at validation — they are simply no + longer declared, documented, or type-checked. The material accept change is + that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` + node without it now validates (previously the one key validation demanded + was the one key guaranteed to do nothing). + + Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README + and `content/docs/api/schema-reference.md` are repaired to the converged + surface in the same change, so no copy of the old contradiction survives. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..a80701d3e 100644 --- a/packages/plugin-charts/CHANGELOG.md +++ b/packages/plugin-charts/CHANGELOG.md @@ -1,5 +1,165 @@ # @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 + +- 894d103: `ObjectChart`'s wrapper div now carries `h-full`, keeping the height chain intact from a dashboard grid cell's declared height down to the element Recharts measures. Previously the chain died at the plain auto-height wrapper: `height: 100%` on the chart container computed to `auto`, Recharts measured a permanent zero, and only the `CHART_MIN_HEIGHT` floor (#5503) kept dashboard charts visible — at a fixed floor height instead of filling the cell (#5451). Under auto-height parents `h-full` resolves to `auto`, so non-dashboard hosts are unchanged. +- 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 [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..ad67df186 100644 --- a/packages/plugin-chatbot/CHANGELOG.md +++ b/packages/plugin-chatbot/CHANGELOG.md @@ -1,5 +1,107 @@ # @object-ui/plugin-chatbot +## 17.7.0 + +### Minor Changes + +- 6ce89da: The 确认修改 (confirm changes) card now carries a UI-owned terminal state after approval (#5695): `detectReplayOutcome` lifts the confirm-replay envelope (`replay_*` tool results) into 应用中 / 已生效 / 已暂存为草稿(含内联发布)/ 未生效(含 publishError 首行), rendered on the original card across the live, hydration/share, and localStorage-cache converters. A failed in-turn publish no longer rehydrates as an ordinary draft card with a live Publish button — the UI-rendered refusal is the layer a model cannot narrate over. New `console.ai.changesApplying/Applied/Drafted/Failed` keys in all ten locale packs. + +### 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`. +- ad404e0: Confirm-replay dispatch errors (bare `{error: …}` envelopes) now resolve the 确认修改 card instead of leaving it on 应用中 forever: `detectReplayOutcome` classifies them as a provisional failure, and a later successful authoring result in the same turn (the model self-repairing, e.g. after an `object not found` on a blueprint-local name) supersedes it via `detectAuthoringVerdict` — so the card never says 未生效 over a change that actually landed. Real publish failures (`publishFailed` envelopes) are never superseded. Measured live on the local rig, 2026-08-24. +- eddc1dd: The Studio copilot tells the agent WHAT the user is discussing (cloud#1610 send half): `ChatPane` accepts a `surfaceContext` and sends it as `context.surface` on every turn (the transport reads the body per send, so it stays fresh); the Studio copilot derives it from the URL alone — the `:tab` pillar segment plus the `?surface=type:name` deep-link the pillars already mirror, so the artifact carries its type discriminator (page/object/dashboard/report). A display chip above the composer (「正在讨论:…」, new `console.ai.discussing` key in all ten packs) makes the sent context visible instead of invisible grounding. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..9a814d023 100644 --- a/packages/plugin-dashboard/CHANGELOG.md +++ b/packages/plugin-dashboard/CHANGELOG.md @@ -1,5 +1,480 @@ # @object-ui/plugin-dashboard +## 17.7.0 + +### Minor Changes + +- 2e471a6: `dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshInterval` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title. +- 1d9100e: Dashboard `table` widget — a numeric **measure** column now sorts **descending + on the first click** (objectui#5845). + + The sortable headers shipped in objectui#5827 started every column ascending, + which left the card's own motivating complaint — the largest industry rendering + last — two clicks away. For a measure in an analytics widget the question a + click asks is "who is biggest", so that column now cycles + **descending → ascending → the dataset's own order**. + + Unchanged: a **dimension** column, and a measure whose values are not numbers + (a `min()`/`max()` over a text field), keep **ascending → descending → the + dataset's own order** — the idiom the console's own DataTable uses. The + first-click direction is decided by the same measure-and-all-values-are-numbers + classification that decides right-alignment, so it follows the column's *role* + rather than "it looks like digits": a digit-keyed dimension (a year, a quarter) + still starts ascending. + + Blanks still sort **last in both directions** — that arm is now what a measure + column's very first click runs — and `aria-sort` reports the direction actually + applied. +- 62c0fa1: Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering + fixes so it reads as a table rather than a raw dump (objectui#5827): + + - **Numeric columns right-align**, header and cells. `tabular-nums` was already + applied to every cell and could not do its job while the digits were flushed + left. A column qualifies when it is a MEASURE and every non-null value in it + is a number — the declared column `type` cannot decide it, because the + analytics executor stamps `type: 'number'` on every measure regardless of the + aggregate, so a `min()`/`max()` over a text field would have been + right-aligned on the strength of a label. Dimensions stay left-aligned even + when their bucket keys are digits: a grouping axis is not a quantity. + - **Sortable headers.** Each header is a keyboard-reachable button that cycles + ascending → descending → back to the dataset's own order, with the console's + existing sort indicator and `aria-sort` on the `th`. Blanks sort last in + BOTH directions. The sort is client-side over the rows already fetched — the + widget issues one `queryDataset` call and paginates nothing, so no round trip + is involved. + - **The empty-dimension bucket sorts last** in the default order instead of + floating to the top. It keeps its `—` label; only its position moves. + - **Row hover feedback on every row**, not only on a drillable one. A drillable + row keeps its stronger accent fill. + + The CSV export now follows the order the table is showing, which is the same + "the CSV is the table's data" convention its cell text already followed. + Drill-through is unaffected: rows are reordered as `(row, incoming index)` + pairs, so a drill still resolves through the server's parallel raw-value + sidecar. + + Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and + chart paths, and every non-dataset table in the console. +- e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it. + + `TableColumn` declares `header: string` and does not declare `label`. The + renderer's column normalization nonetheless read `header: col.header || col.label`, + so the same key had one spelling the type admits and one only the runtime did. + That alias is gone (objectui#5351), and the translation it used to perform happens + once at each producer instead: metadata vocabulary in, adapter vocabulary out. + + **This narrows what `data-table` accepts, so read this if you author `data-table` + nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a + directly authored `data-table` now renders a **headerless** column over live + cells. Spell it `header` — the key `TableColumn` has always declared. Columns + reaching `data-table` through `object-data-table`, `object-grid` or a related + list are unaffected: those producers resolve `header` for you from the spec's + `ListColumnSchema.label`, so every spelling they accepted before they still + accept. + + `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader + producers use to cross that boundary. It is adapter-first (`header` wins over + `label`), so an author who addressed the table directly is never overwritten. + + `object-data-table` also gains a fix from the same move: a column carrying a + `label` used to render a **blank** header there even while the alias existed, + because the widget's field-meta enrichment overwrote the authored `label` before + the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage". + + The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here + and still resolves. Retiring it is objectui#5120's remaining step, which is + gated on two published skill guides that teach that spelling. +- 9e725e0: Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846). + + The flat branch now requests the `[]` marginal grouping — the same server-side + totals machinery the cross-tab has used since framework#1753 — and renders the + answer under the console's existing `dashboard.total` label. Totals are computed + by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/ + `max` reports the whole-set aggregate); nothing is recombined client-side. The + row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null + bucket, and never joins the drillable rows. + + No footer is rendered when the executor answers no `[]` grouping, and none is + requested for a table truncated by `options.limit`, whose visible rows a + whole-set total could not be reconciled against. + +### Patch Changes + +- 3d79455: `plugin-dashboard`'s two private copies of the reference-bearing field family now + read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating + it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside + `computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the + already-converged consumers carry, so a member-identical private copy fails + rather than quietly re-forking the table. + + Two behaviour changes on the dashboard table's `$expand`, in opposite directions: + + - **A `tree` column is now expanded.** A self-referencing hierarchy field is + reference-bearing and a member of the shared family, so its cell renders the + parent record's display name instead of a bare id — the same treatment the form + and grid roads already gave it. + - **A `reference`-typed column is no longer expanded.** Measured before removing + it: `reference` is absent from `@objectstack/spec`'s closed `FieldType` + vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant + object schema can declare a field whose stored type is `reference`. Dropping it + is a no-op on real data; the spelling is a legacy dialect alias on the + action-param surface, folded to `lookup` before any field-type data is read. + + `EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license + widening a published shared set. +- cc5de5c: `DashboardRenderer` no longer emits an empty dashboard header wrapper. The + wrapper used to render whenever `header` was declared, while each of its + children — title, description, actions — was additionally suppressible. With + the console page chrome present (`hideHeaderText`, set because the chrome + already renders the dashboard's title and description) and no `header.actions`, + every child evaluated falsy and the DOM still received + `<div class="col-span-full mb-4"></div>`: zero children, yet a full grid row + (measured 64px) plus `mb-4` of dead band above the filter bar, on every console + dashboard page (objectui#5812, measured on HotCRM 17.1.0). + + The three children are now computed first and the wrapper renders only if one + of them survives. Nothing else changes: a standalone embed (no chrome) renders + title and description exactly as before, and declared `header.actions` keep the + wrapper alive even under the chrome, since the chrome renders text only. Authors + needed this — dropping `header` from the metadata to reclaim the pixels would + have cost the standalone embed its title, which is what `header` is for. +- 84d2e98: `DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained + (card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally + (objectui#4620). + + `isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the + `isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The + `datasetBound` side of that inner fork could never execute: reaching it required + `isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged — + the removed arm never ran, and the reachable fork in the Card branch (the one that gives + a dataset-bound metric its title and border chrome) is untouched. + + The cost was to readers, not to users: the shape read as "both branches handle + dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto + `DashboardGridLayout` had to pay for the reachability argument before it could decline to + copy the dead limb. A comment now names the invariant in place so the arm is not re-added. +- f24195a: `ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for + "no rows yet" instead of a fresh array literal per render (objectui#4629). + + Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so + whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind` + path that resolves to an object — the fallback produced a NEW array identity on + every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so + every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the + `isSystemField` pass, the `fieldLabel` lookups) and then discarded by the + `finalData.length === 0` early return. In `ObjectPivotTable` the value is handed + straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot + rebuilt its row/column sets, bucket map and totals on every render over no rows + at all. + + Nothing rendered wrong before or after; this is wasted work in the empty window, + plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is + the same module-scope frozen empty `data-table.tsx` adopted for its own + `EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings. +- 56f4e34: A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does. + + `ObjectDataTable` derives headers on two paths — from the author's declared + `columns`, and from the object schema when no columns were declared. The + declared path (and the static `data-table` half of the same widget family) + already used `humanizeFieldKey`, whose docstring names it "the single home for + the convention, because both halves of the `table` widget family need it and + they must agree". The auto-derived path carried a third, inline spelling that + split camelCase but never turned `_` into a space, so it left a raw underscore + on screen. Measured over the same object's columns: + + ``` + path close_date needs_analysis + object-bound, AUTO-DERIVED (before) Close_date Needs_analysis + object-bound, AUTO-DERIVED (after) Close Date Needs Analysis + object-bound, DECLARED columns Close Date Needs Analysis + static `data-table`, no columns Close Date Needs Analysis + ``` + + One dashboard can hold all three widgets over one object, so a single field key + rendered under two spellings — the defect class objectui#5425 rules out. The odd + path adopts the shared convention rather than the convention gaining a fourth + dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and + after — the coincidence that kept the snake_case divergence unnoticed), and a + translated header still wins: only the fallback handed to `fieldLabel` changed. + + Dimension MEMBER labels are untouched by this. The same card reported dashboard + members rendering a prettified enum instead of the picklist's translated label, + measured on 17.1.0; re-measured on this branch it no longer reproduces — the + analytics label net shipped in 17.5.0 routes every non-metric dataset dimension + through the field's declared options and the locale bundle. That behaviour had + no test stated in the card's terms and now has one, over the four dashboards the + card measured, including the property that a bar axis and a pivot header cannot + disagree about one stored value. +- 84d70bb: `ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel` + resolvers directly instead of holding them behind refs, so a pivot re-derives + its header and option labels when the resolver genuinely changes + (objectui#5625). + + The refs existed for a reason that no longer holds. `useSafeFieldLabel()` + returned a fresh object on every render outside an i18next provider, so a direct + dependency would have re-run the metadata-derivation effect on every render — + and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly + built objects, so each run scheduled the next one: an unbounded derive loop. + `ObjectPivotTable` worked around that locally with `fieldLabelRef` / + `fieldOptionLabelRef` plus a `useEffect` keeping them current. + `useObjectLabel`'s memo now holds with or without an i18next instance bound + (objectui#5564), so both resolvers have a stable identity on both paths and the + indirection buys nothing. + + It did cost something, and that is the user-visible half: a ref-hidden + dependency meant the derivation did NOT re-run when the resolver changed. A + pivot mounted before its `I18nProvider`, or rendered across a language switch, + kept showing its top-left header label and its select-option cell labels as + resolved by the old resolver — until some unrelated dependency (the data source, + the object name) happened to move. It now re-derives once on that transition and + renders in the active language. + + This is the same removal objectui#5587 made in `ObjectChart`, one package over. + + Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations + across forced re-renders both outside and inside a provider, checks that the two + derived state maps settle, and asserts the language-switch re-derivation. + Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the + no-provider case red (derivations in the dozens instead of 1), so the removal is + pinned to the fix that unlocked it rather than to a comment. +- f1c27f0: Dashboard record fields: percent columns now render through the one percent + scaling decision instead of a second, drifted copy of it. + + `renderFieldValue`'s `%`-format branch normalised the value itself before + calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`, + then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`, + which `@object-ui/core` documents as the single source of truth for percent + display scaling, so the branch was re-deciding what core owns — and its copy had + drifted from it in three measured ways: + + - `(value / 100) * 100` is not value-preserving in binary floating point, + re-introducing one call frame upstream the round trip that was removed from + inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000 + values change bit pattern and 1,108 rendered strings move, every one a + last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is + `1.61%`. + - A stored fraction below `0.01` was scaled twice — the local `* 100` put it + back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered + `50.00%`. + - The local test was `value > 1` rather than core's symmetric `|value| < 1`, so + a negative already in percentage points took the fraction arm: `-5` rendered + `-500.00%`. + + The branch now hands the raw stored value to `formatPercent` — the identical + call the list-view percent cell already makes for an ordinary percent column — + so a percent reads the same as a record field, as a grid cell and as a dashboard + measure. Output moves where it was wrong: values at or above 1 whose round trip + lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly + `1`, which is one percentage point by core's convention and now renders + `1.00%` at two decimals, where the local `value > 1` test had made it + `100.00%`. +- 0ccbdc1: `PivotTable` no longer re-runs its cross-tabulation memo on every render when it + has no rows (objectui#5562). + + The component spelled the empty array twice — as the destructuring default for + `schema.data` and as the `Array.isArray` fallback that keeps a provider-config + object out of iteration — so a schema declaring no `data` key, or one whose + `data` is a provider config rather than rows, produced a fresh array identity on + every render. That value is the first entry of the memo's dependency list, so + the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the + aggregated matrix and the row/column/grand totals on every render, over nothing. + Both spellings now resolve to one module-scope frozen empty, so "no rows" is a + stable value and the memo holds. + + Wasted work only: the churn feeds a memo rather than a `setState`, and + `PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no + render loop was possible. The identical fix landed for `data-table` in + objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the + direct-use path those two did not cover, where `DashboardRenderer` and + `DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the + chain. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect. + + `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module + scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`; + `@object-ui/app-shell` reaches the first of those through the static + `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has + no loader for `.css` at all, so all three resolve and then die during evaluation: + + ``` + TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css" + for .../react-grid-layout/css/styles.css + ``` + + Nothing about how these packages load has changed — every supported host bundles + them (Vite, webpack, or Next with the package in `transpilePackages`), and that is + still the only supported way to consume them. What changed is that the boundary is + now written where a consumer meets it, instead of being learned from a red import. + + objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying + plugin packages — permanently, over the three packages as a group — rather than + moving the stylesheet imports out of module scope. No unbundled-Node consumer + exists, and buying permanent machinery to close a capability gap nobody is pulling + on was the trade the ruling declined. A real consumer request reopens it as a + design question, not as a defect: the READMEs say so and name the issue. +- fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales. + + The dashboard widget config panel carried a private `resolveLabel` documented as + resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference + form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline + per-locale map `I18nLabelSchema` actually admits has neither limb, so + `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy + of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`, + `MetricWidget` and `MetricCard`. + + This was not a display bug. The resolved value seeds the panel's editable draft, + so a widget whose stored title was a map opened with an **empty** Title field and + the next save wrote `''` over the author's map — on the ordinary path, not an + exotic one: open the widget, change anything, save. + + Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301: + + - **Reading** goes through `pickLocalized(value, language)`, so the panel shows + the active locale like every sibling surface post-objectui#4032. + - **Writing** replaces only the active locale's entry and carries every other + locale across. A title the author never touched round-trips the stored object + itself through an unrelated config edit; an edited one merges into the entry + that was displayed. The live-update callback (`onFieldChange`) forwards the + merged map for the same reason — hosts feed it back into the widget the panel + re-opens from, so a bare string there dropped the map before a save ever ran. + + `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side + inverse of `pickLocalized`, so the rule is stated once instead of re-derived per + panel. It follows `pickLocalized`'s first three limbs — exact tag, base language, + region-qualified sibling — and deliberately stops there: the `default` / `en` / + first-value limbs are display fallbacks that hand back *another* locale's string, + and writing to one would let an author editing in `fr` overwrite English. With no + entry for the active locale the edit adds one. The pairing + `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a + write that lands where the read does not look is how a "saved" string disappears. + + A full multi-locale editing UI remains out of scope (objectui#4163). +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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 d3ec891f0..3b377845f 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..2a140277b 100644 --- a/packages/plugin-designer/CHANGELOG.md +++ b/packages/plugin-designer/CHANGELOG.md @@ -1,5 +1,138 @@ # @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 [b2e85a9] +- Updated dependencies [c7cd2b6] +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- 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 [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- 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/components@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..be0e33c7c 100644 --- a/packages/plugin-detail/CHANGELOG.md +++ b/packages/plugin-detail/CHANGELOG.md @@ -1,5 +1,327 @@ # @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 `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- 8a44390: `record:alert` binds the row through `usePredicateRecordContext`, so an + author-declared `properties.visible` is actually consulted. + + `renderers/record-alert.tsx` was the last predicate face in the repo still + handing `useCondition` a root-only `{ record }` bag. Every other row-scoped + predicate — the four generic action renderers (objectui#4075) and app-shell's + `DeclaredActionsBar` (objectui#4077) — binds the row through the shared + `usePredicateRecordContext(record)` helper, which resolves the three spellings + objectui#5330 ruled on: canonical `record.status`, the deprecated row-action + shorthand `status`, and deprecated legacy `data.status`. + + Under the root-only bag only the canonical spelling worked, and the two others + failed in **opposite** directions — both of them silently, because this call + site is fail-soft: + + - **row-action shorthand** (`status == 'x'`) resolved nothing, so the evaluator + threw. The legacy `${…}` path answers a throw with its own source text, a + non-empty and therefore truthy string, so the verdict was **SHOWN on every + row**. A banner the author had gated was permanently on screen. + - **legacy `data.*`** (`data.status == 'x'`) did not throw at all. App-shell's + ambient predicate scope (`providers/ExpressionProvider.tsx`) carries + `data: {}`, so the predicate read that object instead of the row, compared + `undefined`, and the verdict was a constant false — **never shown**. + + **Behaviour change, stated plainly:** a shipped `record:alert` whose `visible` + was written in either deprecated spelling was inert and is now live. A banner + that was permanently visible may begin to hide, and one that never appeared may + begin to show — that is the point of the fix, but it is a verdict change rather + than a no-op. Canonical `record.*` predicates are unaffected in verdict: they + resolved before and resolve now, pinned on both polarities. An in-tree census + found no `record:alert` `visible` predicate outside this package's own tests. + + A node-level `visibleWhen` is a separate gate one tier up in `SchemaRenderer`, + with its own deliberate bindings (`data` is the data-source adapter there, not + the row). This change does not touch it; the two still compose as AND. + + The renderer's header comment described the shared-scope behaviour it did not + have. It now describes what the file does, including the fail-soft policy and + the two-gate composition. +- 23705b7: **`ReferenceRailEntry` is now the spec's type, and the reference-rail `icon` key retires** (objectui#5494, maintainer ruling 2026-08-22). + + `ReferenceRailEntry` is owned by `@objectstack/spec` as of 17.1.0. The hand-written interface in `record-reference-rail.tsx` is replaced by a re-export of the spec's `ReferenceRailEntry` (derived from `ReferenceRailEntrySchema`, `$strict` over `{ objectName, relationshipField, title?, limit?, displayField? }`), and `buildDefaultPageSchema` no longer emits `icon` on the reference-rail entries it synthesizes. + + **Migration — if you write `icon` on `record:reference_rail` entries, remove it.** Be aware of what this does and does not change: + + - **Runtime validation does not move.** The spec's strict schema already refused `icon` at save — that refusal is unchanged. What was broken was objectui's declaration: the TS type advertised a key that could never be saved. + - **Nothing ever rendered `icon`.** No render path in `RecordReferenceRailRenderer` has ever read the key (measured back to the file's first commit, and independently by the spec's `ui-reference-rail-unknown-keys-refused` migration entry). An authored `icon` that survived in unsaved/preview metadata was already a silent no-op — after this change the TS type says so instead of suggesting otherwise. + - **The published TS surface narrows.** Code that imported `ReferenceRailEntry` from `@object-ui/plugin-detail` internals and set `icon` will now get a type error. That error is the contract speaking: remove the key. + + The reference-rail icon affordance is retired rather than proposed upstream: a stock scan of this repo, its examples and schema-catalog corpus, and the objectstack tree (examples, templates, docs) found no reachable authored usage of `icon` on reference-rail entries. Stored customer metadata is not reachable from this seat and was not scanned. + +### Patch Changes + +- d8cf1cb: `record:activity` no longer discards scheduled activities. `sys_activity` rows with + `type: "scheduled"` now map to the `event` feed kind instead of being dropped before any + filter runs, so a not-yet-held meeting reaches the timeline and `types: ['event']` stops + being a permanently empty tab. Held meetings are unchanged: `completed` still maps to + `task` and still hides unless `showCompleted` is set, which is what keeps an upcoming + meeting visible by default while a finished one is not. + + The unknown-type default is unchanged and deliberately so — a row whose type nothing maps + is still dropped rather than bucketed into `system`, because rendering an unmeasured kind + as something it is not would be new wrong data rather than recovered data. What changes is + that the drop is no longer silent: an unmapped type now logs one `console.warn` naming it, + once per type. Types the map knows and deliberately excludes (`commented`, `mentioned`, + `login`, `logout`) stay silent, since those are decisions rather than gaps. +- 0068348: `record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is + widened to `string | I18nLabel` in both copies (`properties.*` and the flat + compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. + + The renderer already resolves `action.label` through the same inline-locale-map + `pickLocalized` call as `title` / `body` (`const ctaLabel = + pickLocalized(props.action?.label, language)`), so a bare `string` declaration + was narrower than the renderer's own runtime behavior — the same + declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for + `title` / `body` one level up in the same interface (objectui#4998). + + Type-only: the block's published authoring surface still declares `action` as + a bare `object` with the member shape in prose only + (`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — + that half stays parked on the `ComponentInput` member-shape question (PR + #3795) and is out of scope here. +- 641543f: `record:alert`'s `visible` predicate no longer logs a spurious `record is not + defined` evaluation-failure warning during the record-detail loading frame. + + The predicate is evaluated on every render (Rules of Hooks), including the + frame before `useRecordContext().data` has loaded. In that frame `record` is + unset, `usePredicateRecordContext` binds an empty context bag by design (no + `record` key at all), and a bare/`${…}` predicate referencing `record.*` + faulted with `record is not defined` — logged via `console.warn` on every + page load, even the correct, working ones, because the SAME predicate + resolved fine one frame later once `record` populated. The banner's own + visibility was never wrong (it already hides unconditionally while unloaded); + only the diagnostic was permanently misleading on the happy path. + + The predicate is now skipped while `record` hasn't loaded — the same + condition the banner's own "hide while unloaded" early return already used — + since its verdict in that frame was never consulted anyway. A predicate that + is genuinely broken (bad field, bad syntax) still faults, and still logs, on + every frame once `record` is populated (objectui#5776). +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin + the names against the runtime `icons` record so the next lucide bump goes red instead of + silently blanking a glyph (objectui#5622). + + lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it + as a deprecated named export. A retired name therefore still imports, still type-checks, + and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it + is used as a STRING, because every string lookup here reads that record. Nothing goes red + either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at + implementation time. + + What a user sees change: + + - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon + again. Its items become an `action:bar` schema whose renderers resolve `icon` through + `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been + drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged. + - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in + both the registration `icon` and the `name` input's `defaultValue`) resolves again: the + designer palette entry's glyph was blank, and an `icon` dropped from that palette + rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette + looks exactly as it did. + - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn` + (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The + gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher + landed in objectui#5586, so one view type no longer draws two different icons depending + on which switcher is on screen. + + Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and + alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather + than resolvability, because every retired spelling repaired here is the SAME component + object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`, + `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`, + `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the + export, would pass on the broken name. That is the blindness that let this ship. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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 37209297f..c0909d813 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..b87f09c4a 100644 --- a/packages/plugin-editor/CHANGELOG.md +++ b/packages/plugin-editor/CHANGELOG.md @@ -1,5 +1,65 @@ # @object-ui/plugin-editor +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- 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 [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@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..6ce4d4f82 100644 --- a/packages/plugin-form/CHANGELOG.md +++ b/packages/plugin-form/CHANGELOG.md @@ -1,5 +1,165 @@ # @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 `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 83ec618: `README.md`'s "Not a `FormField` key" table said a field-level `className` is + "read on exactly one pseudo-field, `type: 'section-divider'`". That quantifier + holds only for the renderer's *explicit* read — `className={fp.className}` on + the `section-divider` branch of + `packages/components/src/renderers/form/form.tsx`. The same renderer forwards + every key it did not destructure, and `className` is not among the names taken + off the field config, not among the ones `stripRendererOnlyProps` removes, and + so rides `{...fieldProps}` into `renderFieldComponent`, whose built-in `input` + branch spreads it onto `<Input>`. A field-level `className` therefore lands + visibly on ordinary built-in controls, and a reader taking "exactly one" + literally concludes the opposite of what the code does (objectui#5131). + + The cell now describes the contract rather than the reader count: an undeclared + key still rides the props spread down to whichever component the field resolves + to, nothing in the contract promises that, and a registered widget honours it + only if it happens to spread its leftover props — the wording the docs site + already ships, so the two sources agree again. The advice in the row is + unchanged and was never wrong (`span` / `colSpan` for width, + `FormSchema.fieldContainerClass` for the grid), and the explicit + `section-divider` read is kept, now named as explicit. + + This is a documentation fix to a file `plugin-form` publishes to npm, which is + why it carries a version: the npm landing page only picks up the correction on a + release. No behaviour, export, type, or `dist` byte changes. +- 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 [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..0b7c1e445 100644 --- a/packages/plugin-gantt/CHANGELOG.md +++ b/packages/plugin-gantt/CHANGELOG.md @@ -1,5 +1,114 @@ # @object-ui/plugin-gantt +## 17.7.0 + +### Minor Changes + +- 199d31b: **`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both + gantt renderer branches honour it** (objectui#5074, maintainer ruling + 2026-08-19: declare-and-wire; the spec half landed first upstream). + + - `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, + DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` + enum by reference, so the member list cannot drift. Deliberately no + default: an omitted `viewMode` keeps letting a persisted layout + (`persistLayoutKey`) seed the timeline granularity before the renderer's + `'day'` fallback. + - The timeline branch (`GanttView`) now receives an authored `viewMode`. + Previously only the resource-workload branch (`resourceView` + + `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt + view was silently ignored. + - The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both + branches read the declared `ganttConfig.viewMode`, which also honours the + key when authored inside the spec's `gantt` config block. + - Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value + (e.g. `viewMode: 'hour'`) becomes a zod validation error where it + previously passed through unvalidated. Values on the published spec enum + are unaffected. + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..730cd49d5 100644 --- a/packages/plugin-grid/CHANGELOG.md +++ b/packages/plugin-grid/CHANGELOG.md @@ -1,5 +1,219 @@ # @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 `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. + +### Patch Changes + +- 9e22085: `ObjectGrid` lowers the deprecated `defaultFilters` through `toFilterNode` instead of + byte-copying it onto `$filter` (objectui#4082). + + The query assembly already lowered the canonical `filter` key through `toFilterNode` — + the repo's single "last hop before the wire" (objectui#4041) — while the legacy branch + beside it assigned `params.$filter = schema.defaultFilters` verbatim. That made this the + one leg on the chain reaching the wire unlowered: `plugin-list`'s `buildEffectiveFilter` + and `plugin-view`'s non-grid fetch both already route the same value through + `toFilterNode` / `mergeFilterNodes`. + + Byte-copying is refused on the wire for both shapes the slot carries. `defaultFilters` is + declared `Record<string, any>` (the MongoDB-style shape) and `isFilterAST` is false for a + plain object; an array of `ViewFilterRule` objects fails the same predicate. Either one + answers `400 INVALID_FILTER` — measured against a real backend in objectui#3431. + + `toFilterNode` handles both without new logic: objects route through + `convertFiltersToAST`, rule arrays lower element-wise, and an AST already in the slot + passes through untouched, so nothing is lowered twice. It also folds an absent or empty + source to `undefined`, which is why the truthiness guard is gone — `defaultFilters: {}` + used to send `$filter: {}`, asking the server a question with no content in a shape it + refuses; now `$filter` is omitted, matching the canonical key's documented behaviour. + + **Grade — this is less dormant than the card assumed.** objectui#4082 was filed + observation-class on "no measured producer", reasoning that `defaultFilters` is not in + `object-grid`'s registered `inputs` so an author writing it only draws a save-gate + warning. That reasoning covers authors, but not the framework: `plugin-view`'s + `ObjectView` writes the slot itself, forwarding an active named view's `filter` as + `defaultFilters: viewFilter || schema.table?.defaultFilters` in its `gridSchema` memo — + and `plugin-view`'s own README documents `listViews.<name>.filter` as + `[{ field, operator, value }, …]`, the exact shape objectui#3431 measured as + `400 INVALID_FILTER`. The registered `object-view` / `view` renderer passes no + `renderListView`, so that path falls through to `ObjectGrid` rather than to `ListView`, + and `ListView`'s lowering does not cover it. So a schema-registration host — the + documented authoring path — reached the raw assignment whenever an active named view + carried a filter. Not asserted here: a failing request captured against a running + deployment. `app-shell` is unaffected either way; it supplies `renderListView` and + delegates to `ListView`, which lowers. + + Not in scope, and deliberately not done: retiring `defaultFilters`. This is + consumer-side only — the key the schema admits is unchanged, and its precedence behind + the canonical `filter` is unchanged. + + The sibling legacy `defaultSort` leg was graded and needs no change; see the PR for the + measurements. +- c574dfb: `ObjectGrid` says which column it dropped, instead of rendering a header-only grid in silence. + + objectui#5068 retired the undeclared `accessorKey` / `header` tolerance branch, so + `ListColumnSchema`'s `field` / `label` is now the only column spelling the renderer + reads. That was right — the spec refuses `accessorKey` and `header` by name, and the + census found zero authored usages. But it relocated a failure mode instead of removing + it: a column authored in a spelling the renderer does not read contributed nothing, and + nothing said so. No error, no warning, no empty state — the author got a grid with its + row-number column and no data columns, which is a success receipt for a disagreement + between the renderer and the author. + + An authored column that can never resolve now emits one `console.warn` naming the + address rather than the symptom: which block (`object-grid` or the `view:grid` alias), + which object and label, which `columns[i]`, the keys that entry actually carries, and the + rewrite that works — for a column authored `{ accessorKey: 'amount', header: 'Amount' }` + the message spells out `{ field: 'amount', label: 'Amount' }`. It reuses the channel `ObjectGrid` already had for "you declared it, the renderer dropped + it" (the export-format warning), rather than adding a second differently-shaped one. + + Rendering is unchanged in every case: this is additive. The diagnostic reads the + `columns` input and nothing else — it never asks whether the grid found rows, because + `object-grid` legitimately draws them from five different places (a bare `data` array, + `data.provider: 'value'`, legacy `staticData`, `bind`, or a host that owns the fetch and + passes the window down as a `data` React prop, which is what `plugin-list`'s `ListView` + does). All five are pinned by test, in both directions. A `hidden: true` column is + authored intent and is never reported, and so are the arms that legitimately produce no + columns of their own: no `columns` key, an empty `columns` array, and the `string[]` + spelling. + + A throw was rejected: a grid that renders nothing today would become a page that renders + nothing. +- 02f48b6: `object-grid` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it. + + `userActions` names two different blocks. On a **view** it is toolbar policy — + the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`, + `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by + name. On an **object** it is the CRUD-predicate block (`edit` / `delete` / + `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is + the only shape `listViewPredicates` can read, since its loop skips every + non-object value. + + `ObjectGrid` read the key view-first when building the `$select` projection + (`(schema as any).userActions ?? resolvedSchema.userActions`). A view carrying a + perfectly legal toolbar block therefore shadowed the object's CRUD predicates, + the harvest found none, and the predicate's operand left the projection. CEL then + faults on the absent key, fails closed, and the row Edit/Delete button disappears + for everyone with nothing pointing at the projection — objectui#3501's failure, + reached with a success receipt at every step. + + The view-level block is not hypothetical: `SpecBridge.transformListView` copies + it onto the `object-grid` node the renderer receives, and `app-shell`'s + `ObjectView` builds one unconditionally. + + The harvest now reads the resolved object block only. Both `userActions` read + sites carry a comment naming the collision, and + `__tests__/gridNonAuthorKeys.test.tsx` pins each clause of it: the two shapes, + the producer that writes the view one, the harvest's blindness to it, and the + projection that must keep the object's operand with a toolbar block present. + + Toolbar policy itself is untouched — it was never read through this path. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..25d3b1842 100644 --- a/packages/plugin-kanban/CHANGELOG.md +++ b/packages/plugin-kanban/CHANGELOG.md @@ -1,5 +1,90 @@ # @object-ui/plugin-kanban +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [d8cf1cb] +- Updated dependencies [0068348] +- Updated dependencies [641543f] +- Updated dependencies [8a44390] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [23705b7] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..8ad9f80c6 100644 --- a/packages/plugin-list/CHANGELOG.md +++ b/packages/plugin-list/CHANGELOG.md @@ -1,5 +1,284 @@ # @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 [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [a865c73] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..ba21b391f 100644 --- a/packages/plugin-map/CHANGELOG.md +++ b/packages/plugin-map/CHANGELOG.md @@ -1,5 +1,137 @@ # @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 [b55a346] +- Updated dependencies [065bba7] +- 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 [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@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..2a247edd9 100644 --- a/packages/plugin-markdown/CHANGELOG.md +++ b/packages/plugin-markdown/CHANGELOG.md @@ -1,5 +1,65 @@ # @object-ui/plugin-markdown +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- 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 [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@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..c5f686d3d 100644 --- a/packages/plugin-report/CHANGELOG.md +++ b/packages/plugin-report/CHANGELOG.md @@ -1,5 +1,88 @@ # @object-ui/plugin-report +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [9a1fb41] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- 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 [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [7a28e1e] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [4bb940b] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [f1690d4] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..3339483a7 100644 --- a/packages/plugin-timeline/CHANGELOG.md +++ b/packages/plugin-timeline/CHANGELOG.md @@ -1,5 +1,79 @@ # @object-ui/plugin-timeline +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..7a27e4203 100644 --- a/packages/plugin-tree/CHANGELOG.md +++ b/packages/plugin-tree/CHANGELOG.md @@ -1,5 +1,78 @@ # @object-ui/plugin-tree +## 17.7.0 + +### Patch Changes + +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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/components@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..8ad3fc284 100644 --- a/packages/plugin-view/CHANGELOG.md +++ b/packages/plugin-view/CHANGELOG.md @@ -1,5 +1,248 @@ # @object-ui/plugin-view +## 17.7.0 + +### Minor Changes + +- 4ef29f0: Remove the inert `showRefresh` designer input from the `object-view` registration (objectui#5567). + + The `object-view` designer no longer offers a "Show Refresh Button" toggle, and the registration no longer defaults `showRefresh: true`. The key was declared, documented, and defaulted, but `ObjectView` never read it — an author who wrote `showRefresh: false` on an `object-view` node always got a no-op. **Behaviour is unchanged for every existing app**, because nothing ever consumed the key. + + Migration: nothing to do. If you wrote `showRefresh` on an `object-view` node, the key simply disappears from the designer's property panel; it never controlled anything. The live refresh channel is `userActions.refresh` (rendered by the list toolbar in `@object-ui/plugin-list`), which is unaffected. `showRefresh` on other surfaces (e.g. `CRUDToolbar`) is also unaffected. + + `@object-ui/app-shell` only drops its two producer writes of the dead key (the app `ObjectView` wrapper and the metadata-admin `ViewPreview`) — no user-visible change. +- 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + + The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + + **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + + The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + + `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + + **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. + +### Patch Changes + +- cad512f: A host-composed `tree` view is now labelled with the tree icon in `ObjectView`'s + view switcher instead of the grid one, and the `tree` / `chart` view types are + recorded as host-composition-only surfaces (objectui#5321). + + `viewSwitcherSchema`'s `iconMap` carried an entry for every view type except + `tree`, so a tree view fell through to the `|| 'table'` fallback and was drawn + with the grid glyph. objectui#2916 fixed exactly this once, for `chart`, by + adding a single key — nothing recorded that the map had to be COMPLETE, so the + next missing member went unnoticed. The map is now typed + `Record<ViewType, string>`, which is how `ViewSwitcher`'s own + `DEFAULT_VIEW_ICONS` (the consumer of these strings) has always been declared: + a future `ViewType` member fails `type-check` rather than silently rendering as + a grid. The `tree` value is `'list-tree'`, the same `ListTree` glyph + `DEFAULT_VIEW_ICONS` already names for this view type, and the runtime fallback + stays for host props that carry an unrecognised type. Reached in practice by + the console, whose `CreateViewDialog` offers `tree` among the view types a user + can create. + + No authoring surface changes. `generateViewSchema` renders eight view types + while `ObjectViewSchema.defaultViewType` and `NamedListView.type` admit six of + them, so `tree` and `chart` are selectable only through the component's `views` + prop. The maintainer ruled on 2026-08-20 that both stay recorded as + host-composition-only rather than being added to those unions, following the + objectui#5097 precedent; the record now lives beside that one, with the branch + set derived from a source fence, the authored unions pinned at the type level, + and host reachability measured. +- cfc2c7a: **Bug fix:** a non-grid `object-view` (calendar / kanban / gallery / timeline) + whose sort came from the deprecated `table.defaultSort` no longer comes back + empty. `ObjectGridSchema.defaultSort` is declared a single `{ field, order }` + object, and `ObjectView`'s own fetch handed it to `$orderby` verbatim — where + the ObjectStack adapter reads it as an `$orderby` map and folds it with + `Object.entries`, so the request went out sorting by two columns literally + named `field` and `order`. The server answers `400 INVALID_SORT`, `ObjectView` + swallows the error, and the view rendered with no records — while the *same* + metadata sorted correctly as a grid, because `ObjectGrid` lowers that pair + before using it (objectui#4869, maintainer ruling 2026-08-22). + + `ObjectView` now performs the same legacy-to-canonical lowering `ObjectGrid` + already does (`sort ?? (defaultSort ? [defaultSort] : undefined)`) and routes + the whole chain — named view sort, `views` prop sort, `table.sort`, + `table.defaultSort` — through the shared `convertSortToQueryParams` sink. This + was the last object-bound read site sending an authored sort to `$orderby` + unlowered; every other block (gantt, map, calendar, timeline, + `record:line_items`) already did, so an adapter that implements `find` itself + now receives one normalized `Record<field, direction>` from all of them instead + of two different dialects. + + Precedence is unchanged, and both spellings of the pair keep working. The + shared sink was deliberately **not** widened to accept a bare `{ field, order }`: + its input slot also legitimately carries `$orderby`'s own map, in which + `{ field: 'desc' }` is a valid ordering by a column named `field`, so widening + it would make one shared function guess. + + Two visible shape changes on the wire, both semantically identical to before: a + string `table.sort` such as `'name desc'` now serializes as `-name` rather than + riding through as `name desc`, and a `SortConfig[]` arrives as a map rather than + as an array. +- 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every + field-type predicate in the renderer, not just by the widget road + (objectui#4914, maintainer ruling B of 2026-08-18). + + `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs + ahead of six predicate faces that previously granted a retired spelling + first-class treatment: the filter builder's operator buckets and its value + control (`@object-ui/components`), the detail page's highlight-strip picker + (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`), + the dashboard's `$expand` whitelist and `isLookupType` + (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter + control (`@object-ui/plugin-list`). Each one now fires the migration + prescription on the console — once per spelling across all of them, never once + per predicate — and then answers as it would for a spelling it does not + recognise. + + This closes the whole CLASS rather than one word: the gate is quantified over + `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the + day it lands. It is the shape objectui#4932 and objectui#4942 already + established for the form and inline-edit roads. + + Measured before the change, and the reason the fix is a gate rather than a + deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')` + equalled the `user` bucket item for item, `computeLookupExpand` actively + requested `$expand` for it, `isLookupType('owner')` was `true` alongside + `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as + `picklist` does. Deleting the members alone would have traded a visible + contradiction for a SILENT degradation — a filter picker collapsing to a bare id + box, `$expand` quietly stopping so cells show raw foreign-key ids — which is + verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent. + The gate keeps that fallback and adds the half that was missing: the author is + told. + + The boundary question is answered on record: `owner` arriving through a + backend-vocabulary normalizer is an authoring error to refuse loudly, not + legitimate foreign input to tolerate. The open backend vocabulary those + normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`, + `datetime_tz` and the rest are equally absent from the spec's closed `FieldType` + and are equally unretired, so they classify exactly as before. + + `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports` + move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that + package's published surface is unchanged apart from the newly ruled gate. + `@object-ui/components` is a consumer of the gate and `@object-ui/fields` + depends on it, so a single shared table could not live in `fields` — and a + second copy would have meant a second dedupe set and two console lines for one + spelling. No package gained a new dependency. + + A retired spelling never loses a stored value: `retypeFilterValue` is + deliberately not gated, and the refused filter row stays operable rather than + drawing a blank operator trigger. +- 6b348d2: `ViewSwitcher` draws an icon for `chart` and `gantt` views again, and both + icon maps in the package now name only spellings lucide still resolves + (objectui#5586). + + `ViewSwitcher.resolveIcon` turns an icon NAME into a component by looking it up + in lucide's runtime `icons` record. lucide retires a spelling by dropping it + from that record while KEEPING it as a deprecated named export, so a retired + name still imports, still type-checks and still renders as a component — and + silently resolves to nothing as a string. `ObjectView` composes the switcher + from names, and two of them had been retired on lucide-react 1.31.0: + `chart: 'bar-chart-3'` and `gantt: 'gantt-chart'`. Both view types rendered as a + label with no icon at all while every sibling type had one, and nothing went red + because no lucide symbol appears in that map for the compiler to check. Measured + against the installed package: `BarChart3` and `GanttChart` are absent from + `icons`, while `ChartColumn` and `ChartGantt` are present. + + - `ObjectView`'s `iconMap`: `bar-chart-3` → `chart-column`, + `gantt-chart` → `chart-gantt`. + - `ViewSwitcher`'s `DEFAULT_VIEW_ICONS`: the adjacent entries that named + deprecated aliases move to the names the record carries — + `BarChart3` → `ChartColumn`, `GanttChartSquare` → `ChartGantt`, + `Grid` → `Grid3x3`. `ChartColumn`/`Grid3x3` are the same components the + aliases already pointed at, so those two glyphs are unchanged; the `gantt` + default picks up the plain gantt glyph, which is what `iconMap` now supplies + for that view type. + + The regression pin widens from `tree` alone to EVERY name both maps supply: a + pin scoped to the two names that broke would not have caught this and would not + catch the next lucide bump. +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [3c9fca3] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- 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 [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [20e317c] +- Updated dependencies [9850c6e] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [83ec618] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [26a2238] +- 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/components@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..439428799 100644 --- a/packages/providers/CHANGELOG.md +++ b/packages/providers/CHANGELOG.md @@ -1,5 +1,47 @@ # @object-ui/providers — Changelog +## 17.7.0 + +### Minor Changes + +- c9327c9: Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. + + Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): + + - `Typography` — the shape lives on as the inline `Theme['typography']` member. + - `BorderRadius` — lives on as inline `Theme['borderRadius']`. + - `Shadow` — lives on as inline `Theme['shadows']`. + - `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. + + Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). + + The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. + + `@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [0e05aac] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [199d31b] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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 (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. + ## 17.6.0 ## 17.5.0 diff --git a/packages/react-runtime/package.json b/packages/react-runtime/package.json index 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..a503091a3 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,279 @@ # @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 `<DetailView dataSource={…} />` callers are untouched), + `object-form` gains a prop form it did not have, and `object-grid` no longer + throws `useSchemaContext must be used within a SchemaRendererProvider` when a + page has no provider. + + And the silence is over. A block in this family that resolves no adapter renders + a **No data source resolved** panel naming the block, the object it was about to + read, and the ancestor that injects the adapter — instead of a header-only grid, + a field-less form card, or nothing at all. The check is opt-in per block, so a + placement with inline rows, inline `customFields`, an inline record or an `api` + endpoint is untouched. + + New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`, + `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`. +- d1ab06f: Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now + warn once, and are unchanged otherwise. + + A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row + scope, a `record:alert`) has bound the row three ways since objectui#4075 — + `record.status`, bare `status`, and `data.status` — without any of them being + declared the contract. The maintainer ruled that question on 2026-08-20 + (objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for + the identical renderer-tolerance shape: **the canon is `record.*`**, and the + other two enter a deprecation window. + + The canon states the **server's** accept set, which was this card's first + measurement and turns out to be strictly narrower than the renderer's. Measured + against `@objectstack/formula@17.1.0`, the engine the server evaluates with: + + | spelling | server runtime | server authoring oracle | + |---|---|---| + | `record.status` | `{ ok: true, value: true }` | accepted | + | bare `status` | `Unknown variable: status` | refused | + | `data.status` | `Unknown variable: data` | **silently accepted** | + + `buildScope({ record })` mounts exactly `['record']` — `data` is never bound and + the row's fields are never flattened to top level. The three-way binding is a + client tolerance with no server counterpart, which is why the warning belongs on + this side. + + `data.*` is the dangerous one, and the reason the warning exists. `data` is in + `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle + waves it through — that list is a deliberately generous "never faults" lint + baseline, not the runtime accept set. A `data.*` row predicate therefore passes + every authoring gate the platform has and then binds nothing at runtime: not an + error, a constant `false`. A `visible` that is constantly false is a button that + silently never appears — the objectui#4075 fail-closed signature. + + What ships: + + - `@object-ui/core` exports `detectNonCanonicalRowSpelling`, + `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and + `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`, + which carries the canon statement and the measurement. + - Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and + `useCondition` (react, for bags bound by `usePredicateRecordContext`). + - Detection reuses the server's own oracles (`collectCelRootIdentifiers`, + `firstUndeclaredReference`) rather than a regex, so no second dialect + judgement is invented client-side. + + **No spelling is removed and no behaviour changes.** Every predicate that + resolved before resolves now — the ruling defers removal behind a stored-metadata + survey, and the warning is what makes that survey possible (ADR-0078: a + tolerance nothing ever reports can never be retired). + + The deprecation is scoped to the **runtime record layer**. `data` remains the + canonical root one layer over, in a metadata-editing form (ADR-0089 D3 + `CANONICAL_ROOT_BY_LAYER`), and the detector stands down there. +- c86185e: Bind `record` into the node-level visibility evaluator, and stop a hoisted + `properties.visible` swallowing a declared `visibleWhen`. + + `@objectstack/spec` has declared since ADR-0089 that a page component's + `visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`, + `current_user`, `page.<var>`"*. `SchemaRenderer` bound no `record` at all. Its + evaluator was built from the ambient predicate scope, `data: dataSource` (the + connector **adapter**, not the row) and `page: pageVariables`; the row lives in + `RecordContext`, which that evaluator never read. + + Because the surface is fail-soft, a `record.*` predicate did not misfire — it + resolved to **shown**. Both polarities of the same predicate returned the same + verdict, so a visibility gate silently did not gate, on every block on every + record page. Measured on `record:alert`, `record:path`, `page:card` and + `element:text`. + + Three changes, all in `SchemaRenderer`'s evaluation memo: + + - **`record` is bound**, as the `record` root only — the three roots the + describe promises and nothing more. Not as bare fields, and never over + `data`, which is what `${data.*}` in a props bag resolves against. Bound + conditionally, so "no row" binds nothing rather than shadowing a `record` a + host supplied through the ambient scope. + - **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*` + onto the node, so a node carrying `properties.visible` short-circuited the + declared node predicate — the one key the spec tells authors to write was the + one key that could be silently ignored. The two deprecated aliases + (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into + `visibleWhen` at parse, so a spec-parsed page never reaches them. + - **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this + predicate is broken" and "this predicate said yes" with the same word. The + verdict is unchanged on every path — `evaluateCondition` already returned + `true` for every unevaluable predicate, including the non-negated `hidden` / + `hiddenOn` legs where that `true` means HIDE — so only the silence moved. + + **Behaviour change, stated plainly:** a shipped page whose node-level + `record.*` predicate was previously inert now evaluates. A block that was + permanently visible may begin to hide — which is the point, but it is a verdict + change, not a no-op. `properties.visible` is unaffected in verdict: an + in-tree census found **zero** node-level `record.*` predicates on page + components, so nothing in this repository changes verdict. + +### Patch Changes + +- 9850c6e: `SchemaRenderer`'s node visibility gate now emits a dev-only warning when a + predicate reads `data.*` and the data-source adapter cannot answer that read, so + an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the + constant they authored instead of shipping it (objectui#5687). + + At the node tier `data` is the data-source **adapter** — the object `${data.total}` + in a props bag resolves against — and it has never been the row. A predicate + written with the deprecated `data.*` spelling therefore resolves `undefined == + 'draft'`, which is a perfectly good `false`: the block is hidden on every row, and + because it does not throw, the unresolvable-predicate reporter added for + objectui#5454 never fired. Measured on this base, the same predicate written as a + `{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether + an author heard about the identical mistake depended on which dialect they happened + to write it in, which is the arbitrariness objectui#5454 existed to remove. + + **No verdict changes and no interpolation changes** (maintainer ruling, + 2026-08-22, option A). The node tier keeps its documented `data` = adapter + semantics; objectui#5330's row binding does not extend here. The evaluator's answer + is returned exactly as computed and `${data.*}` interpolation is untouched — only + the silence moved. + + The report fires on a `data.*` read the bound adapter answers with `undefined`, not + on the spelling. A genuine adapter read stays silent (`data.total > 0` against an + adapter carrying `total`), a canonical `record.*` predicate stays silent, and a + correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per + node type + key + predicate source — the same module, Set and lifecycle as the + objectui#5454 reporter. + + This loudness is temporary by design: it dissolves when objectui#5330's deprecation + window for the `data.*` row spelling closes. +- 4f14ad7: `SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated + spelling of the objectui#5454/#5687 diagnostics when it is written inside + `properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }` + (objectui#5756). + + The `properties.*` evaluation loop runs, and interpolates every `${…}` template it + finds, **before** the visibility gate ever sees the value — so by the time the + existing diagnostics ran, a template-spelled predicate had already collapsed into a + plain boolean and there was no predicate text left to inspect. The bare-string + spelling of the exact same gate (`properties: { visible: "data.status == 'draft'" + }`) was unaffected — nothing interpolates a string with no `${` in it — and was + already reported; only the template spelling was structurally invisible. + + Reached by moving the diagnostic check to inside the `properties` evaluation loop, + on the predicate's raw (pre-interpolation) text, gated on the key being one of the + six visibility keys the render chain consults (`visibleWhen` / `visible` / + `visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content` + interpolation, or any other non-visibility key, is untouched and stays silent. + + Reports only the key that actually **decides** the node's visibility, mirroring + objectui#5454's own leg semantics (its reporter is likewise only ever invoked on + the leg the chain's early-return sequence actually reaches): a `properties.visible` + template that a co-declared `visibleWhen` outranks is not reported for deciding + nothing. + + **No verdict changes and no interpolation changes.** The diagnostic call's return + value is discarded; the real verdict is still computed afterward, off the + post-evaluation, post-hoist schema, by the same code path as before this change. + Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data + predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate — + only the silence moved, one render-step earlier. +- f90b8fb: `toRenderableSchema`'s header now says the bridge is permanent, instead of instructing + callers to remove it (objectui#4622). + + No executable line changes — but the artifact is **not** unchanged, and that is worth + stating plainly rather than rounding to "comment-only". This package builds with plain + `tsc`, and `tsconfig.base.json` sets `"removeComments": false` deliberately, so the JSDoc + is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both + what an editor shows on hover at every call site and bytes that ship. + + Measured by building the package the way the repo builds it, at both revisions: + `dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850 + to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19 + differing lines in the emitted file are JSDoc continuations and the three executable lines + are byte-identical, so the growth is the paragraph and nothing else. The trade is + deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old + paragraph's instruction produced once already. + + The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have + not been reconciled" and that "when it lands, the call sites using this can go back to + forwarding directly". Both halves went false when PR #4608 merged, and the second half is + the harmful one: it is an instruction whose trigger condition has now fired, sitting + directly above the function a future author is about to call. + + The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of + `@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring + an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548 + ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So + a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a + permanent crossing between two intentionally different types rather than scaffolding + awaiting a merge. + + Following the old instruction has a measured cost: five `apps/site` call sites were + forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly + five hours until PR #4621 routed all five through this function (objectui#4617). +- Updated dependencies [b2e85a9] +- Updated dependencies [c7cd2b6] +- Updated dependencies [77f846a] +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [3a58149] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [6ce89da] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [c40f3b8] +- Updated dependencies [199d31b] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [20e317c] +- Updated dependencies [8e00bfd] +- Updated dependencies [8d37efb] +- Updated dependencies [a691c0b] +- Updated dependencies [1e66879] +- Updated dependencies [c5200f0] +- Updated dependencies [af3861f] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [d1ab06f] +- Updated dependencies [38a9568] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [b2437a7] +- Updated dependencies [7a90afd] +- Updated dependencies [eddc1dd] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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..c7f184c15 100644 --- a/packages/runner/CHANGELOG.md +++ b/packages/runner/CHANGELOG.md @@ -1,5 +1,70 @@ # @object-ui/runner +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [dd19463] +- Updated dependencies [894d103] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [fe76ece] +- Updated dependencies [8ebd57f] +- Updated dependencies [58770f3] +- Updated dependencies [485f096] +- Updated dependencies [199d31b] +- Updated dependencies [b655a9d] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [d7bd274] +- Updated dependencies [ebce5a3] +- Updated dependencies [9850c6e] +- Updated dependencies [6c5ee71] +- Updated dependencies [6f017e9] +- Updated dependencies [a691c0b] +- Updated dependencies [0b1326d] +- Updated dependencies [af3861f] +- Updated dependencies [4f14ad7] +- Updated dependencies [fa140b8] +- Updated dependencies [71cba28] +- Updated dependencies [190fbd0] +- Updated dependencies [f2158ec] +- Updated dependencies [72ffc34] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [42887e0] +- Updated dependencies [d1ab06f] +- Updated dependencies [f90b8fb] +- Updated dependencies [91783c4] +- Updated dependencies [5a07e67] +- Updated dependencies [2d36552] +- Updated dependencies [490f482] +- Updated dependencies [27308c5] +- Updated dependencies [8689166] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- Updated dependencies [3c73d99] +- Updated dependencies [c86185e] +- Updated dependencies [4d73b07] + - @object-ui/types@17.7.0 + - @object-ui/components@17.7.0 + - @object-ui/plugin-charts@17.7.0 + - @object-ui/core@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..a4c833e1c 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,93 @@ # @object-ui/sdui-parser +## 17.7.0 + +### Minor Changes + +- 8d58f46: `validateTree` now reports a dashboard widget `options` key that no renderer + consumes as a `unconsumed-widget-option` **warning** naming the consumed set + (objectui#5709 ruling). The census behind the accepted set — the spec's five + declared query keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, + `stageOrder`) plus the `description` sub-caption convention key — is + re-measured on every test run against `@objectstack/spec` and the + `plugin-dashboard` renderer sources. The check fires only on dataset-bound + shorthand widgets (the spec-legal form) hosted by `dashboard` / + `dashboard-grid` nodes, and honours the spec's per-widget + `suppressWarnings: ['unconsumed-widget-option']` escape hatch. Warning + severity only: documents keep parsing, saving and rendering. Exported for + other surfaces: `checkDashboardWidgetOptions`, `CONSUMED_WIDGET_OPTION_KEYS`, + `DASHBOARD_WIDGET_HOST_TYPES`, `UNCONSUMED_WIDGET_OPTION`. + +### Patch Changes + +- 0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's + `source`, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, + Accepted) retracted. objectui#5461 corrected three sites; a multiline census + found eight more, in three spellings a line-oriented grep could not see. + + A page's `source` is *runtime metadata*. The console's Tailwind is compiled at + build time by scanning the console's own `src`, and there is no safelist, so it + never sees your page: an authored utility class produces CSS only by coincidence + (when objectui already ships that exact class) and otherwise produces nothing, + with no error anywhere. That is the ADR-0065 "works only by coincidence" failure + mode, and it is how a modal's `bg-black/50` backdrop reached production fully + transparent. `os validate` reports it as `page-source-className-tailwind`, a + warning on kinds `html`, `react` and `jsx`, shipped in `@objectstack/lint@11.5.0`. + + The tiers themselves are unchanged and every load-bearing claim survives — + parse-never-execute, the untrusted-author safety argument for `html`, and the + deprecated `'jsx'` alias. Only the styling primitive is corrected, to the wording + `content/docs/guide/react-pages.md` §Styling already uses: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`. + + Why each package has an entry — each was measured against its built artefact, not + assumed: + + - **`@object-ui/react-runtime`**: `README.md` is published to npm (npm includes + `README.md` in the tarball regardless of `files`). Its "no sandbox" callout is + the paragraph that routes untrusted-author work to the `html` tier, and it + carried the retracted framing line-wrapped across `:17-18`. It also gains the + §Styling section it was missing — the absence is why the framing survived here. + - **`@object-ui/sdui-parser`**: the corrected header of `src/types.ts` projects + verbatim into the published `dist/types.d.ts`. + - **`@object-ui/components`**: the corrected header of + `src/renderers/basic/html-elements.tsx` projects verbatim into the published + `dist/renderers/basic/html-elements.d.ts`. The `kind === 'html'` dispatch-arm + comment in `src/renderers/layout/page.tsx` does **not** project (it is inside a + function body) and is included here only because the same package already owes + an entry. + + No behaviour change: this is prose only. `CHANGELOG.md` occurrences are + deliberately untouched — immutable release history. +- 305205a: `kind:'html'` page sources keep the space between a text run and an adjacent + inline element: `A <strong>x</strong> page` now compiles to `A `/`<strong>`/` + page` and renders as `A x page` rather than `Axpage` (objectui#5661). + + The parser collapsed each text run's whitespace to a single space — correct, and + what HTML itself does — and then `.trim()`ed it, which is not: HTML collapses a + whitespace run to one space, it does not delete it. The deleted space was + precisely the separator between a run and its inline sibling, so every authored + sentence carrying emphasis or a link in the tier the guide recommends by default + rendered with its words run together. It was silent: the page rendered, the + structure was right, no diagnostic fired. + + The rule is deliberately mechanical rather than a block/inline taxonomy invented + for a schema tree that has none: keep one leading space when a sibling precedes + the run, and one trailing space when a sibling element follows it. The parent's + own start and end still drop their edge space, so `<p> hi </p>` is unchanged. + + Its one bounded cost: a whitespace-only run BETWEEN two siblings survives as a + single space, so a pretty-printed `<ul>` gains one `' '` string child per gap + between its `<li>`s — one space per gap, never the source's newline and + indentation, never at the container's own edges, and never inside an item's own + text. That bound is pinned by a test rather than left as a claim. + ## 17.6.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 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..fd2d86cdb 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,575 @@ # @object-ui/types +## 17.7.0 + +### Minor Changes + +- b55a346: `'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema` + (objectui#5740 — the value-level residue of objectui#5667's key-level + convergence of `CalendarViewSchema` on the registered `calendar-view` + renderer's measured read set; ADR-0049 enforce-or-remove). + + The union declared a value nothing enforced: the registered renderer's `view` + input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves + any off-enum value — `'agenda'` included — to `undefined` (the component's + `'month'` default), and `CalendarView` renders no agenda view. An author + writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with + no error or warning. No in-repo, example, or catalog app authors + `view: 'agenda'` (measured during objectui#5667's sweep and re-measured for + this change, including the objectstack tree). + + **This narrows the accept set — unlike #5667's key retirements, which created + no new rejections.** `view` is a declared key, and declared keys are validated + even under `.passthrough()`, so `view: 'agenda'` is now a **validation error + that previously parsed green** (an `invalid_value` issue on the `view` path, + offering `month`/`week`/`day`). Undeclared keys still pass through unchanged. + Breaking on the published zod surface; ships as `minor` per this repo's + version-alignment policy (majors track `@objectstack`). + + The runtime boundary is unchanged: an off-union `view` in raw metadata still + falls back to the component's `'month'` default at the renderer, and the + registry input already declared the three-value enum. Docblocks, the schema + reference table, and the zod `describe` no longer teach an `'agenda'` + fallback. +- 065bba7: `CalendarViewSchema` (TS interface and zod mirror) converges on the registered + `calendar-view` renderer's measured read set (objectui#5667, maintainer ruling + option A — the renderer is authoritative). + + **Breaking for consumers of the published type** (deliberate; per-repo policy + breaking changes ship as `minor` — the fixed group's `major` tracks + `@objectstack`): + + - Nine inert keys are retired: `events` (the interface's only required key, + which the renderer deliberately drops — objectui#4433), `defaultView`, + `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + `onEventUpdate`, `onDateChange`. None had a read site on the authored-node + path and no measured app authors them (ADR-0049 enforce-or-remove). + - The type now declares what the renderer actually reads: `data`, `titleField`, + `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, + `currentDate`, `allowCreate`, `className`, plus the two host-only function + hatches it forwards (`onEventClick`, `onViewChange`). + - Practical radius, measured: `BaseSchema` carries an index signature and the + zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys + neither fail to compile nor get rejected at validation — they are simply no + longer declared, documented, or type-checked. The material accept change is + that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` + node without it now validates (previously the one key validation demanded + was the one key guaranteed to do nothing). + + Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README + and `content/docs/api/schema-reference.md` are repaired to the converged + surface in the same change, so no copy of the old contradiction survives. +- 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`. +- ed8df3e: `'agenda'` leaves `defaultView` on all three of its declaration faces + (objectui#5784 — the `defaultView` sibling of objectui#5740's retirement on + `CalendarViewSchema.view`; ADR-0049 enforce-or-remove): the + `ObjectCalendarSchema` TS interface (an inline union `#5740`'s + `CalendarViewMode` narrowing could not reach), the zod `ObjectCalendarSchema`, + and the list-view `calendar` config's objectui-only `defaultView` extension. + All three are now `['month', 'week', 'day']`. + + The declarations admitted a value nothing enforced: `ObjectCalendar`'s props + declare `defaultView?: 'month' | 'week' | 'day'`, its schema read casts to the + same three values, and `CalendarView` renders no agenda view. An author + writing the type-legal, zod-valid `defaultView: 'agenda'` on an + `object-calendar` node or in a list view's `calendar` config got a month + calendar with no error or warning. The spec side already agrees: + `@objectstack/spec`'s `ObjectCalendarProps.defaultView` is + `['month', 'week', 'day']`. No in-repo, example, or catalog app authors + `defaultView: 'agenda'` (measured for objectui#5784 with positive controls, + including the objectstack tree — its only `agenda` token is the Agenda + job-scheduler library). + + **This narrows the accept set: an author who writes `defaultView: 'agenda'` + will now be refused at validation.** `defaultView` is a declared key, and + declared keys are validated even under `.passthrough()`, so + `defaultView: 'agenda'` is a **validation error that previously parsed + green** (an `invalid_value` issue on the `defaultView` / + `calendar.defaultView` path, offering `month`/`week`/`day`). Undeclared keys + still pass through unchanged. Breaking on the published zod surface; ships as + `minor` per this repo's version-alignment policy (majors track + `@objectstack`). + + The runtime boundary is unchanged: `ObjectCalendar` still resolves an + off-union raw `defaultView` away to its `'month'` default. Docblocks and both + zod `describe` strings now teach the three-value set. +- 199d31b: **`viewMode` is now declared authoring surface on `ObjectGanttSchema`, and both + gantt renderer branches honour it** (objectui#5074, maintainer ruling + 2026-08-19: declare-and-wire; the spec half landed first upstream). + + - `ObjectGanttSchema` (TS interface and zod mirror) declares `viewMode`, + DERIVED from the pinned `@objectstack/spec` `GanttConfigSchema.viewMode` + enum by reference, so the member list cannot drift. Deliberately no + default: an omitted `viewMode` keeps letting a persisted layout + (`persistLayoutKey`) seed the timeline granularity before the renderer's + `'day'` fallback. + - The timeline branch (`GanttView`) now receives an authored `viewMode`. + Previously only the resource-workload branch (`resourceView` + + `assigneeField`) honoured it, so `viewMode: 'month'` on an ordinary gantt + view was silently ignored. + - The `(schema as any).viewMode` cast in `ObjectGantt` is retired; both + branches read the declared `ganttConfig.viewMode`, which also honours the + key when authored inside the spec's `gantt` config block. + - Accept-set note: `viewMode` is now a DECLARED key, so an off-enum value + (e.g. `viewMode: 'hour'`) becomes a zod validation error where it + previously passed through unvalidated. Values on the published spec enum + are unaffected. +- 3ccd9e8: Split the static `table` column type off the rich shared `TableColumn` + (objectui#5474, maintainer ruling 2026-08-22: Option C), so declared = + enforced holds per renderer. + + `TableColumn` is unchanged and remains the rich shape `data-table`, + `CRUDSchema` and detail-view relations honour. The static `table` renderer's + `TableSchema.columns` now declares the new narrow `StaticTableColumn` + (`header`, `accessorKey`, `className`, `cellClassName`, `width` — exactly the + keys that renderer reads). The eleven keys the static renderer never read are + retired from its surface as ADR-0049 tombstones: `hoverable` / `striped` on + `TableSchema`, and `minWidth` / `align` / `fixed` / `type` / `sortable` / + `filterable` / `resizable` / `editable` / `cell` on its columns. + + Breaking for authored metadata that wrote those keys on a `type: 'table'` + node: they were silently inert before and are now refused loudly — a tsc + error on the interface (`?: never`) and a parse rejection naming the key in + `@object-ui/types/zod`. That loud refusal is the ruled outcome. Migration: + nodes that wanted the interactive behaviour move to `type: 'data-table'` + (whose columns keep the rich `TableColumn`); right-aligned columns on the + static table use `cellClassName: 'text-right'`; alternate-row styling uses + Tailwind on `className`. +- 78cbdb5: Retire `ThemeComponentSchema` (`type: 'theme'`) — a component kind no renderer + implemented (objectui#5489). + + `packages/types/src/theme.ts` declared a theme-manager **component** carrying + `themes[]`, `activeTheme`, `allowSwitching`, `persistPreference` and + `storageKey`, and `packages/types/src/zod/theme.zod.ts` published the matching + Zod object as a member of `ThemeUnionSchema` and therefore of + `AnyComponentSchema`. Nothing rendered it: `'theme'` appears at no + `ComponentRegistry.register(...)` / `registerLazy(...)` site in `packages/*/src`, + and in neither `PROTOCOL_COMPONENTS` nor `PALETTE_PLACEHOLDER_BLOCKS` + (`packages/components/src/renderers/placeholders.tsx`), so it did not even + resolve to a placeholder — a page declaring one got the registry's "Unknown + component type" panel (OBJUI-001) instead of a theme manager. Declared-but- + unenforced, removed under the maintainer ruling of 2026-08-21 on + objectstack#10485 (option B). + + Removed from the published surface: the `ThemeComponentSchema` type + (`@object-ui/types`), the `ThemeComponentSchema` Zod object + (`@object-ui/types/zod`), the `ThemeComponentSchemaType` inference alias, and the + `'theme'` member of `ThemeUnionSchema` / `AnyComponentSchema`. A schema spelling + `type: 'theme'` is now REFUSED by `AnyComponentSchema.safeParse` rather than + accepted and then rendered as an error panel, which is pinned by a test. + + **The theme system is unchanged.** `Theme` (the spec's authoring theme + document), `ThemeDefinitionSchema`, `ThemeModeSchema`, `ThemeEngine` + (`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are all retained and + untouched — the same ruling retains them explicitly. Author a theme as a + document handed to `ThemeProvider`; that path never went through the removed + component kind. +- b7543a9: Retire `ThemeSwitcherSchema` (`type: 'theme-switcher'`) and + `ThemePreviewSchema` (`type: 'theme-preview'`) — the two remaining theme + component kinds, which no renderer implemented — together with + `ThemeUnionSchema`, the union that after objectui#5489 held only these two + members (objectui#5647). + + `packages/types/src/theme.ts` declared a theme-switcher control (`variant`, + `showMode`, `showThemes`, `lightIcon`, `darkIcon`) and a theme-preview panel + (`showColors`, `showTypography`, `showComponents`), and + `packages/types/src/zod/theme.zod.ts` published the matching Zod objects as + the two members of `ThemeUnionSchema` and therefore of `AnyComponentSchema`. + Nothing rendered either: neither literal appears at any + `ComponentRegistry.register(...)` / `registerLazy(...)` site in + `packages/*/src` (202 registered keys enumerated; positive control on the same + pipeline: `tooltip` → 1), nor in `PROTOCOL_COMPONENTS` / + `PALETTE_PLACEHOLDER_BLOCKS` + (`packages/components/src/renderers/placeholders.tsx`), and no fixture + declares either kind (control: `"type": "form"` → 81) — so a page declaring + one got the registry's "Unknown component type" panel (OBJUI-001), never a + switcher or a preview. Declared-but-unenforced, removed under the 2026-08-21 + maintainer ruling (option B) on objectstack#10485, extended to these siblings + by inheritance on identical evidence (objectui#5647). + + Removed from the published surface: the `ThemeSwitcherSchema` / + `ThemePreviewSchema` types (`@object-ui/types`), the matching Zod objects and + `ThemeUnionSchema` (`@object-ui/types/zod`), and the `ThemeSwitcherSchemaType` + / `ThemePreviewSchemaType` inference aliases. `zod/theme.zod.ts` now exports + nothing and stands as the tombstone module. A schema spelling + `type: 'theme-switcher'` or `type: 'theme-preview'` is now REFUSED by + `AnyComponentSchema.safeParse` rather than accepted and then rendered as an + error panel, which is pinned by a test. + + **The theme system is unchanged.** `Theme` (the theme document vocabulary, + owned by `@object-ui/types` since objectui#5716), `ThemeEngine` + (`@object-ui/core`) and `ThemeProvider` (`@object-ui/react`) are retained and + untouched — the rulings retain them explicitly. Author a theme as a document + handed to `ThemeProvider`; that path never went through the removed component + kinds. +- c9327c9: Localize the theme document types: `@object-ui/types` now owns `Theme`, `ThemeMode` and `ColorPalette` (objectui#5716 ruling, 2026-08-23). The spec retired its theme module (objectstack#10485) while ObjectUI retained the theme system, so the types are hand-written from the last-published `@objectstack/spec` 17.1.0 shapes instead of re-exported — a spec dependency refresh past the retirement no longer breaks these packages. + + Published-name REMOVALS from `@object-ui/types` (zero in-repo readers, deleted under the same ruling's rider): + + - `Typography` — the shape lives on as the inline `Theme['typography']` member. + - `BorderRadius` — lives on as inline `Theme['borderRadius']`. + - `Shadow` — lives on as inline `Theme['shadows']`. + - `ThemeDefinition` — the deprecated alias of `Theme`; use `Theme`. + + Also added: `THEME_MODES`, a runtime tuple witness of the theme mode vocabulary (`['auto', 'light', 'dark']`). + + The `UI` protocol namespace (`import { UI } from '@object-ui/types'`) now resolves `UI.Theme` / `UI.ThemeMode` / `UI.ColorPalette` to the local owners, so they survive the upcoming spec refresh; the rest of the namespace continues to track `@objectstack/spec/ui`. After that refresh, retired spec/ui members (`UI.ThemeSchema`, `UI.ThemeModeSchema`, `UI.ThemeParsed`, `UI.Typography`, `UI.BorderRadius`, `UI.Shadow`, `UI.defineTheme`) drop out of the namespace. + + `@object-ui/providers`: `ThemePreference` is now derived from `@object-ui/types`' `ThemeMode` instead of the retired spec `ThemeModeSchema` (same union: `'auto' | 'light' | 'dark' | 'system'`). +- 920165d: Remove the six retired `@objectstack/spec/ui` theme-schema re-exports from `@object-ui/types/zod` — `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, `ShadowSchema`, `ThemeModeSchema`, `ThemeDefinitionSchema` (the spec's `ThemeSchema`) — plus their `…SchemaType` inference helpers, and the `theme` / `mode` props of `ThemePreviewSchema` (zod and interface) that consumed them. + + objectstack#10485 (ADR-0049 enforce-or-remove, PR objectstack#10695) retired the spec's whole `ui/theme.zod.ts` module, and the maintainer's ruling on objectstack#10856 (Options A + C) has objectui remove the dangling imports first so the Console Pin Gate can build objectui against the framework tree; restoring the spec exports (Option B) was explicitly not taken. Breaking in effect for anyone importing those six names from `@object-ui/types/zod`: there is no replacement — the validators retired upstream with no successor. The theme TYPE surface (`Theme`, `ThemeMode`, `ColorPalette`, … re-exported from `@object-ui/types`) and the ThemeEngine/ThemeProvider runtime are unchanged. +- 4d73b07: The zod `BaseSchema` mirror now accepts everything its TypeScript declaration + declares — five keys had drifted narrower (objectui#4605). + + `@object-ui/types/zod` is a published runtime validator hand-written to mirror the + `BaseSchema` interface. As the interface widened, the mirror did not, so five keys + refused at parse time a spelling the published types invite and the renderer + implements — "declared = enforced" inverted. `.passthrough()` rescued none of them: + passthrough admits UNDECLARED keys, and all five are explicitly declared, so the + narrow declaration won. + + Measured against the unmodified mirror before the change, these were the refusals: + + | key | authored input | old mirror said | + |---|---|---| + | `visible` | `'${data.status === "open"}'` | `expected boolean, received string` | + | `disabled` | `'${data.status === "locked"}'` | `expected boolean, received string` | + | `ariaLabel` | `{ key, defaultValue }` | `expected string, received object` | + | `label` | `{ en: 'Owner', 'zh-CN': '负责人' }` | `expected string, received object` | + | `description` | `{ en: 'The record owner' }` | `expected string, received object` | + + `visible`/`disabled` now take `boolean | string` — what `evaluateCondition` accepts, + no wider. `ariaLabel` takes the KEYED reference through a new exported + `KeyedI18nLabelSchema`; `label`/`description` take the spec's own `I18nLabelSchema` + BY REFERENCE, so a change to the spec's label contract is picked up rather than + re-typed. Every spelling that parsed before still parses. + + The two i18n vocabularies are kept apart rather than merged into "some object". + `label`/`description` are the spec's INLINE locale map (resolved by + `resolveI18nLabel(label, locale)`); `ariaLabel` is the KEYED reference (resolved by + `resolveKeyedI18nLabel`, which returns `undefined` for a locale map and would render + an EMPTY aria-label). Widening both slots to accept either shape would have + reproduced objectui#4167's confusability hazard inside the validator that exists to + catch it, so each slot admits only its own vocabulary and both cross pairings are + pinned as rejections. + + The new pin is DERIVED rather than a hand-written key list: it reads the mirror's own + `.shape` and compares each key against the declaration, so the next widening of + `base.ts` that forgets this file turns it red with no list to maintain. It reads + `.shape` and not `keyof z.input<…>` because that spelling was measured vacuous — + `.passthrough()` collapses the inferred key union to bare `string`, and a pin written + over it resolved `never` while five keys were demonstrably narrow. Two guards pin the + derivation against both degenerations (`never` and `string`). + +### Patch Changes + +- 100547e: `objectui validate` now refuses a form field whose widget id names a namespace + other than `field:`, matching the verdict `@object-ui/core`'s `validateSchema` + has given since objectui#5375 (objectui#5449). + + The CLI reaches `FormFieldSchema` through `safeValidateSchema`, and that schema + declared `type` and `widget` as bare optional strings — so a field typed + `ui:password` validated clean while the runtime validator rejected the same + document with `UNRESOLVABLE_FIELD_WIDGET_NAMESPACE`. The CLI is the surface an + author actually runs before shipping, so it was the one handing out the false + green: an author did exactly the diligence objectui#5375 asks for and still + shipped metadata that renders a secret into a plain text box. + + A `superRefine` on `FormFieldSchema` now states the rule, mirroring core's + precedence (`widget` before `type`), the key it blames, its error code and its + message verbatim, so the two entry points cannot describe one defect two ways. + + **This rejects documents that previously validated.** Only colon-qualified + field widget ids outside the `field:` namespace are affected — `field:`-prefixed + ids and bare names such as `password` still pass, registered or not. A field + carrying, say, `type: 'ui:password'` must be rewritten as `password` or + `field:password`; it never rendered as a password box in any case. + + Which of the repo's authoring-time validators is canonical remains open + (objectui#4631) — this states the rule on the zod side rather than unifying + them. +- 0e05aac: The console's cold load no longer asks `/api/v1/runtime/config` or + `/auth/me/localization` twice (objectui#5544). + + Two pairs of boot callers were racing each other for the same URL, with no shared + provider between them, so no guard inside either component could see the other: + + - `GET /api/v1/runtime/config` — the pre-React branding script inlined in + `apps/console/index.html` (it runs during HTML parse so the tab title and + favicon are the operator's before the bundle is fetched) and + `initRuntimeConfig()`. Measured ×2 on prod and on staging. This is the + expensive one: the console `await`s `initRuntimeConfig()` before + `createRoot().render()`, so the duplicate sat on the critical path to first + paint, and at the control plane's ~0.5–1.4 s for this endpoint it also pushed + boot concurrency further past the server's pool knee. + - `GET /api/v1/auth/me/localization` — `seedTenantLanguage()` on a device's true + first visit and `LocalizationFetchProvider` on every boot. The seed keeps + running past its 500 ms race by design and the provider mounts the moment that + race resolves, so on a first visit the two overlap. Measured ×2 on staging. + + `@object-ui/types` gains `sharedGetJson()`: callers that ask for the same GET + while one is already in flight join that request instead of starting another. It + shares the in-flight promise and nothing else — the entry is deleted the instant + the request settles, so there is no cache, no TTL and no stale window, and a + caller arriving after settle fetches fresh exactly as before. Rejections fan out + to every sharer with the status intact (`LocalizationFetchProvider`'s retry + policy still sees its own 503), each caller receives its own copy of the parsed + body, and only GETs are eligible — a non-GET is refused rather than quietly + rewritten. + + Requests that differ in credentials mode or headers keep separate identities, so + the console's two deliberate `auth/get-session` calls — one Bearer-only with the + cookie omitted to detect a stale token, then one through the cookie — stay two + requests. Collapsing those would have destroyed the signal the first one exists + to read. + + No component receives anything different: same payloads, same errors, one fewer + round trip. +- f9e4f91: `DashboardComponentSchema.dateRange.defaultRange` is now bound to + `DateRangeDefaultRange` from `@objectstack/spec/ui` instead of restating it as a + hand-written 14-member union (objectui#4984). + + The union was byte-faithful to the spec — all 14 members, same order — so nothing + a user hits changes today. What was missing is the tie that keeps it faithful: + `resolveDashboardFilterDefs` takes `Pick<DashboardComponentSchema, 'globalFilters' | + 'dateRange'>`, so this union is what typechecks every TS-constructed dashboard, and + a preset the spec ADDS would have been a legal document that objectui's own types + said could not exist — the "narrower than the contract it implements" shape whose + consequence in objectui#4163 was that the bad reads were invisible to `tsc`. + + No gate reported it: `check:spec-symbols` rule 1 matches by NAME and an inline union + on an interface member has no symbol to collide with, while rule 2's claim heuristic + was waved through by the `SpecGlobalFilter` reference a few lines above. Binding makes + the file's existing "Aligned with @objectstack/spec" comment structural rather than + prose. + + The emitted `.d.ts` collapses the inline union to the imported alias; the published + type surface is unchanged — measured with the TypeScript checker over the emitted + declarations (679 reachable exports from `dist/index.d.ts`, 22 from `dist/complex.d.ts`, + and `defaultRange` resolving to the same 14 string-literal members before and after). +- 4e8622b: `ActionParam`'s doc block no longer claims that spec 17 narrowed `I18nLabel` to a + plain string (objectui#4611). + + The paragraph explaining why `label` / `options[].label` are inherited rather than + locally overridden justified itself with a claim about `@objectstack/spec` that was + never true: "in spec 17 `I18nLabelSchema` is `z.ZodString` — inline per-locale objects + were dropped in favour of translation files". Measured against the installed GA pin + `@objectstack/spec@17.0.0` (`dist/ui/index.d.ts:614`), `I18nLabelSchema` is a union of + a string and a string-to-string record, and the schema's own doc block states two + authorized forms with "Both are real; neither is deprecated by this schema". Executed + against `dist/ui/index.mjs`: plain string accepted, inline locale map accepted, + `{ key, defaultValue }` rejected. A reader who believed the comment would have taken a + widening to `string | I18nLabel` for a no-op — which is what the finding recorded, one + seat having nearly done exactly that. + + The replacement describes what `I18nLabel` admits and cites the spec's own doc block + rather than restating a zod expression; where today's spelling is named it is scoped as + a measurement against 17.0.0 with its file and line, so it ages as a reading rather than + as a standing fact. The decision itself is unchanged and never depended on the false + premise — `label` flows in by reference through the spec's schema, and a local + `string | I18nLabel` collapses to `I18nLabel` whichever forms the union holds. + + Documentation only, and the release-visible surface is the declaration file: measured + with the package's real `tsc` build (`removeComments: false`, per `tsconfig.base.json`), + 108 emitted files on both sides, `dist/ui-action.d.ts` 29,176 → 31,026 bytes, and every + other file byte-identical — including `dist/ui-action.js` (3,480 bytes, unchanged sha), + because the comment documents an `interface`, which is erased at emit along with its + leading comment. No behaviour changes; hover text and the shipped `.d.ts` do. +- dffd752: The `I18nLabel` "inverted pin" now watches the premise it claims to watch, and + `ui-action.ts` no longer imports a symbol it never uses (objectui#5612, objectui#5613). + + Both are residue of the same removed local `label` / `options[].label` override. + + The `it(...)` case in `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` + that called itself an inverted pin on the spec's `I18nLabel` rested on one assertion, + `const label: SpecI18nLabel = 'Priority'`, under a comment claiming spec 17 had narrowed + `I18nLabel` to a plain string and that a re-widening would stop it compiling. A plain + string is assignable under the narrow shape *and* under the wide one, so that assignment + could only ever fail if the plain-string form were removed — the opposite of the event it + was written to catch. The widening had already landed: `@objectstack/spec@17.0.0` + declares `I18nLabelSchema` as a union of a string and a string-to-string record + (`dist/ui/index.d.ts:614`), and the pin stayed green through it. It reported protection + it did not provide, and asserted a false premise in its own name. + + It is retargeted at what actually holds the decision up — not which single form the spec + has, but that **both** authorized forms stay assignable, on the spec type and on the + inherited `ActionParam['label']` and `options[].label`. It now fails when either form is + withdrawn, and deliberately does not fail on a further widening, since inheriting by + reference is exactly what stays correct as the authorized set moves. The comment is + rewritten against the schema's own doc block (two authorized forms, "Both are real; + neither is deprecated by this schema") instead of the false premise. Verified by + construction: against a locally built narrow `type I18nLabel = string` the new assertions + fail with `TS2344` and `TS2322`, where the old assignment compiles clean under both + shapes. + + `ui-action.ts`'s `I18nLabel` type import is deleted — no type position had used it since + the override was removed, and nothing re-exported it — and the doc paragraph that + recorded the pin as `NOT guarded` is corrected, since the same change makes it a guard. + + No behaviour changes; the release-visible surface is the declaration file. Measured with + the package's real `tsc` build, both legs building from a cleared `dist/` and cleared + composite build info: 108 emitted files on both sides, exactly one differing — + `dist/ui-action.d.ts`, 31,026 → 31,117 bytes, JSDoc prose only, no declaration changed. + Every other file is byte-identical, including `dist/ui-action.js` (3,480 bytes, unchanged + sha), because the comment documents an `interface`, which is erased at emit along with + its leading comment. The deleted type import contributes no emitted delta at all, and the + rewritten test file is not part of the build. +- a691c0b: `PageSchema.kind`'s TSDoc names the real per-tier styling primitive for source-authored pages instead of the "HTML + Tailwind" framing ADR-0080's own amendment retracted. + + This is a published type surface: the TSDoc ships in `@object-ui/types`'s built + `.d.ts` and is what an author reads on hover over `kind`. It said a `kind:'html'` + page is "constrained JSX/HTML + Tailwind" — and it links + `content/docs/guide/react-pages.md`, which objectui#5413 has already corrected to + say the opposite. Shipped type documentation was contradicting the guide it points + readers to. + + ADR-0080's header amendment (2026-06-30, under ADR-0065, Accepted) supersedes that + framing on styling: a page's `source` is *runtime metadata*, the console's Tailwind + is compiled at build time by scanning the console's own `src`, and there is no + safelist — so an authored utility class produces CSS only by coincidence, when + objectui already ships that exact class, and otherwise produces nothing with no + error anywhere. That is the ADR-0065 failure mode verbatim ("works only by + coincidence"), and it is how a modal's `bg-black/50` backdrop reached production + fully transparent. + + The tiers themselves are unchanged, and every load-bearing claim in the TSDoc + survives verbatim — parse-never-execute and untrusted-author safety for `html`, + the deprecated `'jsx'` alias, EVALUATED-in-the-main-tree with no sandbox behind the + `react-pages` host capability for `react`, the ADR-0080 citation and the guide + link. Only the styling conclusion changes, to the primitive each tier actually has: + + | `kind` | Style with | + |---|---| + | `"html"` | The blocks' own structured props (`` `<flex direction gap>` ``, `` `<grid columns>` ``) plus a JSON `style` object. | + | `"react"` | Inline `style` objects. | + + Colors on both tiers come from the theme as `hsl(var(--token))`, so a page follows + light/dark and whatever theme the deployment installs. The TSDoc now also names the + rule that reports a violation — `page-source-className-tailwind`, shipped in + `@objectstack/lint@11.5.0` as `validatePageSourceStyling` and reported by + `os validate` as a warning on both tiers. + + No behaviour change, and the accepted `kind` set is untouched. + + `packages/components/src/renderers/layout/react-page.tsx` carries the same + correction on its two source comments (the injected-scope note and + `buildComponentScope`), and gains the styling note the file was missing. Those are + internal comments — they do not project into any `.d.ts` and change no export — so + they get no entry of their own; there is nothing an `@object-ui/components` + consumer could read in a CHANGELOG and act on. +- 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..93f0ec0db 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 17.7.0 + +### Patch Changes + +- Updated dependencies [b55a346] +- Updated dependencies [065bba7] +- Updated dependencies [100547e] +- Updated dependencies [d7573b3] +- Updated dependencies [bf3edfe] +- Updated dependencies [0e05aac] +- Updated dependencies [e719ebd] +- Updated dependencies [f9e4f91] +- Updated dependencies [fa429cf] +- Updated dependencies [ed8df3e] +- Updated dependencies [8ebd57f] +- Updated dependencies [199d31b] +- Updated dependencies [7138bc1] +- Updated dependencies [cef27e2] +- Updated dependencies [4e8622b] +- Updated dependencies [dffd752] +- Updated dependencies [3ccd9e8] +- Updated dependencies [a691c0b] +- Updated dependencies [af3861f] +- Updated dependencies [f2158ec] +- Updated dependencies [78cbdb5] +- Updated dependencies [b7543a9] +- Updated dependencies [6c6cee7] +- Updated dependencies [d1ab06f] +- Updated dependencies [91783c4] +- Updated dependencies [2d36552] +- Updated dependencies [c9327c9] +- Updated dependencies [920165d] +- 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",