diff --git a/.changeset/action-dialog-param-values-as-option-record.md b/.changeset/action-dialog-param-values-as-option-record.md deleted file mode 100644 index 4e5fa7b7ba..0000000000 --- a/.changeset/action-dialog-param-values-as-option-record.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -An action dialog's per-option `visibleWhen` predicates now read the dialog's own in-progress param values. - -A field's per-option `visibleWhen` reaches a dialog param's control (objectui#3559), -but the dialog supplied no record to evaluate it against: it passed no -`dependentValues`, so the shared cascading-options evaluator fell through its -chain (`dependentValues ?? formValues ?? data`) to the host page's record, or to -nothing at all. A predicate written against a SIBLING PARAM — `record.country == -'cn'` on a province option, next to a `country` param in the same dialog — could -therefore never see the value the user had just entered. Authored cascades were -dead on this surface, in the safe direction: an unresolvable predicate offers the -option rather than hiding it, so nobody was shown a wrongly-narrowed list. - -Per the maintainer's 2026-08-11 ruling (Option B on objectui#3765) the dialog is -a small form, and its in-progress values are that record. The dialog now passes -them as `dependentValues` to the option widgets (`select`, `multiselect`, -`radio`, `checkboxes` — the same allow-list the object form threads the live -record to). The evaluator is unchanged; this is the supply half that was missing. - -Ruled consequence, stated because it is a behavior change and not only a fix: a -supplied record wins that chain outright, so an option predicate naming a field -of the underlying ROW that the dialog has no param for no longer resolves inside -a dialog. It becomes unresolvable, which fails open — the option stays offered, -never wrongly hidden. Merging the two records was the alternative reading and was -deliberately not taken: it would introduce a third scope dialect that has to be -written into the contract before anything can rely on it. diff --git a/.changeset/address-field-locale-labels-and-order.md b/.changeset/address-field-locale-labels-and-order.md deleted file mode 100644 index c311b74bfb..0000000000 --- a/.changeset/address-field-locale-labels-and-order.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/i18n': patch ---- - -`AddressField` is translatable, shows no US example placeholders, and formats its readonly line in the reader's address order. - -The five sub-labels ("Street Address", "City", "State / Province", "ZIP / -Postal Code", "Country") were English string literals with no i18n key. On a -non-English console every address field showed five English words in the middle -of an otherwise fully translated form, and an app had no way to reach them: the -parts are not fields on the object (`billing_address` is a single `address` -column), so a translation bundle had nothing to key on, there is no `subLabels` -property to declare, and the widget cannot be replaced from metadata. They now -resolve through `fields.address.street` / `.city` / `.state` / `.postalCode` / -`.country`, added to all ten locale packs. The `en` values are byte-identical to -the literals they replace, and `FIELD_DEFAULTS` carries the same five, so -English and provider-less rendering are unchanged. - -The five input placeholders (`123 Main St`, `San Francisco`, `CA`, `94102`, -`United States`) are **removed** rather than keyed. They were untranslated and -US-specific — a zh/ja/ar user was shown an American address as the example of -what to type — and the right example is a function of the address's country, -not the reader's language, which no channel in the stored value can supply -today. Each box keeps the visible label that names it. - -The readonly line's part order now follows the reader's display locale -(`useDisplayLocale()`): `zh`, `ja` and `ko` read largest-first (`Country, ZIP -State, City, Street`), every other locale keeps the unchanged small-to-large -order (`Street, City, State ZIP, Country`). The display cell renderer takes the -same locale through the same shared `formatAddress`, so a stored address reads -identically in a readonly form and in a grid cell. diff --git a/.changeset/adr36-server-fault-policy-narrowing-3888.md b/.changeset/adr36-server-fault-policy-narrowing-3888.md deleted file mode 100644 index 97bbee707f..0000000000 --- a/.changeset/adr36-server-fault-policy-narrowing-3888.md +++ /dev/null @@ -1,45 +0,0 @@ ---- ---- - -Docs-only correction, no behaviour and no authoring-surface change (objectui#3888). - -ADR-0036's `## Server enforcement (framework)` section stated the server's fault -policy unconditionally: "A predicate that fails to evaluate is **fail-open** and -logged". That covered both server-enforced predicates in one line, and it has been -false for one whole fault class since objectstack#4889: a `readonlyWhen` predicate -that faults because it names a scope ROOT the write never bound (`parent.status == -'paid'` with no master-detail header in hand) is fail-CLOSED — -`isReadonlyWhenLocked` warns `… treating the field as LOCKED` and resolves the -field to locked, and `stripReadonlyWhenFields` / `stripReadonlyWhenFieldsMulti` -then delete that key from the UPDATE payload, surfacing as a `droppedFields` entry -with `reason: 'readonly_when'`. - -This was the last uncorrected copy of that stale assertion. The code-comment copy, -in `packages/core/src/evaluator/fieldRules.ts`, was narrowed by objectui#3828; the -ADR is where a reader looks FIRST when asking what the server does, so leaving it -preserved the wrong mental model in the more authoritative place. - -The summary bullet now carries the exception and points at a new subsection that -states it in full: why the unbound-root case is not a broken predicate, what the -server does with it, and — since the client deliberately does NOT mirror it — the -silent symptom that divergence produces (field renders editable, save reports -success, value never lands) plus which end to debug. The `## Client enforcement -(objectui)` section's "the same posture as the server" clause is narrowed the same -way, since it asserted the same parity. - -Two things verified as still accurate and left alone: the `requiredWhen` half -(objectstack#4977 bound the same `parent` scope but deliberately kept fail-open -semantics, so an unevaluable requirement is skipped on both ends) and the -`visibleWhen` bullet (the server never evaluates it). Both are now stated as -explicit non-exceptions rather than implied by an over-broad summary. - -The ADR-0057 D10 citation is marked as the **framework's** numbering: this repo -carries an unrelated `docs/adr/0057-console-ai-chat-one-conversation-docked.md`, -so an unqualified "ADR-0057" resolves to the wrong document for a reader in this -tree. It is also written as an attribution — the shorthand the framework's -rule-validator, lint diagnostics and QA runner all use — rather than as a claim -that the D-anchor resolves, because verification could not confirm that it does -(filed against the framework, not fixed here). - -No package is declared because nothing published changed: the diff is one -markdown file under `docs/adr/`. diff --git a/.changeset/anchor-drift-cumulative-report-4974.md b/.changeset/anchor-drift-cumulative-report-4974.md deleted file mode 100644 index 6fe43e103f..0000000000 --- a/.changeset/anchor-drift-cumulative-report-4974.md +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- - -Test-only: the two generator anchor rules — `packages/cli`'s app/init manifests and -`packages/create-plugin`'s template devDependencies — now collect every drifted range and -report them from a single assertion, instead of one `expect` per name that threw on the -first mismatch. A dependabot batch that moves several in-repo ranges is one round of -repair rather than one round per name, and which name got reported no longer depends on -its position in the anchor table. The preconditions (the anchor must resolve; in-repo -manifests must agree) still fail fast and are kept in a separate pass, so a repo-state -failure is never reported as, or beside, template drift. No published behaviour changes -(objectui#4974). diff --git a/.changeset/api-console-drop-dead-catalog-keys-4303.md b/.changeset/api-console-drop-dead-catalog-keys-4303.md deleted file mode 100644 index d3c68f3ece..0000000000 --- a/.changeset/api-console-drop-dead-catalog-keys-4303.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': patch ---- - -API Console's endpoint catalog drops the `workflow` and `feed` entries — 7 endpoint declarations that could never render on any host - -`SERVICE_ENDPOINT_CATALOG` keys are looked up directly in `/discovery`'s `services` map, which the framework keys by `CoreServiceName`. Neither `workflow` (5 endpoints under `/api/v1/workflow/*`) nor `feed` (2 endpoints under `/api/v1/feed/*`) names a `CoreServiceName` slot: the `workflow` slot was retired upstream (objectstack#4451) and `feed` never was one. Both were unconditionally hidden by the fail-closed lookup (ADR-0076 D12) — a miss is indistinguishable from "no such service" — so this changes no rendered output; it only removes two catalog entries that could never surface an endpoint. - -Counter-probed against current objectstack `origin/main` before removal: no `registerService('workflow')`, no mounted `/api/v1/workflow` route, and no `/api/v1/feed` route anywhere in source — both are confirmed dead, not merely unused. Per objectui#4303's ruling, this is dead-code removal, not a rename: neither key has a correctly-spelled slot to move to. - -#4240's tripwire test — which pins `SERVICE_ENDPOINT_CATALOG` keys against `CoreServiceName` and had carried `workflow`/`feed` as a documented exception set — is trimmed alongside the catalog: the exception set and its `#4303` reference are removed now that both keys are gone, so the assertion goes back to a plain "every catalog key is a canonical slot" with no carve-outs. diff --git a/.changeset/app-shell-docs-nav-examples.md b/.changeset/app-shell-docs-nav-examples.md deleted file mode 100644 index 66184f299b..0000000000 --- a/.changeset/app-shell-docs-nav-examples.md +++ /dev/null @@ -1,11 +0,0 @@ ---- ---- - -Docs + test-only (objectui#4793). `content/docs/layout/app-shell.mdx`'s two `SidebarNav` -examples now spell the real `NavItem` keys — `title` instead of `label`, and the imported -Lucide **component** instead of a quoted icon name — and stop teaching a `header` / -`footer` prop that `SidebarNavProps` never declared. The examples are pinned to the real -types by `packages/layout/src/__tests__/app-shell-docs-nav-example.test.ts`. - -No published behaviour changes: nothing in any package's runtime source was touched, only -the documentation page and the test that now compiles it. diff --git a/.changeset/app-shell-props-block-4808.md b/.changeset/app-shell-props-block-4808.md deleted file mode 100644 index 14ecb882ed..0000000000 --- a/.changeset/app-shell-props-block-4808.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -Docs-only alignment plus its pin: `content/docs/layout/app-shell.mdx` now reprints all -seven props `AppShellProps` declares (adding `branding` and `rightRail`), and the existing -app-shell docs pin test compares that block against `packages/layout/src/AppShell.tsx` on -every run. No published behaviour changes — the MDX page is not part of any released -package, and the only source file touched is a test (objectui#4808). diff --git a/.changeset/approval-admin-override-affordance-and-timeline-marker.md b/.changeset/approval-admin-override-affordance-and-timeline-marker.md deleted file mode 100644 index b3665c67b4..0000000000 --- a/.changeset/approval-admin-override-affordance-and-timeline-marker.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch -'@object-ui/i18n': patch ---- - -An admin override of an approval now looks like one — before the click, and in the timeline afterwards. - -A platform/tenant admin who holds **no slot** in a request's pending-approver -slate saw the exact same filled **Approve** / **Reject** / **Reassign** a -designated approver sees: no distinct styling, no warning, nothing. One unmarked -click takes framework#3424's privileged branch, which is *authoritative* — it -finalises the node even under `per_group` / `unanimous` / `quorum`, silently -bypassing every co-sign group that has not voted. The measured consequence in a -real app project was a PM who clicked Approve *"to see if it was real"* and -finalised a stage for an approver who never acted, then filed it as "countersign -is broken". The backend was working as designed; the console gave the admin no -way to see that. - -Two halves, both client-side: - -- **Affordance.** A viewer the server reports as `can_act: false` **and** - `can_override: true` now gets a warning-styled action labelled as an override - (`Override Approve`), and the dialog it opens **names the pending approvers - being bypassed**. The wording is the fix, not decoration — a warning that does - not say what is about to be bypassed would not have stopped that click. -- **Timeline.** `sys_approval_action.via_override` has been written since - framework#4466 and sent on the wire ever since, but **no console surface read - it** — an override rendered byte-for-byte like an ordinary approval in both the - record page's approvals panel and the Approval Center. Override rows are now - marked with an `Admin override` chip and a distinct timeline dot. This is - framework#4466's own *Expected* ("surfaced in the timeline"), which never - landed. - -Nothing here relaxes anything. Who may act or override is unchanged, the request -the console sends is byte-identical, and no audit record is altered — this only -renders one that was already being written, and adds friction in front of a -privileged path. - -Two details worth knowing, because both are load-bearing: - -- The warning rides the **param dialog's** title and description rather than a - chained `confirmText`. These decision actions collect params, so the param - dialog is already the confirm — nothing is POSTed until its own Confirm — and - putting a second dialog in front of it produces a first prompt that reads as - "the action ran" (framework#7278, maintainer ruling 2026-08-10). One condition, - one wording, one dialog. -- The notice travels as its own dispatch key, **not** folded into the action's - `description`. The runtime resolves `description` through - `_actions..description`, preferring a bundle hit over the passed literal, - and `plugin-approvals` ships exactly such an entry for `approval_reject` — so a - warning routed that way would have been silently replaced by the ordinary - reject copy in every locale carrying the bundle. A safety notice a translation - can delete is not a safety notice. - -Which actions get the treatment is read from each action's **own declared -`visible` gate** (does it OR in `can_override`?), not from a hard-coded name -list, so a future decision action still ships as metadata alone — and -`approval_recall`, gated on `is_submitter`, is never relabelled. Every new string -goes through the `approvalsInbox.*` i18n path in all ten locale packs. diff --git a/.changeset/appshell-drop-dead-sidebar-import.md b/.changeset/appshell-drop-dead-sidebar-import.md deleted file mode 100644 index 2123d91f3e..0000000000 --- a/.changeset/appshell-drop-dead-sidebar-import.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@object-ui/layout': patch ---- - -AppShell: drop the unused `Sidebar` import. `AppShell` renders the node the caller passes -in the `sidebar` prop and never constructs a `Sidebar` itself, so the import was dead -(tree-shaken out of every bundle) and only suggested otherwise to readers. No runtime -behaviour changes. diff --git a/.changeset/attachments-api-disabled-no-retry.md b/.changeset/attachments-api-disabled-no-retry.md deleted file mode 100644 index 3258613901..0000000000 --- a/.changeset/attachments-api-disabled-no-retry.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@object-ui/plugin-list': patch -'@object-ui/react': patch ---- - -RecordAttachmentsPanel no longer offers a Retry for an api-disabled `sys_attachment` read. - -`OBJECT_API_DISABLED` (404, `enable.apiEnabled: false`) and its sibling -`OBJECT_API_METHOD_NOT_ALLOWED` (405, the operation is absent from -`enable.apiMethods`) are pure functions of the object's metadata — no user, no -session, no request body — so every retry of every persona re-fetches the -identical refusal. Before this change both landed in `RecordAttachmentsPanel`'s -`unavailable` state and offered a Retry that was guaranteed to change nothing, -the same wrong advice `ListView`'s error panel already stops giving for list -reads. - -The panel gains a fifth status, `api-unavailable`: no Retry button, and honest -copy ("The attachments list is not available on this object.", new -`detail.attachmentsApiUnavailable` key in all ten locale packs) instead of -"We couldn't load the attachments for this record." The pre-existing `denied` -(authorization) and `unavailable` (network/5xx/expired-session) states and -their affordances are unchanged. - -`ListView.classifyLoadError` — the classifier that already separated this case -into its own `api-disabled` kind for list views — is lifted out of -`packages/plugin-list/src/ListView.tsx`'s module scope into -`@object-ui/react` (`classifyLoadError`, `LoadErrorKind`), so both surfaces -consume one classification instead of `RecordAttachmentsPanel` re-deriving it. -`ListView`'s own behavior is unchanged — it now imports the function it -previously defined locally. The classifier delegates its api-disabled check to -`isApiAccessDeniedError` (`@object-ui/data-objectstack`), removing a second, -independently-maintained copy of the same code list. diff --git a/.changeset/attachments-denied-vs-empty.md b/.changeset/attachments-denied-vs-empty.md deleted file mode 100644 index 5c7571dd81..0000000000 --- a/.changeset/attachments-denied-vs-empty.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -RecordAttachmentsPanel distinguishes a DENIED attachment list from an empty one. - -A `sys_attachment` list read refused for authorization reasons (HTTP 403 / -`PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security denial) was swallowed -into the panel's empty state, so a member denied the parent record was told -"No attachments yet. Upload a file to get started." about a record holding -2095+ attachments — and was offered an Upload the server would refuse. - -The panel now classifies that refusal with the same `isPermissionError` -predicate the kanban, calendar and form surfaces branch on, renders a distinct -denied state ("You don't have access to these attachments.", new -`detail.attachmentsAccessDenied` key in all ten locale packs), and withdraws -the Upload affordance. The denied state renders the translated sentence and -nothing sourced from the error — no status code, no server text, no row count. -The empty state is now reserved for a genuine 200-with-zero-rows; non-authz -failures keep their pre-existing handling. diff --git a/.changeset/attachments-unavailable-vs-empty.md b/.changeset/attachments-unavailable-vs-empty.md deleted file mode 100644 index 5b94ddfc8d..0000000000 --- a/.changeset/attachments-unavailable-vs-empty.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -RecordAttachmentsPanel distinguishes an UNLOADED attachment list from an empty one. - -A `sys_attachment` list read that failed for any non-authorization reason — a -network failure (server unreachable, DNS, aborted request), a 5xx, or a 401 / -`AUTH_REQUIRED` (an expired session is authentication, not authorization, so the -denied predicate deliberately does not claim it) — was swallowed into the -panel's empty state. All three rendered "No attachments yet. Upload a file to -get started.": an affirmative claim about the record's contents, made by a panel -that never got an answer, over a record that may hold thousands of files. - -The panel now carries the same four-way status vocabulary its siblings use — -`loading` / `loaded` / `denied` / `unavailable` — and every state that means -"the panel does not know" is answered before `rows.length === 0` is allowed to -mean "the record holds nothing". A failed read renders a distinct unavailable -state ("We couldn't load the attachments for this record.", new -`detail.attachmentsLoadFailed` and `detail.retryLoadAttachments` keys in all ten -locale packs) with a **retry** — unlike the denied state, an outage and a lapsed -session are both things a second attempt can fix — and withdraws the Upload -affordance, because offering an upload against a list the panel could not reach -is the same over-assertion as the empty state it replaces. Like the denied -state, it renders the translated sentence and nothing sourced from the error: no -status code, no server message, no host. - -The empty state is now reserved for a genuine 200-with-zero-rows, and the -denied state (403 / `PERMISSION_DENIED` / `FORBIDDEN` / a row-level-security -denial) is unchanged. The "table not provisioned on an older stack" case is -unaffected: the ObjectStack adapter degrades a bare 404 to `{ data: [], total: 0 }`, -so it resolves through the success path and still renders the empty state. - -This restores a house rule that had already landed twice as a bug fix — -`HomeActionCenter` may only say "You're all caught up" once the inbox has -answered, and an unloadable app list is UNKNOWN rather than "no default app". diff --git a/.changeset/audit-lock-state-zh-vocabulary-5004.md b/.changeset/audit-lock-state-zh-vocabulary-5004.md deleted file mode 100644 index b67f1180ee..0000000000 --- a/.changeset/audit-lock-state-zh-vocabulary-5004.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The metadata Audit panel's lock column now shows Chinese for every lock state it can print. - -`AuditPanel` renders `MetadataAuditEntry.lockState` through -`translateConsoleValue('lock', …)`, but `CONSOLE_VALUE_ZH.lock` held -`draft` / `locked` / `published` / `none` — a draft-status vocabulary, not the -ADR-0010 §3.6 four-state lock. The misalignment was total rather than partial -(objectui#5004): `draft` / `locked` / `published` matched no value `lockState` -can hold; `none`, the only entry that did match, is excluded by the call site's -own guard (an unlocked row renders an em dash) and so was unreachable; and the -three values that actually reach the helper — `no-overlay` / `no-delete` / -`full` — had no entry at all. Hit rate 0/3. A zh-CN admin opening any locked -item's Audit panel read bare English tokens in a column headed 锁状态. - -The table is now the lock vocabulary it claims to be — `禁止编辑` / `禁止删除` / -`完全锁定`, wording tracked to the lock-banner sentences a reader meets on the -same screen — and the three draft-status entries are gone. They were dead in the -measured sense: `translateConsoleValue('lock', …)` has exactly one call site -repo-wide and `CONSOLE_VALUE_ZH` is module-private, so nothing else could read -them. - -Following objectui#4982's `LAYER_SCOPE_ZH`, the keys are bound to their -producer's union — `Record< NonNullable< MetadataAuditEntry['lockState'] >, -string >`. A fifth lock state therefore fails `type-check` naming the label that -is missing, instead of the column silently shipping a raw English value. The -union is this repo's own hand-written one in `@object-ui/data-objectstack`, not a -`@objectstack/spec` enum; whether the spec should own the lock vocabulary is a -separate question and is deliberately not answered here. - -`none` is kept in the record even though the call site never asks for it, so the -key set stays complete over the producer's type rather than tracking a `!==` -guard in another file. Unchanged on purpose: `translateConsoleValue` is still -zh-only, and the em-dash branch for `none` / `null` still renders exactly as -before. diff --git a/.changeset/audit-log-action-retirement.md b/.changeset/audit-log-action-retirement.md deleted file mode 100644 index e5e7088958..0000000000 --- a/.changeset/audit-log-action-retirement.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@object-ui/console": patch -"@object-ui/plugin-detail": patch ---- - -Retire `permission_change`, `export`, and `restore` from the audit-log action filter (`AuditLogPage`'s `ACTION_OPTIONS`) and badge maps (`AuditLogPage` and `HistoryTimeline`'s `ACTION_VARIANT`). These three values never had a writer anywhere on the platform, so the filter always returned zero rows for them and the badges never rendered — a visible product defect (audit surface should be narrow-but-honest, not broad-but-lying). `import`, `login`, and `config_change` are kept: `import` has a real writer (`plugin-auth`'s `admin-import-users.ts`) and is still declared by the server enum and filtered by the `config_changes` list view; `login`/`config_change` gained real writers in objectstack#8144/#8145. diff --git a/.changeset/auth-invitation-status-closed-union-3879.md b/.changeset/auth-invitation-status-closed-union-3879.md deleted file mode 100644 index f150019a76..0000000000 --- a/.changeset/auth-invitation-status-closed-union-3879.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/auth': minor -'@object-ui/app-shell': minor ---- - -`AuthInvitation.status` is the closed four-member union it always documented, enforced at the auth client's wire boundary. - -The field was declared `status: string` while its own doc comment enumerated -`'pending' | 'accepted' | 'rejected' | 'canceled'`, and `createAuthClient` cast -better-auth's `any` responses straight to `AuthInvitation[]` with no check. The -enumeration was therefore advisory: any value a backend stored reached the -console untouched, where the invitations screen coloured it through a -`default: 'secondary'` badge arm and printed it with -`defaultValue: inv.status` — the raw wire string rendered as interface copy, in -all ten packs (objectui#3879). Nobody could trigger it today, because the four -are what better-auth writes; it was the contract that was loose, and a loose -consumer is where an unexpected value would have hidden. - -`AuthInvitationStatus` now carries the union and **binds better-auth's own -`InvitationStatus`** rather than restating it, the way `org-roles.ts` binds the -spec's `BUILTIN_MEMBERSHIP_ROLES` — a member-for-member copy is the state one -upstream release away from drifting silently. The runtime list -(`AUTH_INVITATION_STATUSES`) is derived from a total map keyed by that union, so -a fifth status upstream is a build failure here rather than a gap in the guard. -`isAuthInvitationStatus` is exported alongside, and all four invitation-returning -client methods (`listInvitations`, `listUserInvitations`, `getInvitation`, -`inviteMember`) narrow their wire rows through it. - -Behaviour change, stated because it is one: an invitation whose `status` is -outside the set now **fails loudly** — the call rejects with a message naming the -value it refused and the four it expected — instead of resolving into a badge. -Both call paths already render a rejection with a retry, so the throw lands on a -designed surface. Degrading quietly was the alternative and was deliberately not -taken: a neutral label is how the raw value shipped in the first place, and -dropping the row would delete an invitation from an administrative ledger without -saying so. The console's badge switch is exhaustive over the union now and has no -`default:` arm, so if better-auth ever adds a member the type-check gate stops the -build at the one place a human has to choose a colour. - -Consumers assigning an arbitrary string to `AuthInvitation.status` (a hand-built -fixture, a mock) will need one of the four members. diff --git a/.changeset/badge-declared-hex-fidelity-5141.md b/.changeset/badge-declared-hex-fidelity-5141.md deleted file mode 100644 index 0500378a7a..0000000000 --- a/.changeset/badge-declared-hex-fidelity-5141.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/fields': minor ---- - -fix(fields): render the option colour an author declared as an explicit hex, instead of quantizing it to nine palette families (objectui#5141) - -`options[].color` accepts any hex, but the badge renderer answered a lossy -question with it: `hexToPaletteName` bucketed the value by hue into nine -families (`red`/`orange`/`yellow`/`green`/`blue`/`indigo`/`purple`/`pink`, plus -`gray` below 22% saturation), and `BADGE_COLOR_MAP` held exactly one class set -per family. Two tiers an author declared as distinct therefore rendered -byte-identical: `#2ecc71` ("in progress") and `#1e8449` ("completed") differ by -0.1 degree of hue, both landed in `green`, and both emitted -`bg-green-50 text-green-700 border-green-200`. Pressing the second colour darker -still changed nothing. End users could not tell the two states apart in a list. - -`plugin-gantt` had already settled this class of conflict the other way — -*explicit colorField value (hex or semantic name) — metadata wins* — and -Studio's own option editor paints the author's swatch straight from the raw hex. -Badges were the odd surface out. - -Now an explicitly declared hex is rendered as declared: the soft-pill surface, -label and border are derived from that hex rather than snapped to a family, for -both `appearance: 'badge'` and `appearance: 'dot'`. - -Two properties the family maps gave us for free are kept deliberately: - -- **The design system keeps control of theming.** The derived colours are - published as CSS custom properties and consumed by *static* Tailwind - utilities, so light and dark remain ordinary `dark:` variants - (`.dark\:bg-...:where(.dark, .dark *)` in the built sheet) rather than a - hard-coded inline background that would render identically in dark mode. - Tailwind cannot generate a class for a runtime value, so the custom property — - not the colour — has to be the dynamic part. -- **Contrast is pinned, not just colour identity.** The label is the lightness - along the declared hue nearest the declared one that still clears WCAG AA - (4.5:1) against the surface actually rendered. Authors can and do declare - colours that are unreadable under a label; honoring the declaration must not - turn legibility loose across every list view. Dots are held to 1.9:1 against - the row, the measured floor of the `-500` shades shipped today. - -**What changes for an author relying on the current look:** every select/status -badge whose option colour is declared as a hex — which the renderer's own notes -describe as almost all of them — will render in that declared colour rather than -its palette family's fixed pill. Colours near a family's canonical shade look -much as before; a colour the author picked deliberately *away* from it (a deep -green, a muted red) now looks like what was written, and the pill's depth tracks -the declared lightness. Declarations that are not an explicit hex are untouched: -family names, the semantic value map and the deterministic hash fallback all -resolve exactly as they did, and `getSemanticColorName` still returns family -names, so the Gantt path is unaffected. - -The badge classes exported by `getBadgeColorClasses` are unchanged, so callers -that consume only a class string (the grid's compact card view and group-header -pills, Kanban) keep today's quantized rendering until they adopt the new -`getBadgeHexAppearance` / `getDotHexAppearance` helpers. diff --git a/.changeset/badge-hex-cross-surface-5183.md b/.changeset/badge-hex-cross-surface-5183.md deleted file mode 100644 index c901448d15..0000000000 --- a/.changeset/badge-hex-cross-surface-5183.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-grid': patch -'@object-ui/plugin-kanban': patch ---- - -Grid group headers, compact cards and kanban card badges honour an author-declared option hex, matching the grid cell. - -objectui#5141 taught the grid's cell renderer (`SelectCellRenderer`) to render an -explicitly declared `options[].color` hex as declared instead of quantizing it -onto one of nine palette families. Four badge call sites in `plugin-grid` and -`plugin-kanban` still resolved through `getBadgeColorClasses`, which returns a -class string and therefore cannot carry a runtime colour — Tailwind can only -emit classes it saw in the source at build time. - -The result was worse than the bug it replaced: the same option in the same -object rendered one colour in a desktop grid cell and a different one in the -group header above it or on the kanban card beside it. The pre-#5141 state was -at least uniformly wrong. - -All four sites now resolve exactly as the cell does — prefer -`getBadgeHexAppearance(color)` and use its `className` **and** its `style`, -falling back to `getBadgeColorClasses` for palette-family names, no colour, and -every other declaration. The compact card keeps its pipeline-stage heuristic -when the author declared no colour at all. - -Two carriers were widened so the style can reach the element, because these -badges are not all plain JSX: `GroupRow` takes a `labelColorStyle` alongside -`labelColorClass`, and a kanban card badge takes `colorStyle` alongside -`colorClass` (`KanbanCard.badges[]`). Both additions are optional and additive; -a badge that carries only a class renders exactly as before. The colours ride -CSS custom properties that the class reads, so a class passed without its style -paints against undefined variables — the two halves have to travel together. - -Behaviour is unchanged for every declaration that is not an explicit hex. diff --git a/.changeset/ban-package-self-import.md b/.changeset/ban-package-self-import.md deleted file mode 100644 index d7247c752e..0000000000 --- a/.changeset/ban-package-self-import.md +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- - -Tooling + test-only (objectui#4801). No package may name itself in a module specifier inside -its own `src/`, and a new CI gate — `pnpm check:self-import`, -`scripts/check-package-self-import.mjs` — enforces it. - -A file inside a package that imports the package's own name resolves, legally, through that -package's `exports` map to `dist/`. What does not exist is an ordering: turbo gives -`type-check` `dependsOn: ["^build"]`, the DEPENDENCIES' builds, never the package's own. So on -a cold CI cache the declarations the specifier points at have not been produced yet and the -file fails with `TS2307`. PR #4789's first CI run was red on exactly one line of that shape in -`packages/fields/src`, and no local workflow can see it: every one of them builds before it -type-checks, so a stale `dist/` makes the tree green on any machine that has ever run a build. - -The two remaining sites were converted to relative imports — -`packages/core/src/__benchmarks__/core.bench.ts` and -`packages/components/src/__tests__/snapshot-critical.test.tsx` — and the `paths` entry in -`packages/components/tsconfig.test.json` that existed only to redirect that self-import away -from `dist/` is gone with it. No published behaviour changes: a benchmark file, a snapshot test -and repo tooling, so this declares "no release" rather than a bump. diff --git a/.changeset/bell-linkless-notification-opens-inbox.md b/.changeset/bell-linkless-notification-opens-inbox.md deleted file mode 100644 index cf2adee0e9..0000000000 --- a/.changeset/bell-linkless-notification-opens-inbox.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A notification with no link now opens the full inbox from the bell instead of being consumed silently. - -`InboxPopover`'s notification click handler marked the row read and then, when -nothing resolved a target, returned. A linkless row was therefore spent — the -mark-read is server-authoritative — while the user was taken nowhere and the -popover stayed open on the row it had just greyed out. - -Linkless rows are a routine state, not a corner case: `service-messaging` leaves -`action_url` undefined whenever an emit carries neither a `payload.url` nor a -`source`. Home's action centre already answers that state deliberately by -opening the user-scoped full inbox (objectui#4074); the bell was the unruled -half of one behaviour. It now reuses the drill it already had one screen up, so -the click closes the popover and lands on -`/apps/{host app}/sys_inbox_message?view=mine` — the same page the row came -from, resolved through `resolveHostAppSegment` like every other app-independent -drill in the file (never a bare or empty app segment, which matches no route). - -Also removes the pre-ADR-0030 `source_object`/`source_id` back-compat arm and -the two fields from the `InboxNotification` shape. `mergeInboxRows` — the single -producer of every row both the bell and Home render — mapped neither, and -`sys_inbox_message` declares `action_url` with no source columns to map from, so -the arm was unreachable in the shipped tree. A declared input that no producer -fills, read by a handler that silently does nothing, is a standing invitation to -wire it up; the fields are removed rather than maintained (objectui#5190). diff --git a/.changeset/brave-pandas-invent.md b/.changeset/brave-pandas-invent.md deleted file mode 100644 index d02a2309fa..0000000000 --- a/.changeset/brave-pandas-invent.md +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -Test-only change: adapts the spec parity gates to `@objectstack/spec` 17.0.0 GA ahead of the pin bump — pin-aware coverage for the four `object-*` blocks GA adds, GA-pending exemptions for five keys the renderers already honour, a reasoned exemption for `FormFieldSchema.publicPicker`, and the `record:details` / `record:highlights` gates now reading GA's `unrecognized_keys` refusal instead of rc.6's silent strip. No renderer, no registry `inputs` and no published authoring surface changes; declared as releasing nothing. diff --git a/.changeset/builtin-input-max-length-dual-read-5201.md b/.changeset/builtin-input-max-length-dual-read-5201.md deleted file mode 100644 index 9e9f21113d..0000000000 --- a/.changeset/builtin-input-max-length-dual-read-5201.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The built-in form `input` branch now honours a declared ceiling in both authored spellings. - -The branch spread its leftover field props straight onto the element and never -read the declared ceiling, so one declaration produced two different outcomes. -Measured on `origin/main`, rendering the built-in branch (no `registerAllFields()`) -and dumping the element's `getAttributeNames()` / `getAttribute('maxlength')`: - -| declaration | `maxlength` on the element | effect | -|---|---|---| -| `maxLength: 50` | `"50"` | capped — but only by the coincidence that `maxLength` names a real DOM attribute | -| `max_length: 50` | `null`, plus a stray `max_length="50"` | no cap at all, and invalid HTML | - -Two distinct defects: the missing cap, and an inert attribute on the DOM that -reads like a working cap to whoever greps the file next. - -`max_length` is a live authoring spelling, not a fossil. The registered -`field:*` widgets have dual-read `maxLength ?? max_length` since framework#1878 -§3, all three producers of a form field normalize it (`ObjectForm`, -`sectionFields`, `EmbeddableForm.applyDefaultMaxLengths`) and `@object-ui/types` -declares it on several field types. Every reader in the repo honoured it except -this branch — which is precisely the one serving a hand-written `FormSchema` fed -straight to the renderer, where no producer sits in between to normalize it and -the author is the producer. This is the same mechanism objectui#3439 resolved -for the built-in `textarea` branch. - -The legacy key is destructured off locally rather than added to the shared -`stripRendererOnlyProps` list: that helper feeds every branch -(`checkbox`/`switch`/`select`/`default` all share `domFieldProps`), so extending -it would change what reaches the DOM for widgets this change neither fixes nor -tests. The neighbouring `textarea` branch strips it the same local way. - -Scope, stated because the sibling card resolved more than this one: the ceiling -only. Whether a single-line input should also carry the visible `{n}/{max}` -counter and the announced limit that the `textarea` branch grew in -objectui#3439 is an independent design trade-off that does not follow from that -card's conclusion, and is deliberately left undecided here. diff --git a/.changeset/builtin-textarea-character-count-3439.md b/.changeset/builtin-textarea-character-count-3439.md deleted file mode 100644 index f6fbbd6cca..0000000000 --- a/.changeset/builtin-textarea-character-count-3439.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/fields': patch ---- - -The form renderer's built-in `textarea` branch now honours a declared character cap the same way the registered `field:textarea` widget does. - -One `maxLength` declaration produced two experiences. The registered path has -shipped four things since objectui#3406/#3408/#3417 — the native cap, visible -`{n}/{max}` digits, a description reached through `aria-describedby` so the -limit is announced on focus, and a threshold-gated debounced near-limit notice. -The built-in branch — the path standalone and embedded hosts take, the ones that -call no `registerAllFields()` — shipped a subset of one of them. - -The accessibility half is the half that mattered: a screen-reader user on this -path learned the field's limit only as a validation error AFTER submitting. All -four affordances now render on both of the branch's surfaces (the inline control -and the fullscreen dialog), from the SAME `CharacterCount` component the widget -renders rather than a second copy of it. - -Also fixed, and wider than the visible gap: the branch never READ the cap, it -only spread its leftover field props onto the element. A camelCase `maxLength` -therefore worked by coincidence — it names a real DOM attribute — while the -legacy `max_length` spelling, which the registered widget and all three -producers of a form field have dual-read since framework#1878 §3, landed as a -stray inert `max_length="…"` attribute and capped nothing at all. The branch now -resolves both spellings and keeps the non-attribute spelling off the DOM. - -`CharacterCount` moved from `@object-ui/fields` to `@object-ui/components`, the -package both render paths may import, in the direction and for the reason -objectui#3398 measured for `FullscreenEditor`. It was internal to `fields` (never -exported from that package's barrel), so no published export changed; it is a -new export of `@object-ui/components`. Its copy moved with it onto the same -`fields.textarea.*` keys with byte-identical English defaults, so the ten locale -packs need no edit and provider-less rendering is unchanged. diff --git a/.changeset/bulk-action-dialog-param-values-as-option-record.md b/.changeset/bulk-action-dialog-param-values-as-option-record.md deleted file mode 100644 index 40b3112215..0000000000 --- a/.changeset/bulk-action-dialog-param-values-as-option-record.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-grid': patch ---- - -A bulk action dialog's per-option `visibleWhen` predicates now read the dialog's own in-progress param values. - -The second landing site of the same gap objectui#3765 closed for the -single-record action dialog. A field's per-option `visibleWhen` reaches a bulk -param's control, but the dialog supplied no record to evaluate it against: it -passed no `dependentValues`, so the shared cascading-options evaluator fell -through its chain (`dependentValues ?? formValues ?? data`) to whatever record -the host grid page happened to publish, or to nothing at all. A predicate -written against a SIBLING PARAM — `record.country == 'cn'` on a province option, -next to a `country` param in the same dialog — could therefore never see the -value the user had just entered. Authored cascades were dead on this surface, in -the safe direction: an unresolvable predicate offers the option rather than -hiding it, so nobody was shown a wrongly-narrowed list. - -Per the maintainer's 2026-08-11 ruling (Option B on objectui#3765) the dialog is -a small form, and its in-progress values are that record. The bulk dialog now -passes them as `dependentValues` to the option widgets (`select`, `multiselect`, -`radio`, `checkboxes` — the same allow-list the object form and the single-record -action dialog thread the live record to). The evaluator is unchanged; this is the -supply half that was missing. - -Bulk is the cheap case for that ruling, which is why it needed no separate one: -an action over N selected rows has no single row record for the dialog's values -to displace — the selection was never offered to these predicates, so the -dialog's values are the only record there has ever been. What the supplied record -does displace is the host page's, since it wins the chain outright: a predicate -naming a column the dialog has no param for stays unresolvable, which fails open -— the option is offered, never wrongly hidden. diff --git a/.changeset/bulk-param-data-source-rule-derives-from-core-4815.md b/.changeset/bulk-param-data-source-rule-derives-from-core-4815.md deleted file mode 100644 index 51d0e4936f..0000000000 --- a/.changeset/bulk-param-data-source-rule-derives-from-core-4815.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-grid': patch -'@object-ui/core': patch ---- - -The bulk-action dialog's "this widget needs a DataSource" rule now derives from the shared reference-field family instead of a fourth private copy. - -`packages/plugin-grid/src/components/bulkParamToField.ts` held its own -`DATA_SOURCE_WIDGET_TYPES` — the fourth hand-maintained answer to one question -("which widget has to query records, so it must be handed a DataSource and a -`reference_to`"), and the only one whose member set matched none of the other -three: `lookup` / `master_detail` / `user`, against `@object-ui/core`'s -`EXPANDABLE_FIELD_TYPES` (which also holds `tree`) and the object form's rule -(which adds three widget-hint pickers). Nothing anywhere could detect the drift; -the same shape objectui#4770 and objectui#4790 each closed on another surface. - -It now reads core's set through one predicate, so all three consumers of the rule -— the label prefetch / option source (`isLookupishParam`), the `dataSource` prop -the dialog threads into the widget (`fieldNeedsDataSource`), and the -`reference_to` / `display_field` branch of `bulkParamToField` — cannot drift apart -from each other or from the form again. - -No behaviour change on any reachable path, which is why this is a patch. The one -member the two tables differed on, `tree`, can never be a widget key on this -surface: it is absent from the fields widget map and `mapFieldTypeToFormType` -sends it to `field:lookup`, so a `tree` param arrives at the rule as `lookup` -(pinned). The divergence in the other direction is deliberately preserved: the -form additionally wires `object-ref` / `filter-condition` / `recipient-picker`, -widget hints no object schema can declare and no bulk param produces — absorbing -them would change which widgets receive a DataSource here, which is a behaviour -change and not a convergence. - -The pin is an identity pin, not a membership one: it spies on the `has` of the -Set object core exports, so a member-identical private copy fails it. A -value-equality assertion would have passed against exactly the defect this -change removes. diff --git a/.changeset/cascade-clear-normalize-prefixed-field-type.md b/.changeset/cascade-clear-normalize-prefixed-field-type.md deleted file mode 100644 index 30bb5033a4..0000000000 --- a/.changeset/cascade-clear-normalize-prefixed-field-type.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The form's cascade clear now recognises object-form fields, so a narrowed option list no longer submits a stale value. - -`field:select` and `select` name the SAME field kind: the object-form path -(`mapFieldTypeToFormType`) emits the prefixed widget id, hand-written SDUI -schemas the bare one. The form host's cascade-clear effect (objectui#2284) -compared the RAW type string against the bare-name set, so every option field -coming from an OBJECT schema fell out of the effect entirely. Its controlling -field could change, its option list narrow, and the no-longer-offered value was -never dropped — the form submitted exactly the stale "china + california" pair -the effect exists to prevent. Only genuinely cascading fields were affected -(those carrying a `dependsOn` or a per-option `visibleWhen`); a plain picklist -has nothing to recompute either way. - -The comparison now normalizes the type before the lookup, which is what the -render path a few hundred lines below has done for `isOptionField` since -objectui#3231 — the two readers of "is this an option field?" no longer -disagree about what a `select` is. This half was the one missed then. - -Stated because it is a behavior change and not an equivalent refactor: the -object-form path gains cascade clearing for the FIRST time. A form whose stored -value is genuinely excluded by its chosen parent will now clear that value where -it previously kept it. The narrowing is bounded by the rules already in place -for the bare-name path, both of which the object path now inherits unchanged: a -GATED list (a declared `dependsOn` parent still empty) is treated as unknown -rather than invalid and never deletes anything (objectui#4247), and a field with -no `dependsOn` and no per-option predicate is never recomputed at all. diff --git a/.changeset/cascade-option-allow-table-single-source.md b/.changeset/cascade-option-allow-table-single-source.md deleted file mode 100644 index b47ff78cb0..0000000000 --- a/.changeset/cascade-option-allow-table-single-source.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/fields': patch -'@object-ui/components': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-grid': patch ---- - -The allow-list of option widgets that are fed the live record is now one exported constant, `CASCADE_OPTION_WIDGET_TYPES`, instead of three private copies. - -`select` / `multiselect` / `radio` / `checkboxes` are the widgets whose OFFERED -option set is re-resolved against a record (per-option `visibleWhen`, plus the -`dependsOn` gate), so they are the widgets a surface must thread its live record -to. Three surfaces feed that one evaluator — the object form, the single-record -action dialog and the bulk action dialog — and until now each carried its own -private `new Set([...])` of the same four keys, with a comment in each asking the -next person to change all three together. Nothing could have reported them -drifting: every copy passed its own behavioural tests, and a divergence would -have shown up only as one surface silently disagreeing with another about what -"the record" is. - -The set now lives in `@object-ui/core`, next to `resolveCascadingOptions` — the -evaluator that reads that record — because core is the one package all three -surfaces already depend on, and it is re-exported from `@object-ui/fields` next -to `resolveFormWidgetType`, whose output is the vocabulary the keys are written -in. Both are the same object, pinned by test; each consumer keeps its own -normalization (`normalizeFieldType` in the form, `resolveFormWidgetType` in the -dialogs), which agree on these four members. - -No behaviour changes: the members are identical on all three surfaces, and the -existing pins for each surface still assert the same records reaching the same -widgets. The rationale that was repeated in the three copies — including the -note that the widget-hint picker family (`filter-condition`, `recipient-picker`, -the lookup family) reads a different sibling key off the same channel and is -deliberately NOT in this set — is now stated once, in the constant's own -documentation. Whether the action and bulk dialogs should ever feed those -pickers stays an open question (objectui#4771), unchanged by this convergence. diff --git a/.changeset/changesets-cli-v3-migration-5296.md b/.changeset/changesets-cli-v3-migration-5296.md deleted file mode 100644 index edcb41f784..0000000000 --- a/.changeset/changesets-cli-v3-migration-5296.md +++ /dev/null @@ -1,31 +0,0 @@ ---- ---- - -Release-tooling only, no published package changes (objectui#5296). - -`@changesets/cli` moves from `^2.31.1` to `^3.0.0`, together with the -`.changeset/config.json` changes v3's defaults require — the two halves are -broken apart, and each half is broken silently until the next release, so they -land in one commit. - -- `privatePackages` is now declared explicitly as `{ "version": true, - "tag": false }`. Omitting it used to mean exactly that under v2 and means - `{ "version": false, "tag": false }` under v3, and `version: false` marks - private packages *ignored* rather than merely unbumped. This repo keeps one - private package inside the `fixed` group — `object-ui` - (`packages/vscode-extension`), which ships to the VS Code marketplace instead - of npm — and one pending changeset names it beside four published packages, - which under the new default makes the release plan a rejected "mixed - changeset". Measured: `changeset status` and `changeset version` both exit 1 - on the bump alone, and green PR CI cannot see it, because no PR workflow runs - either command. -- `___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange` - is removed. `@changesets/assemble-release-plan@6` reads it in six places and - `@7` in none, so it is load-bearing under v2 and inert under v3 — removable - only in the commit that lands the bump. -- `$schema` moves from `@changesets/config@3.1.2` to `@changesets/config@4.0.0`. -- `scripts/check-changeset-fixed.mjs` gains the two rules that make the above - mechanical: `privatePackages` must be declared, and a private package in the - `fixed` group must not be paired with `version: false`. Rehearsed against the - real pending stock in a throwaway clone; the v3 release plan is identical to - the v2 one, package for package. diff --git a/.changeset/chart-unprojected-series-dimension.md b/.changeset/chart-unprojected-series-dimension.md deleted file mode 100644 index 0d7e8c6139..0000000000 --- a/.changeset/chart-unprojected-series-dimension.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -A grouped chart whose SECOND dimension was never projected says so instead of -drawing an empty frame. - -"Cannot know refuses loudly" was answered on the first dimension only. -`AdvancedChartImpl`'s `hasNoCategoryKey` (framework#4033) names an unprojected -x-axis dimension rather than drawing a bare axis; the series axis had no -counterpart, so a pivot whose second dimension was absent from the result rows -produced `series: []` and rendered axes, grid, tooltip and legend around zero -marks — indistinguishable, to the author, from "no data matched". - -Such a chart now renders the same explanatory placeholder -(`data-chart-error="no-plottable-series"`) and logs the same diagnostic pair the -category-axis guard logs: the axis it did plot, and the keys its rows actually -carry. - -The three-way distinction is unchanged and pinned: null / empty-string group -values still DRAW (they are real groups with real buckets), a partially -projected group key still draws what projects — mirroring the category axis, -which refuses only when NOT ONE row carries the key — and an ordinary pivot -renders unchanged. The refusal is limited to the families whose marks come from -`series` and nothing else (bar, horizontal-bar, line, area, combo); pie, donut, -funnel, radar and scatter draw from a `value` column with no series declared, so -they are untouched. A caller that computed no series binding at all -(`series === undefined`) is also untouched. diff --git a/.changeset/cli-app-generator-lucide-range-4968.md b/.changeset/cli-app-generator-lucide-range-4968.md deleted file mode 100644 index ca32e4ed0c..0000000000 --- a/.changeset/cli-app-generator-lucide-range-4968.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -The routed temp app's generated manifest now asks for the same `lucide-react` range this repo installs. - -`utils/app-generator.ts` writes the routed variant's `dependencies` with two -quoted third-party ranges, and `lucide-react` had fossilised a minor behind the -22 sibling manifests that declare it: the generated manifest said `^1.29.0` -while the repo had moved to `^1.31.0`. A generated app therefore asked npm for -an icon library older than the one every `@object-ui/*` package it installs -alongside was built against. - -The drift was not silent — `app-generator.test.ts` derives its expectation from -the in-repo range precisely so a bump on one side and not the other fails a -test, and both of its pins were red. What went wrong is that they went red too -late to stop anything: the dependency PR that moved the repo range merged while -those shards were still running, so the failure surfaced on `main` and then on -the merge ref of every unrelated open PR. The range is now caught up; the -reporting hole and the merge-ordering hole are filed separately (objectui#4968). - -The remaining eleven anchored ranges were swept against the same dependabot -batch and are all in sync, so this is the batch's only consumer-side follow-up. -Deriving the value from the workspace instead of quoting it was considered and -rejected: nine of the thirteen anchored ranges quote the repo root manifest, -which is not published with this CLI, so no single derivation can serve the -table and a bespoke one for this one name would leave the class untouched. diff --git a/.changeset/cli-check-known-types-derived-5115.md b/.changeset/cli-check-known-types-derived-5115.md deleted file mode 100644 index 175f8c24ff..0000000000 --- a/.changeset/cli-check-known-types-derived-5115.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -`objectui check`: the known-type list is now derived from the component registry instead of being a hand-written copy, which had drifted in both directions at once. - -The command judged a schema's `type` against a seventeen-entry array typed by -hand into `packages/cli/src/commands/check.ts`. Nothing held that array against -the registry, and measured on `origin/main` @ `8378e9954` it was wrong in both -directions simultaneously: - -- **Two phantoms.** `crud` and `gallery` were on the list and are registered by - nothing. `objectui check` passed `{ "type": "crud" }` in silence while - `SchemaRenderer` painted the OBJUI-001 "Unknown component type" panel for the - very same file — measured, both halves. `CRUDSchema` still has its interface, - zod mirror, validator branch and builder; what it has never had is a - registration. For `gallery`, the registered spelling is `object-gallery`. -- **221 bare keys missing, plus every namespaced spelling.** `object-grid`, - `object-form`, `card`, `div` and `view:grid` were all reported as - `⚠️ Unknown schema type`. False warnings at that volume are not a cosmetic - problem: they train authors to skip the output, which costs the phantom - direction its only reader. - -The list now lives in `packages/cli/src/utils/known-schema-types.ts`, generated -by `node scripts/regenerate-known-schema-types.mjs` from the same -`deriveRegistryKeys` derivation that judges documentation snippets, and held to -it by a bidirectional pin in -`scripts/__tests__/known-schema-types-derivation-5115.test.ts` — a key the -registry has and the list lacks fails, and so does a key the list has and the -registry lacks. Bare and namespaced spellings are both carried, because -`register('grid', C, { namespace: 'view' })` really does store both. - -A runtime lookup through `ComponentRegistry` was measured and rejected: eleven -of the fifteen genuinely-registered entries come from plugin packages the CLI -does not depend on, and a published CLI runs against a user project whose plugin -set this repository cannot know either way. - -**Behaviour change, in both directions.** `{ "type": "crud" }` and -`{ "type": "gallery" }` now produce the `Unknown schema type` warning they -always should have, and a large number of real component types stop producing -one. The warning remains advisory — it never changes the command's exit code, -which is still driven only by files that fail to parse — so no run that passed -before fails now. - -`check()` additionally takes the directory to scan as an optional argument -(defaulting, as before, to `process.cwd()`), so the behaviour can be tested -against a fixture tree. diff --git a/.changeset/cli-check-reads-json-as-jsonc.md b/.changeset/cli-check-reads-json-as-jsonc.md deleted file mode 100644 index 0f432ac37d..0000000000 --- a/.changeset/cli-check-reads-json-as-jsonc.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -`objectui check` reads `.json` as JSONC, so a `tsconfig.json` no longer fails the run. - -`check` globbed every `**/*.{json,yaml,yml}` and handed each `.json` straight to -`JSON.parse`. A throw there is the only thing that increments the error count, and -a non-zero error count is the only thing that calls `process.exit(1)` — so a `//` -comment or a trailing comma, which is how TypeScript documents `tsconfig.json`, -was reported as a malformed file and **failed the command**. Every TypeScript -project hit this: `objectui check` exited 1 for anyone who ran it, and at this -repository's own root it reported 64 errors, all of them `tsconfig*.json` -(objectui#5237). - -`.json` on disk means JSONC in practice — `tsconfig.json`, `.eslintrc.json`, -`devcontainer.json` and VS Code's own settings are all written that way — so the -file is now read with `jsonc-parser`, which permits comments and trailing commas. -No new package enters what users install: `jsonc-parser` is already a runtime -dependency of `@object-ui/app-shell`, it is already at the version the lockfile -resolves, and it declares no dependencies of its own. - -The reader is a real JSONC parser and **not** a comment-stripping regex, because -a `//` inside a string value — a URL, say — is not a comment, and a stripper that -cannot tell the difference corrupts valid files instead of reading them. - -Genuinely malformed JSON still errors and still exits 1. That needed saying in -code as well as in tests: `jsonc-parser`'s reader is error-tolerant and returns a -best-effort value rather than throwing, so the command consults its reported-error -array instead of inferring success from the absence of a throw. Error output now -names the reason and the line and column. - -The unknown-schema-type warning arm is deliberately untouched: it still warns, and -it still does not affect the exit code. Files that previously died at the parse -step now reach it, so a JSONC file carrying an unrecognised root `type` warns -where it used to error — the verdict and the exit-code neutrality are unchanged. diff --git a/.changeset/cli-directory-argument-5037.md b/.changeset/cli-directory-argument-5037.md deleted file mode 100644 index bab2ef004d..0000000000 --- a/.changeset/cli-directory-argument-5037.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -`dev`, `serve` and `build` accept the documented directory argument from anywhere, and refuse a non-project directory in plain language. - -`content/docs/utilities/cli.mdx` has recorded the positional argument as "Path -to JSON/YAML schema file or `pages/` directory" and printed `objectui dev -pages/` as the file-system-routing example. That promise had never actually been -parsed. Detection's first step required `statSync(...).isFile()`, so a directory -argument fell straight through it; the one spelling that worked — `objectui dev -pages/` from inside the project — worked by coincidence of position, caught by -the working-directory fallback rather than read as an argument. Every pathful -spelling reached single-schema mode and handed a directory to `readFileSync`: - -``` -$ objectui dev my-app/pages -Error: Invalid schema file: EISDIR: illegal operation on a directory, read -``` - -A directory argument now resolves through file-system routing in the shared -resolution step the three commands were centralized on (objectui#4923), in -either of the two shapes a user can mean: the directory **is** a `pages` -directory, or it **contains** one. Both produce the same routed answer as naming -the app config beside them — same project root, same routes, same app config — -so `objectui dev my-app/pages`, `objectui dev my-app` and `objectui dev -my-app/app.json` agree, from any working directory, across all three commands. -The limb lives in the shared resolver, not in three command branches. - -The remaining directory-shaped miss is now diagnosed instead of leaking a -`readFileSync` errno: a directory that is neither shape is refused by name, -saying what would have been accepted. The refusal sits **after** the -working-directory fallback, so nothing that resolves today stops resolving: this -change accepts strictly more than before and rejects nothing that worked. diff --git a/.changeset/cli-project-root-parity-4923.md b/.changeset/cli-project-root-parity-4923.md deleted file mode 100644 index 6d07f58ac4..0000000000 --- a/.changeset/cli-project-root-parity-4923.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -`objectui serve` and `objectui build` now locate the project the way `dev` does, instead of looking only in the current directory. - -For a project with an app config and a `pages/` directory beside it, the three -commands answered one invocation two different ways. `dev` anchored on the -schema argument — `dirname()` is the project root, `pages/` beside it -means file-system routing, the named file is the app config. `serve` and `build` -looked for a `pages/` directory in the current working directory and nowhere -else, so from any directory above the project they fell through to single-schema -mode and handed the app config to the renderer as if it were a page. - -Measured on the reported fixture (`/app.json` + `/pages/index.json`, -invoked from the directory above): `dev` reported the project and one route, -`serve` reported `Loading schema: /app.json`, and `build` did the same and -**exited 0** — the emitted bundle embedded the app config as the page schema and -contained no page from `pages/` at all. A wrong artifact, produced silently. - -The detection now lives in one helper the three commands share -(`utils/project-source.ts`), resolving in a fixed order: a `pages/` directory -beside the schema argument, else one under the current directory, else -single-schema mode. `serve` and `build` also pass the resolved app config to the -routed app generator, which they never did, so a routed project keeps its layout -under all three commands. - -A lone schema file with no `pages/` beside it is unchanged — that is the -fallback, and it is still a supported way to run. diff --git a/.changeset/cli-serve-no-open-4924.md b/.changeset/cli-serve-no-open-4924.md deleted file mode 100644 index e0f43137f9..0000000000 --- a/.changeset/cli-serve-no-open-4924.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -`objectui serve` gains `--no-open`, matching the flag `objectui dev` already ships, and no -longer prints a bare `Error: spawn xdg-open ENOENT` stack after its success banner in a -headless environment. - -**`--no-open`.** `serve` hardcoded Vite's `open: true` and its only options were -`--port`/`--host` — `dev` already had `--no-open` (`options.open !== false`), so the same -invocation behaved inconsistently across the two commands. `serve` now threads the same -flag the same way; the default is unchanged — with the flag omitted, `serve` still opens a -browser exactly as it always has (objectui#4924). - -**The headless spawn failure.** Vite's own browser-open step already catches a failed -`open(url)`, but reports it via `logger.error(err.stack || err.message)` with no -`{ timestamp: true }`, so the default logger prints the bare Node `ChildProcess` stack with -no `[vite]` prefix and no context — and because that promise chain is fire-and-forget from -`server.listen()`, it lands *after* the "✓ Server started successfully!" banner, reading -like a crash even though the server is fine. There's nothing in `serve.ts` to try/catch — -the error never leaves Vite. `serve` now supplies a `customLogger` that wraps Vite's default -logger and replaces exactly that message with a short, contextual line naming the missing -opener binary (`(could not open the browser automatically — 'xdg-open' is not available in -this environment; open the URL above manually)`); every other log call — including -unrelated errors — passes through unchanged. - -Sibling card objectui#4923 (project-root detection on the same command) is intentionally -untouched here; it is a separate defect with a separate PR. diff --git a/.changeset/cli-workspace-alias-derivation-3890.md b/.changeset/cli-workspace-alias-derivation-3890.md deleted file mode 100644 index 13c0d382f9..0000000000 --- a/.changeset/cli-workspace-alias-derivation-3890.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/cli': patch ---- - -Inside a pnpm workspace, `objectui dev` / `serve` / `build` now resolve every platform package -from workspace source (objectui#3890). - -The temp app these commands generate installs nothing inside a workspace — it resolves by -hoisting, and the repo root declares no `@object-ui/*` — so a Vite alias table is the only thing -that resolves a platform package there. That table was a hand-kept list of eleven names in -`dev`, which is not a list of what the app imports but of what it imports *transitively*: -measured on the reported commit, the generated entry closes over 21 packages, ten were unlisted, -and every module whose transform hit one of them answered 500 with a blank page behind it. Vite's -dependency scan named only four of the ten, because a scan stops at the first layer it cannot -resolve. - -The table is now derived from `pnpm-workspace.yaml` — every scoped workspace package that exposes -a source barrel, targeting its `src` directory — and a test reconciles it against the manifest so -it cannot drift again. `serve` and `build` had no workspace branch at all (no aliases, and an -unconditional `npm install` against a manifest that is empty here); all three commands now share -one helper. The `lucide-react` entry moved from a resolved entry file to the package root, so -subpath imports of it stop being rewritten into a path that cannot exist. - -Measured with the reported repro, from the repo root: 8 of the first 400 modules a browser walk -reaches answered 500 before, 0 of 2498 after, and the page renders its schema instead of nothing. diff --git a/.changeset/clone-as-override-returns-mutable-5257.md b/.changeset/clone-as-override-returns-mutable-5257.md deleted file mode 100644 index 495193d44c..0000000000 --- a/.changeset/clone-as-override-returns-mutable-5257.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@object-ui/core': patch ---- - -`cloneAsOverride()` now returns `DeepMutable`, so a Tenant/User override draft type-checks as the mutable value it has always been. - -`cloneAsOverride(view: T): T` handed the input type straight back. Cloning a -`SystemView` — which is `DeepReadonly` plus the marker symbol — therefore -returned something still typed deep-readonly, even though the implementation has -always produced a plain mutable object (`structuredClone`, or a JSON round-trip -fallback) and deliberately drops the marker. The declaration was simply wrong -about its own value, and the documented override flow was the thing that broke: - -```ts -const draft = cloneAsOverride(userListView) -draft.columns.push({ name: 'name' }) // TS2339 before this change -``` - -That is `packages/core/README.md`'s override example, and it failed to compile -against the built types — measured by the doc-snippet compile gate, which reads -`dist/*.d.ts` rather than source. The neighbouring line one block up, -`userListView.columns.push(...) // ❌ TypeError (strict mode)`, is the opposite -demonstration and correctly still fails; only the draft line changes colour. - -The fix adds `DeepMutable`, the inverse of the `DeepReadonly` that -`SystemView` is built from, and returns it. Per the maintainer's 2026-08-19 -ruling (option A on objectui#5257), the alternatives were rejected by name: -teaching a cast in the README is the lenient-consumer pattern the contract rules -out, and declaring the block a documentation fragment hides a real signature -defect behind the fragment marker. - -Why this is a patch and not a break: the return type relaxes TOWARD what the -runtime already does, never away from it. A caller gains permission to mutate; -nobody loses one. `DeepMutable` stays assignable everywhere `DeepReadonly` -or `SystemView` was expected, so a caller who fed a draft back into a -deep-readonly position still compiles — both directions are pinned as type-level -cases in `freeze-schema.types.test.ts`. A repo-wide sweep found no call site at -all outside the README, so nothing in this workspace needed changing. - -Two limits of `DeepMutable`, stated rather than discovered later. It is -symmetric with `DeepReadonly` arm for arm, which means it inherits the same -tuple behaviour: a tuple widens to an array, exactly as `DeepReadonly` widens it -in the other direction. And it does not remove the `SYSTEM_VIEW_MARKER` key — -the clone never carries the symbol at runtime, but the key is declared optional, -so keeping it states "may be absent", which is true. Excluding it would require -a non-homomorphic mapped type that drops the `?` modifier from every other -property and turns optional keys required — a strictly worse type traded for -removing a key that already reads as optional. diff --git a/.changeset/cloud-connection-bind-error-message-5028.md b/.changeset/cloud-connection-bind-error-message-5028.md deleted file mode 100644 index 5d93f2e78c..0000000000 --- a/.changeset/cloud-connection-bind-error-message-5028.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -`CloudConnectionPanel` prefers `error.message` over `error.code` on a bind-poll failure, matching the precedence the same file already applies everywhere else. - -`poll()`'s terminal branch read `body?.error?.code ?? t('cloudConnection.errors.bindFailed')`, -so a failure body that carried a human-readable sentence beside its machine code -was still rendered as the code. Fifty lines above, this file's own `getJson` -helper already reads `body?.error?.message ?? body?.error?.code ?? body?.error`; -this brings the one call site that diverged into line with it (objectui#5028). -The `code` arm is kept as the fallback — a producer still short of -`ApiErrorSchema`'s required `message` shows its code rather than nothing — and -the chain still ends at the translated string. - -Narrower than the filed card, because verification moved the boundary. The card -expected this line to be what displays a device-authorization failure -(`expired_token`, `access_denied`) after objectstack#9267 / PR #9369 added -`error.message` to that envelope. It is not: `/bind/poll` serves the terminal -device-code failure with HTTP **400** (before and after that PR), and `getJson` -throws on any non-2xx whose body is not `success: true`, so the string a user -reads there was always picked by `getJson`'s already-correct precedence. The -upstream message reached the UI the moment it merged, with nothing owed here. - -What the changed line genuinely governs is the 2xx path: `/bind/poll` forwards -the control plane's own `/bind` answer verbatim, with the control plane's status. -A 200 that says `success: false` — an envelope violation of the kind -objectstack#9364 is still counting — is handed back by `getJson` and lands in -this branch, and that is where a code was being shown instead of the sentence -next to it. Four cases pin the surface end to end, including a control that -records where the device-authorization text actually comes from, so the next -reader is not sent looking for it in `poll()`. diff --git a/.changeset/color-variant-picker-radiogroup-name.md b/.changeset/color-variant-picker-radiogroup-name.md deleted file mode 100644 index 7be0d06d29..0000000000 --- a/.changeset/color-variant-picker-radiogroup-name.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The designer's colour swatch rows now announce WHAT they colour, and the Dashboard widget inspector's "Color Variant" label no longer points at nothing. - -`ColorVariantPicker` renders its swatches inside a `div role="radiogroup"`. Each -swatch was named by its colour ("Blue", "Success"); the group was named by -nothing at all, so focus entering it announced an anonymous radiogroup — eight -colours with no statement of which field they set — while the visible label -beside it belonged to no one. - -`DashboardWidgetInspector` had the worse half of it. Its `Field` wrapper renders -`Label htmlFor={id}` and expects the wrapped control to carry that id; the picker -accepted no id, so `htmlFor="widget-color"` was a DANGLING IDREF — an association -tooling reports as closed while it resolves to no element, which is why a static -"every label has a `for`" check saw nothing wrong (objectui#4010). - -A `for` cannot fix this: `role="radiogroup"` is a container, not a labelable -element, so a `for` aimed at it is inert HTML that names nobody. The picker now -takes its name through ARIA, required at the type level and singular — exactly -one of `ariaLabelledBy` or `ariaLabel`, so an unnamed colour group no longer -compiles (the contract `InspectorComboField` got in objectui#3997): - -- **Dashboard widget inspector** — the "Color Variant" label publishes - `widget-color-label` and drops its `for`; the group answers by IDREF. The - visible text IS the accessible name, in every locale, with no second string to - translate. -- **Page block properties panel** — the "Color" label never carried a `for`, so - nothing dangled there; the group was simply anonymous. Same repair, with the - id minted per instance so two colour rows cannot collide. -- **Generic `color-picker` widget** — its host writes `Label htmlFor` before it - can know whether this field renders a swatch group or a labelable - `input type="color"`, so the group carries its own name, taken from the host's - own label source. - -This is the WAI-ARIA group pattern this repo already applies to group-labelled -field widgets (objectui#3961 → #3990), applied to the three surfaces that were -still outside it. diff --git a/.changeset/combo-drill-doc-truth.md b/.changeset/combo-drill-doc-truth.md deleted file mode 100644 index d7c0d22f48..0000000000 --- a/.changeset/combo-drill-doc-truth.md +++ /dev/null @@ -1,14 +0,0 @@ ---- ---- - -Test-and-docs only: corrects `AdvancedChartImpl`'s `onChartClick` doc comment to what is -actually wired (bar/horizontal-bar/line/area/pie/donut/funnel/scatter/treemap/sankey; the -comment previously listed scatter/treemap/sankey as no-ops though each already has a wired -click handler) and names the derived-family trap — a series' own `type` disagreeing with -the chart's family silently turns the whole chart into a `combo`, which has no click -wiring at all, so an author changing one series' mark can lose drill on the entire chart. -Pins that combo's current no-click-props state with a positive-control test (an -identically-shaped click on a plain bar chart still fires `onChartClick`). - -No renderer behaviour changes — `ComposedChart` still receives no click props. Whether -combo should drill is left open (objectui#4692). diff --git a/.changeset/combo-mark-drill.md b/.changeset/combo-mark-drill.md deleted file mode 100644 index 5feb5feaea..0000000000 --- a/.changeset/combo-mark-drill.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/plugin-charts': patch ---- - -Combo charts drill from their marks - -objectui#4692, ruled Option B. `AdvancedChartImpl` built `cartesianClickProps` once and -applied it to exactly one element — the final cartesian `ChartComponent`. The `combo` -branch returns earlier, from its own `ComposedChart`, which was rendered with `data` and -no click props at all, so a combo chart fired `onChartClick` never: not on a mark, not on -the axis. Its marks are the same `Bar` / `Line` / `Area` components the drillable branch -renders. - -The trap that made this worth fixing rather than documenting is that the family is -**derived**, not only authored: `effectiveChartFamily` resolves a chart to `combo` -whenever its series declare different families (objectui#2945), so adding `type: 'line'` -to one series of a drillable bar chart silently turned that chart's drill-through off — -nothing in the authored spec said drill had been touched, and nothing errored. - -A combo's `Bar` / `Line` / `Area` marks now emit `{ category, categoryId, series, value }` -with the same semantics the plain cartesian branch gives, reusing the item-level -series-identity machinery from objectui#4672 / objectui#4682: the mark handler records the -series it was rendered with, the chart-level handler composes the one event, so a gesture -still produces exactly one `onChartClick`. Retyping one series now changes that series' -mark and nothing else. - -**Only the marks drill.** A click on a combo's plot surface or axis stays silent, where -the plain cartesian branch falls back to its axis-level answer. A combo plots several -measures on one plot, so a surface click there has no single series to report and the -fallback would have to invent one — the same reasoning objectui#4672's ruling gave the -pivoted case. Combo also carries no chart-wide pointer cursor for that reason; the -affordance sits on the marks that answer. - -Radar is now the one cartesian-adjacent family with no click wiring. The `onChartClick` -doc comment, corrected in objectui#4705 to say combo was a no-op, states the new rule and -its one deliberate exception. diff --git a/.changeset/component-input-type-union-arms.md b/.changeset/component-input-type-union-arms.md deleted file mode 100644 index 07286bf5dd..0000000000 --- a/.changeset/component-input-type-union-arms.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/core': minor -'@object-ui/sdui-parser': minor -'@object-ui/components': minor -'@object-ui/plugin-detail': minor ---- - -`ComponentInput.type` can declare a UNION, so a block stops warning about legal -writes its own description recommends - -A registration's `type` was one coarse control kind, while a good number of spec -keys accept more than one shape. A declaration therefore had to pick an arm, and -the repo's own manifest gate then reported `type-mismatch` on the other arm's -legal values. Four of the five measured cases were the loud shape: the input's -`description` teaches the author to write an inline translation map -(`{ en, "zh-CN", … }`) while the same input's `type: 'string'` made -`sdui-parser`'s `checkType` warn about exactly that map — one platform authority -contradicting itself on the write it had just recommended. Because these land at -warning severity the page still compiled and rendered; the cost is that noise on -correct authoring trains authors, AI authors included, to dismiss the -`unknown-prop` and `type-mismatch` reports that are real. - -`type` now accepts an ARRAY of coarse kinds as well as a single one (maintainer -ruling on objectui#3832, direction (a)), and a value passes the coarse check when -ANY declared arm accepts it. Both declaration sites in `@object-ui/types` move -together with the registry's own copy in `@object-ui/core`, and -`ComponentInputSchema` enforces the same widening — a non-empty array of -DISTINCT kinds, so an empty arm list or a repeated arm is refused at authoring -time rather than normalized behind the author's back. - -Five declarations now spell their real contract, and the `type-mismatch` warning -on each of these legal writes is gone: - -- `page:header.title`, `page:header.subtitle`, `page:card.title` — - string **or** inline translation map (the spec's union, measured against - `ComponentPropsMap` at the pinned rc.6; the renderers resolve both through - `pickLocalized`); -- `record:alert.title`, `record:alert.body` — the same two shapes, justified - against the RENDERER since the pinned spec carries no `record:alert` props - schema; -- `element:text_input.defaultValue` — `string | number`, the spec's union, - which had been narrowed to `'string'` with the number arm named only in prose. - -**Backward compatible, and measured as such.** The single-kind form stays valid -and is still the canonical spelling for a one-arm key: it validates identically -(the diagnostics for one arm, `invalid-enum` and its `error` severity included, -are byte-identical), and `manifestFromConfigs` collapses a one-element array back -to the bare string, so every entry already in a published `sdui.manifest.json` -serializes unchanged and arrays appear only where a union was really declared. -The JSX authoring surface follows in the same step — `generateDts` emits a -TypeScript union for a union input, so the `.d.ts` an author type-checks against -accepts exactly what the gate accepts. - -A union widens what is legal; it does not switch the check off. A value matching -NO declared arm is still reported, a multi-arm mismatch reports at its strictest -arm's severity (`error` when an `enum` arm is present, so an enum's closed list -does not become dismissible by having a second arm added next to it), and arms -are meant to match the contract rather than relax the gate: -`element:text_input.defaultValue` deliberately gains no `object` arm because the -spec rejects a map there, and `element:record_picker.emptyText` keeps its single -`'string'` arm because that renderer drops the map form (objectui#4163) — an arm -the renderer never honours would advertise a shape that cannot reach the screen. diff --git a/.changeset/config-panel-footer-i18n.md b/.changeset/config-panel-footer-i18n.md deleted file mode 100644 index 7e8ae961c6..0000000000 --- a/.changeset/config-panel-footer-i18n.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/i18n': patch ---- - -The config panel footer translates: `ConfigPanelRenderer`'s Save / Discard labels come from the locale pack. - -`saveLabel` and `discardLabel` carried the English literals `'Save'` and -`'Discard'` as parameter defaults, and no caller in the repo passes either prop, -so the sticky footer that appears the moment a config draft is dirty stayed -English in every locale — inside panels whose every other string had already -been routed through `t()`. The fix is in the renderer rather than per-caller: -the footer is the renderer's own chrome, so a caller-side fix would translate -one panel's footer and leave the next host's English. - -Both labels now resolve through `createSafeTranslation` — the mechanism this -package already uses for its built-in copy in `form.tsx`, -`fullscreen-editor.tsx`, `data-table.tsx` and friends. An explicitly passed -`saveLabel` / `discardLabel` still wins, unchanged and untranslated. - -`common.save` is reused rather than twinned: it already ships `Save` in all ten -packs and is what the console's other save buttons read. `common.discard` is -new, because the packs carried no shared spelling of the word — the three that -existed are each scoped to one surface (`form.discard`, -`console.settingsView.discard`, `console.objectView.discard`) and the last of -them diverges from the other two in zh/ko/fr. Its ten values are the majority -spelling, byte-identical to `form.discard` and `console.settingsView.discard`. - -Both English defaults are byte-identical to the literals they replace, so a -host that mounts no `I18nProvider` renders exactly what it did before. diff --git a/.changeset/console-app-list-route-comment-truth.md b/.changeset/console-app-list-route-comment-truth.md deleted file mode 100644 index 506b11e327..0000000000 --- a/.changeset/console-app-list-route-comment-truth.md +++ /dev/null @@ -1,17 +0,0 @@ ---- ---- - -Comments-only truth correction: six comments in the console app surface named the app -LIST endpoint with a plural-collection spelling that no request in this repo constructs -and that no framework route ledger declares as a row of its own. All six now name what -the console actually calls — the generic metadata list route `GET /api/v1/meta/:type`, -requested with the singular type segment `app` (`MetadataProvider` → -`client.meta.getItems('app')`) — and keep the per-session filtering claim, which -verification confirmed: `filterAppForUser` in `packages/rest/src/rest-server.ts` gates the -list inside that `:type` handler once the type segment resolves to `app`. Three of the six -are test-file rationale headers; every assertion is untouched, and one of them -(`appAccessProbe.test.ts`) already pinned the singular address in its own expectation, -which is what made the header's spelling demonstrably wrong. - -No behaviour changes: each touched file transpiles byte-identically with comments stripped -(objectui#4887). diff --git a/.changeset/console-index-fallback-matches-index-schema.md b/.changeset/console-index-fallback-matches-index-schema.md deleted file mode 100644 index cb3cd41dbf..0000000000 --- a/.changeset/console-index-fallback-matches-index-schema.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The console's embedded index editor no longer offers controls for keys the spec removed. - -`index` is an embedded-only sub-type with no metadata type of its own, so `/meta` has no -slot to publish a schema for it and `EmbeddedItemEditor` ships a hand-copied one in -`FALLBACK_SCHEMAS.index`. That copy had drifted from `IndexSchema` -(objectstack-ai/objectstack#5247), and `@objectstack/spec` 17.0.0 turned the drift from a -silent bug into a hard failure: - -- **`type` ("Algorithm")** and **`partial`** were retired in spec 17.0.0 - (objectstack-ai/objectstack#5248, ADR-0049 enforce-or-remove) and are `retiredKey` - tombstones today — `IndexSchema` rejects them at any value, so every option of the - Algorithm select produced a 422 on the parent object save. Its `brin` option was never - in the spec's enum to begin with. -- **`where` ("Partial-index predicate")** was never a declared key — the spec's spelling - was `partial`, itself now retired — so an administrator's predicate was silently - stripped on every save. - -Both controls are removed. An index method is the driver/dialect's choice and a partial -index is built at the database layer by a runtime migration, so neither is a -declaration-surface concern. - -`unique` is converged onto the ADR-0120 scope union: the console can now author -`'organization'` (one holder per organization, NULL-safe) and `'global'`, instead of only -emitting the deprecated bare `true` that protocol 18 rejects. Indexes already carrying the -boolean keep rendering their existing control and are saved unchanged. diff --git a/.changeset/console-objectstack-spec-dist-hook.md b/.changeset/console-objectstack-spec-dist-hook.md deleted file mode 100644 index 7191fe0138..0000000000 --- a/.changeset/console-objectstack-spec-dist-hook.md +++ /dev/null @@ -1,24 +0,0 @@ ---- ---- - -Build tooling only (objectui#4854). `apps/console/vite.config.ts` now honours -`OBJECTSTACK_SPEC_DIST`, the spec twin of the existing `OBJECTSTACK_CLIENT_DIST` -hook, so a framework build can bundle the console against its own -`@objectstack/spec` instead of the last published one. Mechanism ruled on -objectstack#8134; the framework half (`scripts/build-console.sh`) lands after an -objectui SHA carrying this hook is pinned. - -No release: nothing under a published package's `src/` changed, and with the -variable unset the console's config is byte-for-byte the build it was before — -the alias table, the pre-bundle list, the `vendor-objectstack` chunk test and the -dev server's `fs.allow` all keep their baseline values. - -The client hook is one prefix alias, which is only safe because -`@objectstack/client` exports a single entry. `@objectstack/spec` publishes an -18-entry exports map that redirects every subpath into `dist/`, so the injection -is derived from the override's own map — one alias per entry, subpaths ahead of -the bare specifier — rather than from a hand-written rule. Every failure mode -(path absent, not the spec package, an exports entry naming a file the built -package does not contain, a wildcard pattern) throws with the offending value -named: a lenient fallback to the installed spec would silently rebuild the exact -skew the hook exists to end. diff --git a/.changeset/core-plugins-tooling-artifacts-out-of-dist-4836.md b/.changeset/core-plugins-tooling-artifacts-out-of-dist-4836.md deleted file mode 100644 index bf75dd297e..0000000000 --- a/.changeset/core-plugins-tooling-artifacts-out-of-dist-4836.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-designer': patch -'@object-ui/plugin-grid': patch -'@object-ui/plugin-view': patch ---- - -Four packages stop publishing tooling material in their `dist/` - -Each of these packages spelled its build exclusions as `*.test.*`, while this repo's tooling convention is a directory one — `__tests__` / `__mocks__` / `__benchmarks__`, exactly as `TOOLING_FILE` in `scripts/check-phantom-dependencies.mjs` spells it. Any tooling file whose *name* is not `*.test.*` therefore stayed in the emit program and shipped in the tarball. This is the same shape and the same cause as objectui#4006, which fixed `@object-ui/fields` and `@object-ui/plugin-editor` by the filename criterion and so did not reach these four. - -Measured by building each package from a cleared `dist/` on both sides of the change. Nine files disappear, none appears, and every surviving file is untouched — the totals move by exactly the count removed: - -| package | `dist/` files | removed | -| --- | --- | --- | -| `@object-ui/core` | 176 to 174 | `dist/__benchmarks__/core.bench.js`, `core.bench.d.ts` | -| `@object-ui/plugin-designer` | 70 to 66 | `dist/__tests__/__mocks__/plugin-form.d.ts`, `plugin-grid.d.ts`, and both `.d.ts.map` | -| `@object-ui/plugin-grid` | 62 to 60 | `dist/__tests__/explainDouble.d.ts` and its `.d.ts.map` | -| `@object-ui/plugin-view` | 13 to 12 | `dist/__tests__/explainDouble.d.ts` | - -Only `@object-ui/core`'s had runtime weight. The other eight are declarations nothing resolves, but `core.bench.js` is a real emitted module whose first import is `import { bench, describe } from 'vitest'` — a runtime import of a package a consumer never installs, since `vitest` is a devDependency of `@object-ui/core` and devDependencies are not installed transitively. Nothing resolves it today either (it is not in the `exports` map), so no consumer breaks in either direction; this is the tarball shedding files nothing reached. - -No type coverage leaves with the emit. The three plugins' helper and mock files are already program inputs of the `tsconfig.test.json` that each package's `type-check` chains, reached through the imports in the suites beside them — `tsc --listFiles` names all four files on both sides of the change. `core.bench.ts` had no such edge, since nothing imports a benchmark, so it is now named explicitly in `packages/core/tsconfig.test.json`. That move was deliberate rather than forced: `scripts/check-type-check-coverage.mjs` enumerates `*.test.ts(x)` only, so a benchmark that no program reads is invisible to it, and dropping the coverage silently would have been the "coverage that was right by accident" objectui#4006 recorded. Verified by appending a provably-false annotation to the benchmark, which turns `tsc -p packages/core/tsconfig.test.json` red at exit 2. diff --git a/.changeset/core-readme-registry-singleton-5258.md b/.changeset/core-readme-registry-singleton-5258.md deleted file mode 100644 index 8a5f5c2936..0000000000 --- a/.changeset/core-readme-registry-singleton-5258.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@object-ui/core': patch ---- - -Fix `packages/core/README.md`'s Component Registry example, which taught -`new ComponentRegistry()` against an exported singleton **instance**, not a -class — the built `packages/core/dist/registry/Registry.d.ts` declares -`export declare const ComponentRegistry: Registry`, so the snippet did -not compile (`TS2351: This expression is not constructable`, measured by the -objectui#5138 doc-snippet type gate). A reader who copied it got a compile -error; if `new ComponentRegistry()` had compiled it would have produced a -second, empty registry nothing renders from, the more expensive half of the -mistake. - -The snippet now calls `ComponentRegistry.register(...)` / -`ComponentRegistry.get(...)` directly on the singleton, with one line stating -it is the process-level shared instance `SchemaRenderer` resolves every -`type` against — the same wording `packages/components/README.md` was given -in objectui#5160, kept consistent across both READMEs. Readers who want their -own isolated registry still have `Registry` itself, separately exported as a -real class. - -`scripts/check-doc-snippet-types.mjs`'s `UNGATED_DOCS` entry for -`packages/core/README.md` is updated to match: `TS2351x1` is dropped from its -reason text now that the diagnostic is gone. The entry is not deleted — the -document's remaining `TS2339x2` pair (a different, pre-existing defect) is -out of scope for this change; it's tracked as objectui#5257. diff --git a/.changeset/create-plugin-jest-dom-range-4968.md b/.changeset/create-plugin-jest-dom-range-4968.md deleted file mode 100644 index 957db8e70f..0000000000 --- a/.changeset/create-plugin-jest-dom-range-4968.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/create-plugin': patch ---- - -A scaffolded plugin's generated manifest now asks for the same `@testing-library/jest-dom` range this repo installs. - -`src/templates.ts`'s `DEV_DEPENDENCIES` had fossilised one patch behind the repo -root: the template said `^7.0.0` while the root manifest had moved to `^7.0.1`. -`templates.test.ts`'s anchor rule caught it and was red on `main`. - -Same defect class, same day and same dependabot wave as the `lucide-react` drift -in `@object-ui/cli`'s app generator, so both templates move together here — which -is how the previous occurrence of this incident was handled too (objectui#4098 / -PR objectui#4099 moved these same two templates in one PR). This one came from -the dev-dependencies group bump rather than the single-package bump, and it was -found only because the two ratchets live in different packages: the anchor rule -throws on its first mismatch, so nothing reports the second template until the -first is green. - -The remaining seven anchored ranges in this template were swept against the same -wave and are all in sync. diff --git a/.changeset/dashboard-components-rekey-5064.md b/.changeset/dashboard-components-rekey-5064.md deleted file mode 100644 index cfb8f1aa28..0000000000 --- a/.changeset/dashboard-components-rekey-5064.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-dashboard': minor ---- - -BREAKING: `dashboardComponents` is re-keyed from 11 PascalCase component class -names to the 8 schema `type` keys the package actually registers -(`dashboard`, `metric`, `metric-card`, `object-metric`, `pivot`, -`object-pivot`, `dashboard-grid`, `object-data-table`), aligning the map with -its four sibling `*Components` maps (objectui#5064, Route A per the -2026-08-18 maintainer ruling). Every value is the exact component the -side-effect import registers for that type — for the two `object-*` types -that is the internal data-source-gate wrapper, not the exported widget. The -three config-panel components (`DashboardConfigPanel`, `WidgetConfigPanel`, -`DashboardWithConfig`) leave the map; they remain named exports. Any code -reading the old keys (e.g. `dashboardComponents.DashboardRenderer`) breaks — -two independent word-boundary greps measured zero such consumers in-tree. -Per AGENTS.md §版本号策略, objectui's major tracks `@objectstack`'s major, not -its own breaking-change count — this package's own breaking changes are -scored `minor` with the break spelled out in this body, which is what makes -this a `minor` (maintainer ruling, 2026-08-19). diff --git a/.changeset/dashboard-config-panel-i18n.md b/.changeset/dashboard-config-panel-i18n.md deleted file mode 100644 index 7e6b44f004..0000000000 --- a/.changeset/dashboard-config-panel-i18n.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch -'@object-ui/i18n': patch ---- - -The dashboard config sidebar translates: `WidgetConfigPanel` and `DashboardConfigPanel` are wired through `t()`. - -Both panels build a `ConfigPanelSchema` — breadcrumb, section titles, field -labels, placeholders, help text and option labels — and neither imported a -translation hook at all, so all 61 of their user-visible strings were English -literals. Both are exported from the package barrel and mounted by -`DashboardWithConfig` as the dashboard editing sidebar, so a user on any -non-English console opened a panel that stayed English inside chrome that had -translated around it. - -They now resolve through a new `dashboard.config.*` namespace — 75 keys, added -to all ten locale packs. The namespace sits beside `dashboard.trend.*` and -`dashboard.filters.*`, which is where this package's other translated surfaces -already read from, and the panels reach it through -`useConfigPanelTranslation`, a `createSafeTranslation` hook whose -`CONFIG_PANEL_DEFAULT_TRANSLATIONS` map carries the English defaults for hosts -that mount no `I18nProvider`. - -The keys are authored fresh against the wording the panels actually ship rather -than restored from the retired `configPanel.*` block: that vocabulary had no -reader, was never validated against a shipped label, and covered 16 of the 61 -strings. Where the two name the same word the translations are reused. - -Every `en` pack value and every built-in default is byte-identical to the -literal it replaces, so English rendering and provider-less rendering are -unchanged — asserted row by row, in both directions, against a frozen table of -the pre-change literals. diff --git a/.changeset/dashboard-grid-object-chart-metadata-probe-network-escape-5299.md b/.changeset/dashboard-grid-object-chart-metadata-probe-network-escape-5299.md deleted file mode 100644 index 362c2e69d8..0000000000 --- a/.changeset/dashboard-grid-object-chart-metadata-probe-network-escape-5299.md +++ /dev/null @@ -1,22 +0,0 @@ ---- ---- - -Test-only (objectui#5299). `DashboardGridLayout.datasetPath.test.tsx`'s -'options.data provider widget' negative-control test escaped to the real network: -1 live TCP connection attempt per run against `http://localhost:3000` (happy-dom's -default document URL), best-effort-swallowed so all 13 pre-existing tests stayed -green throughout. `-t` bisected across all 13 cases in isolation to pin the exact -site — the only escape in the file — refuting the `isLegacyRetiredWidget` sentinel -candidate a previous round suspected (neither of its two tests escapes alone). - -Root cause: the `options.data.provider: 'object'` widget maps to component type -`object-chart`, whose `ObjectChart.tsx` (`@object-ui/plugin-charts`) still carries -its own inline `apiFetch ?? fetch` category-color probe — the original objectui#4106 -defect site, never migrated onto the `useDatasetDimensionMeta` hook objectui#4389 -extracted for the dataset-bound path. Same fix shape as objectui#5225's reference PR -(#5283) and objectui#5280's port (#5300): a recording double answers the metadata -route, `afterEach` fails on any non-metadata escape instead of swallowing it, and -`cleanup()` runs before `vi.unstubAllGlobals()` in the same `afterEach` to avoid the -reverse-registration flake #5280 measured. One new test pins the probe's own request -shape (exactly one call to `/api/v1/meta/object/invoices`). All 13 pre-existing tests -still pass; none depended on the swallowed failure. No published behaviour changes. diff --git a/.changeset/data-objectstack-files-drops-src-4847.md b/.changeset/data-objectstack-files-drops-src-4847.md deleted file mode 100644 index ab6d8835c2..0000000000 --- a/.changeset/data-objectstack-files-drops-src-4847.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -`@object-ui/data-objectstack` stops publishing its `src/` tree - -The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 43 source files — 38 of them `*.test.ts`. It had been that way since the package's first commit (`780a1b993`), never added for a consumer, and objectui#4006 recorded the same shape without acting on it: its scope was the `*.test.d.ts` half that the build program emitted into `dist`, and its own triage note graded this half as tarball weight rather than a break. - -Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has one entry (`.`) and every condition under it targets `dist`; `main` / `module` / `types` are `./dist/index.js`, `./dist/index.js`, `./dist/index.d.ts`; the repo and the docs teach only the root specifier, and no `@object-ui/data-objectstack/src/...` deep import exists anywhere (the `src` paths in sibling `vite.config.ts` / `vitest.config.mts` files are workspace aliases resolved through `path.resolve()` against the source tree, which no `files` array shapes); and the tarball holds no sourcemap that could point back at `src`, since `tsup.config.ts` sets `sourcemap: false` and its bundled `dts` writes no `.d.ts.map` — the built `dist` contains four files, zero `.map` among them, and zero occurrences of `sourceMappingURL` or `../src/`. - -`npm pack --dry-run` across the change, on the same `dist`: - -| | before | after | -| --- | --- | --- | -| entries | 51 | 8 | -| unpacked | 1356830 B | 719876 B | -| tarball | 393379 B | 222157 B | - -43 files leave, none arrives, and every surviving entry is byte-identical apart from the edited `package.json`: `dist/index.{js,cjs,d.ts,d.cts}`, `README.md`, `CHANGELOG.md`, `LICENSE`, `package.json`. The 43 are the 38 tests plus the five modules they cover (`index.ts`, `errors.ts`, `metadata-client.ts`, `userState.ts`, `cache/MetadataCache.ts`), whose published form remains the bundled `dist/index.js`. diff --git a/.changeset/data-surface-create-affordance-matrix-5164.md b/.changeset/data-surface-create-affordance-matrix-5164.md deleted file mode 100644 index b7afbcd68b..0000000000 --- a/.changeset/data-surface-create-affordance-matrix-5164.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -The `/data` surface's "New" button now reads the whole CRUD affordance matrix, not just the permission. - -`ObjectDataPage` — the parameterized bare data surface (ADR-0055, objectui#2251, -route `/apps/:appName/:objectName/data`) — gated its "New" on -`can(objectDef.name, 'create')` and nothing else. It never called -`resolveEffectiveCrudAffordances` at all, so every layer below the principal's -grant was missing at once, and one authored object got a different affordance -here than it gets on the object-list page next door: - -- the `managedBy` bucket default was ignored — `append-only`, `engine-owned` and - `better-auth` all resolve `create: false`, yet this surface still offered a - "New" that navigates to `../new`; -- the object-level `userActions: { create: false }` opt-out did not close the - button; -- the effective API operations intersection (objectui#3391) was absent, so the - toolbar could offer a create the server would answer with a 405; -- and `createPredicates` — the layer objectui#5153 gave the object-list page — - was unread here as a consequence of the wider gap. - -The server is the enforcement point throughout, so this was a UI-truthfulness -defect rather than a privilege escalation: the button was offered, the write was -still refused. It is now resolved exactly as `ObjectView` resolves it — the -spec's bucket/`userActions` matrix intersected with the server-resolved -effective operations, then the toolbar-scope predicate layer on top. - -Predicate binding and failure posture are the family's, unchanged and not -reinvented here: `visibleWhen` fails CLOSED with declared-ness read by `?? true` -(so `visibleWhen: false` hides the button rather than reading as "ungated"), and -`disabledWhen` fails SOFT with its `!= null` declared-ness gate outside the -evaluation (so `disabledWhen: ''` reads as "no condition", and an unevaluable -predicate never greys a button forever). Like the standalone object list, this -surface has no record in scope, so a predicate reading `record.*` has nothing to -bind and fails closed — the spec's documented binding for a toolbar predicate. - -The layers only ever narrow. The pre-existing permission gate is not replaced, -it is one conjunct of the new one: a passing predicate cannot re-open what the -bucket, the effective operations or the principal's grant have closed. diff --git a/.changeset/dataset-chart-category-colors-and-order.md b/.changeset/dataset-chart-category-colors-and-order.md deleted file mode 100644 index 06efee8a00..0000000000 --- a/.changeset/dataset-chart-category-colors-and-order.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-report': patch ---- - -A report's embedded chart now paints its category dimension's own option colours and renders ordered-sequence charts (funnel/pyramid) in the field's declared picklist order — the same two derivations a dashboard chart has always gotten for the identical dimension (objectui#4906). - -`DatasetReportChart` (`DatasetReportRenderer.tsx`) resolved its dimension's option **labels** but called neither `buildOptionColorMap` nor `buildCategoryOrder` — the two `@object-ui/core` helpers `DatasetWidget` (plugin-dashboard) already runs off the same resolved field metadata. The chart forwarded only an author-supplied `colors` record (objectui#4877); with none authored it fell back to the positional palette, and a funnel's stages sorted by value instead of the declared pipeline. - -This is convergence onto an already-ruled behavior, not new capability: the report path now runs the identical `useDatasetDimensionMeta` → `localizeFieldOptions` → `buildOptionColorMap`/`buildCategoryOrder` chain the dashboard widget uses (framework#3588's declared-picklist-order ruling), reused rather than re-derived. - -**This visibly changes rendering for an existing report** that groups by a select/lookup dimension carrying option colours, or is declared on an ordered field: - -- a chart with no authored `colors` now paints each category in that dimension's own option colour (e.g. a `health` dimension now paints its own green/amber/red) instead of the renderer's positional palette; -- a `funnel`/`pyramid` chart now orders its stages by the field's declared picklist order instead of sorting by value. - -Precedence is unchanged and preserved: an authored `colors` record (objectui#4877) still wins over the derived per-category map, merged UNDER it exactly as the dashboard already does — an author's explicit colour for a category is never overridden by the field's own. diff --git a/.changeset/dataset-widget-chart-bucket-drill-render-count.md b/.changeset/dataset-widget-chart-bucket-drill-render-count.md deleted file mode 100644 index 90c59d9d7f..0000000000 --- a/.changeset/dataset-widget-chart-bucket-drill-render-count.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only: fixed a flaky overshoot in `plugin-dashboard`'s DatasetWidget chart-bucket -drill test, where `waitFor` pinned a cumulative drill-filter render count that a late, -unrelated dimension-metadata re-render could push past 1 with no way to recover -(objectui#4718, same defect class as objectui#4706 / PR #4708). The assertion now waits -for the drill to have opened at least once and keeps its content check (which records -the drawer filtered on) as the substantive assertion. No published behaviour changes. diff --git a/.changeset/dataset-widget-dotted-dimension-race.md b/.changeset/dataset-widget-dotted-dimension-race.md deleted file mode 100644 index db6f134b84..0000000000 --- a/.changeset/dataset-widget-dotted-dimension-race.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only: fixed a flaky race in `plugin-dashboard`'s DatasetWidget dotted-dimension -tests, where `waitFor` was bound to a rendered cell while the assertion checked a -separately-resolving metadata fetch recording (objectui#4487). No published behaviour -changes. diff --git a/.changeset/dataset-widget-drill-title-render-count.md b/.changeset/dataset-widget-drill-title-render-count.md deleted file mode 100644 index 59e3c817ee..0000000000 --- a/.changeset/dataset-widget-drill-title-render-count.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only: fixed a flaky overshoot in `plugin-dashboard`'s DatasetWidget drill-title -tests, where `waitFor` pinned a cumulative drawer-render count that a late, unrelated -dimension-metadata re-render could push past 1 with no way to recover (objectui#4706). -The assertions now wait for the drawer to have opened at least once and keep their -content check (the drawer title/filter) as the substantive assertion. No published -behaviour changes. diff --git a/.changeset/dataset-widget-metadata-probe-network-escape-5280.md b/.changeset/dataset-widget-metadata-probe-network-escape-5280.md deleted file mode 100644 index 90d7c0f464..0000000000 --- a/.changeset/dataset-widget-metadata-probe-network-escape-5280.md +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - -Test-only (objectui#5280). `DatasetWidget.test.tsx`'s dimension-metadata probe — -`useDatasetDimensionMeta`'s `apiFetch ?? fetch` fallback, exercised whenever a mocked -`queryDataset` result carries `object` — escaped to the real network: 5 live TCP connection -attempts per run against `http://localhost:3000` (happy-dom's default document URL), all -best-effort-swallowed so the suite stayed green throughout. Same root cause and same fix -shape as objectui#5225's reference PR (#5283, `plugin-report`'s `DatasetReportRenderer.test.tsx`): -a recording double answers the metadata route with an option-free payload — byte-identical -to what the failed read produced, so no pre-existing assertion changes meaning — records -every URL, and `afterEach` fails on any non-metadata escape instead of swallowing it. Three -new tests pin the probe's own request shape (object, count, route) and its previously -unexercised success path. All 52 pre-existing tests still pass; none depended on the -swallowed failure. No published behaviour changes. diff --git a/.changeset/dead-i18n-namespaces-configpanel-renderer.md b/.changeset/dead-i18n-namespaces-configpanel-renderer.md deleted file mode 100644 index bb31bfc3de..0000000000 --- a/.changeset/dead-i18n-namespaces-configpanel-renderer.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -Delete two dead i18n namespaces — `configPanel.*` (16 keys) and `renderer.*` (13 keys) — from all ten locale packs. - -290 translated strings (29 keys x 10 packs) with **no reader anywhere in the -repo**. Measured with `node scripts/check-i18n-dead-keys.mjs`, the reverse sweep -from objectui#4658: it subtracts the referenced key set (the AST walk that -`check-i18n-call-site-keys.mjs` already uses, plus plural suffixes, plus -`returnObjects` branches, plus every dynamic template head) from the pack key -set, then re-checks each survivor with a fixed-string grep over the whole repo. -Both namespaces scored a clean sweep — `configPanel.*` 16/16 CONFIRMED and -`renderer.*` 13/13 CONFIRMED, zero NEEDS-REVIEW, and a fixed-string grep for all -29 keys returns nothing at all outside `packages/i18n/src/locales/`. - -`configPanel.*` is a dashboard-widget config-panel vocabulary (`layout`, -`columns`, `gap`, `rowHeight`, `refreshInterval`, `appearance`, `theme`, -`general`/`advanced`, …). The two components that surface exactly that -vocabulary — `packages/plugin-dashboard/src/WidgetConfigPanel.tsx` and -`DashboardConfigPanel.tsx` — import no translation hook at all and hardcode the -same words in English (`title: 'Layout'`, `label: 'Columns'`, `label: 'Gap'`, -`label: 'Show description'`, `label: 'Theme'`). The two config panels that do -use i18n read other namespaces: `ViewConfigPanel.tsx` reads -`console.objectView.*`, `ReportConfigPanel.tsx` reads `common.*` and -`report.editor.*`. No component reads `configPanel.*`. - -`renderer.*` is SchemaRenderer placeholder/status vocabulary (`noPageSchema`, -`noFormSchema`, `noDashboardSchema`, `pageRendering`, `dashboardRendering`, -`formRenderingMode`, …). It is dead in the stronger sense: the English strings -have no hardcoded twin either — `No page schema provided`, `No form schema -provided`, `No dashboard schema provided`, `Page rendering`, `Dashboard -rendering` and `Form rendering in` each return zero hits repo-wide outside the -packs, and `packages/react/src/SchemaRenderer.tsx` imports no translation hook. -The messages themselves are gone from the product, not merely un-translated. - -No behaviour change is possible: a key with no reader cannot be read. No test -fixture pinned any of the 29 keys, and neither i18n baseline JSON names one, so -nothing else had to move. - -Part of #4730. diff --git a/.changeset/dead-i18n-namespaces-workflow-publicform-demo.md b/.changeset/dead-i18n-namespaces-workflow-publicform-demo.md deleted file mode 100644 index 3626c83f13..0000000000 --- a/.changeset/dead-i18n-namespaces-workflow-publicform-demo.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -Delete two dead i18n namespaces — `workflow.*` (58 keys) and `publicForm.demo.*` (36 keys) — from all ten locale packs. - -940 translated strings (94 keys x 10 packs) with **no reader anywhere in the -repo**. Measured with `node scripts/check-i18n-dead-keys.mjs`, the reverse sweep -from objectui#4658: it subtracts the referenced key set (the AST walk that -`check-i18n-call-site-keys.mjs` already uses, plus plural suffixes, plus -`returnObjects` branches, plus every dynamic template head) from the pack key -set, then re-checks each survivor with a fixed-string grep over the whole repo. -`workflow.*` scored 54/58 CONFIRMED with the other 4 appearing only in two i18n -test fixtures — mentions, not consumers; `publicForm.demo.*` scored 36/36 with -zero textual footprint anywhere outside the packs. - -`workflow.*` is a complete BPMN/workflow-designer vocabulary (`userTask`, -`serviceTask`, `parallelGateway`, `boundaryEvent`, `importBpmn`/`exportBpmn`, -`undo`/`redo`, …). Its one plausible consumer, -`packages/plugin-designer/src/ProcessDesigner.tsx`, hardcodes English -(`{ label: 'User Task', value: 'user-task' }`) and imports no translation hook -at all, while six sibling components in the same package do use one — the -vocabulary was never wired up. `publicForm.demo.*` is demo content (contact and -support form titles, field labels, industry/issue-type/priority options) for a -public-form showcase page that does not exist in this repo. - -No behaviour changes: a key with no reader cannot be read. The `publicForm.*` -parent namespace and every other namespace are untouched. diff --git a/.changeset/dependabot-automerge-gate-4973.md b/.changeset/dependabot-automerge-gate-4973.md deleted file mode 100644 index a98dff81a1..0000000000 --- a/.changeset/dependabot-automerge-gate-4973.md +++ /dev/null @@ -1,35 +0,0 @@ ---- ---- - -CI + tooling + test only (objectui#4973). No published package changes: the files touched are -`.github/workflows/dependabot-auto-merge.yml`, a new `scripts/` helper with its pin test, and the -CI/CD guide page. - -`dependabot-auto-merge.yml` no longer runs `gh pr merge --auto --squash` unconditionally. `--auto` -lands the merge the moment GitHub considers the pull request mergeable — i.e. the moment the -*branch-protection required set* is satisfied, which is a different set from "the checks this -repository runs". On 2026-08-17 that difference put a red commit on `main`: #4959 merged at -08:13:36Z with nine of its nineteen check runs still in flight, and its shard 3/4 and shard 1/4 -then reported `failure` 5m25s and 8m20s later. The four-way test shard matrix is the slowest job -in the repository by construction (it exists to cut a ~9 minute wall clock), so it is the check -`--auto` systematically outruns; `main` went red for every parallel agent until #4968 repaired it, -the second such block in seven days (#4098). The channel was never specific to lockfile ranges — -any red on a slow job could ride it, which is #3523 and #3243 again. - -The wait is now explicit and this workflow owns it. `scripts/dependabot-merge-gate.mjs` polls the -Checks API for the pull request's head SHA and returns a verdict; approval and enqueue are both -behind `gate == 'green'`, and the merge is pinned to the judged SHA with `--match-head-commit`. A -required context that is missing, still running at the deadline, or anything other than `success` -is not green — nothing merges, the job goes red, and a PR comment names what refused. The semver -policy (patch/minor auto, major comment-only) is unchanged, and `--auto` is still the merge action -because an enforced merge queue rejects a direct merge with 405. - -`scripts/__tests__/dependabot-merge-gate.test.ts` replays #4959's measured check-run timeline and -asserts the counterfactual — `pending` at 08:13:36Z, `red` once shard 3/4 reports — and asserts -that the gate's three declared buckets partition exactly the check names that -`pull_request`-triggered workflows produce, so a renamed or added job fails a test instead of -quietly dropping out of the wait. - -Not addressed here, because it is a repository-**settings** surface this repository can neither -read nor change: the branch-protection / merge-queue required set itself, which provably contains -none of the four shards (a merge happened while all four were `in_progress`). diff --git a/.changeset/deregister-app-shell-component-key.md b/.changeset/deregister-app-shell-component-key.md deleted file mode 100644 index 2a58e1643f..0000000000 --- a/.changeset/deregister-app-shell-component-key.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/layout': minor ---- - -**Breaking (shipped as `minor`, following the `page-header` `description` retirement):** -`app-shell` is no longer a component key. `registerLayout()` registered `AppShell` under -that key with no `inputs`; the registration is gone (objectui#4841, ADR-0049 -enforce-or-remove, remove side, maintainer ruling 2026-08-16). - -**What it could never do.** Four of `AppShellProps`' seven keys are `React.ReactNode` -slots — `sidebar`, `navbar`, `children`, `rightRail` — and a JSON document can fill none -of them, so `{ "type": "app-shell" }` resolved to a component that had exactly two -outcomes, neither of them a shell. `children` was dropped in silence: `SchemaRenderer` -strips `children` (and `body`) before spreading a node's keys as props, and `AppShell` -reads its `children` prop, never `schema.children`, so the `
` element rendered -empty with nothing logged. A schema written into `sidebar` / `navbar` / `rightRail` -arrived as a plain object React refuses to render, replacing the node with an error box. -Only `className`, `defaultOpen` and `branding` ever survived the JSON path, i.e. the best -result JSON could reach was a shell with no navigation, no top bar and an empty content -area. With no `inputs` declared, `sdui-parser` had no declaration face to compare a node -against either, so neither outcome was diagnosed. - -**What changes for an author.** The middle state — parses, resolves, renders nothing — is -replaced by a named refusal. `SchemaRenderer` now shows its `Unknown component type: -app-shell` panel (`OBJUI-001`) and `sdui-parser` reports an `error`-severity -`unknown-component` diagnostic before render. - -**FROM → TO.** There is no in-place rewrite, because the node never produced a shell. -Schema authors who want the whole shell from metadata use `app-schema-renderer` -(`AppSchemaRenderer`), which declares its `inputs` and builds branding and sidebar -navigation from an `AppSchema` document: `{ "type": "app-shell", … }` → -`{ "type": "app-schema-renderer", "schema": { … } }`. Everyone else composes in React — -`AppShell` is **unchanged and still exported** from `@object-ui/layout`, and remains the -way to build a shell and render JSON pages inside it. - -Repo-wide scan before removal found no `"type": "app-shell"` node anywhere in -`objectstack-ai/objectui` or `objectstack-ai/objectstack` at `origin/main` — no example, -catalog schema, fixture or template authored one. `content/docs/guide/layout.md` is -updated to state the new fact, and -`packages/layout/src/__tests__/app-shell-not-a-component-key.test.tsx` pins it on both -faces (source and live registry) plus the rendered diagnostic. diff --git a/.changeset/dirty-pumpkins-shout.md b/.changeset/dirty-pumpkins-shout.md deleted file mode 100644 index 9fda590a2d..0000000000 --- a/.changeset/dirty-pumpkins-shout.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/app-shell': minor ---- - -Action param `visible`: one dialect answer, and a fault that is fail-open and LOUD - -`ActionParamDialog`'s `filterVisibleParams` was the last predicate face never -converted to the canonical entry. It evaluated each param's `visible` on a bare -`ExpressionEvaluator` inside `try { … } catch { return true }`, and that produced -two defects at once (objectui#4640, measured on `main`): - -- **Silence.** Three of the four fault shapes emitted nothing at all — an - unparseable source, an unbound identifier and a faulting legacy predicate all - resolved without a word, so a broken `visible` was indistinguishable from an - absent one. The standing 2026-08-06 ruling on objectui#4051 / - objectstack#5149 names silence as the one option that is not available. -- **The fail DIRECTION was decided by the predicate's dialect, not by the - surface.** A bare string ran the legacy JS evaluator (lenient → falsy → param - silently DROPPED); a `{ dialect, source }` envelope ran CEL (fault → param - silently KEPT). One `visible` key, two opposite outcomes, chosen by whether - the authored text happened to contain `${…}` / `===` — the objectui#3314 - shape. Both halves hurt: a dropped param means the dialog never collects a - value the server requires and the action fails at submit with nothing pointing - at the predicate; a kept one offers a field the backend rejects. - -`filterVisibleParams` now routes through `evalRowPredicate`. A param whose -`visible` cannot be evaluated is **shown**, and reported once, with the action -and the param named and the predicate quoted. Fail-open is the ruled direction -for this surface: an extra offered field is rejected by the server with a -message, while a silently hidden required param is undiagnosable. (Row surfaces -keep failing closed — there the harm runs the other way.) Boolean and blank -predicates are answered before the evaluator, so `visible: false` hides the -param and an empty predicate is not reported as broken. - -**Behaviour change worth knowing before you upgrade.** On the canonical CEL -engine an ABSENT key is a runtime fault, not a falsy read. A param gated on -`features.phoneNumber == true` in a deployment whose scope carries no -`phoneNumber` key at all now takes the fail-open branch: the param is SHOWN, -with a warning naming it, where it used to be hidden. The conservative outcome -is still available, in the spelling that is portable to the server's own engine: - -``` -has(features.phoneNumber) && features.phoneNumber == true -``` - -Deployments that DECLARE the flag (`features: { phoneNumber: false }`) are -unaffected — that is a genuine verdict on both engines, and it did not move. - -`@object-ui/core` gains the two evaluator changes this needed: - -- `evalRowPredicate` accepts **`rowless`** — "this surface has no row of its - own", so nothing is bound over the host scope and a `record` / `data` the - scope carries survives instead of being shadowed by an empty row. Row surfaces - are untouched: without the option the row is still the subject (objectui#3796). -- A faulting **`{ dialect, source }` envelope now reports its own source**. - It used to print the literal `"(expression)"`, and because the warn-once key - is (label, predicate), the first faulting envelope under a label silenced - every other one. The envelope is what `@objectstack/spec` normalizes every - authored predicate into, so this was the likeliest shape in served metadata - and the least diagnosable one. diff --git a/.changeset/div-deprecation-provenance-scope.md b/.changeset/div-deprecation-provenance-scope.md deleted file mode 100644 index 4e8cbbb0c1..0000000000 --- a/.changeset/div-deprecation-provenance-scope.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@object-ui/sdui-parser": patch -"@object-ui/components": patch ---- - -`div` 的废弃提示按 provenance 收窄:只对 **JSON 作者面**的节点报,不再对 `kind:'html'` tier 自己解析出的节点开火。 - -html tier 的页面是一段受限 JSX/Tailwind 文本,由引擎自己的解析器编译(只解析、不执行),标签名原样映射成节点 —— 作者在那一层写下的盒子标签,是该 tier 词表里的一等成员,**没有别的拼法可迁移**。提示照旧对他们开火,给的还是 JSON 作者面的替代建议:一条谁都无法执行的提示不是废弃,是噪声;它同时意味着这个类型永远退不掉,因为引擎自己的编译器一直在产出它。 - -判据是**来源**,由生产者确立:解析器给它产出的每个节点打一个 symbol 标记(`Symbol.for` 注册键),渲染器读这个标记。symbol 对 `JSON.stringify` / `Object.keys` / DOM 全部不可见 —— 所以它既不会落进被持久化的文档,也就无法被一份(手写或 AI 生成的)JSON 元数据复制回来给自己买到豁免;通过花括号属性夹带进来的 JSON **不打标记**,那部分本来就是手写的,建议对它成立。 - -迁移建议一字未改,JSON 作者面照旧每次模块加载报一次;提示文案现在写明它针对哪一个作者面。 diff --git a/.changeset/doc-component-type-ratchet-4823.md b/.changeset/doc-component-type-ratchet-4823.md deleted file mode 100644 index 65bd974184..0000000000 --- a/.changeset/doc-component-type-ratchet-4823.md +++ /dev/null @@ -1,32 +0,0 @@ ---- ---- - -Tooling + docs-only (objectui#4823). Every `type` string literal in a `content/docs/**.mdx` -code block must now name a component the repository actually registers, enforced by a new CI -gate — `pnpm check:doc-types`, `scripts/check-doc-component-types.mjs`, in its own -`doc-component-types.yml` workflow. - -The catalog side has had this ratchet since objectui#4616: -`examples/schema-catalog/test/catalog-gallery-render.test.tsx` renders every catalog entry and -fails if any paints the registry's "Unknown component type" panel (OBJUI-001). The teaching -surface had no equivalent — a fenced snippet in `content/docs/**` is not rendered, not parsed -and not compared against anything — so a page could teach a `type` that does not exist and -every check in the repo stayed green. The same defect landed three times on that surface -(objectui#4786 `stats-card`, objectui#4796 `plugin:grid` and `plugin:map`), each found by a -human probe rather than by a check. - -The registered-key universe is derived from the register calls themselves on every run — no -hard-coded list and no build step, so the gate is a checkout plus one `node` call and can -therefore run unfiltered, which matters because the change that introduces this defect is -docs-only and `ci.yml`'s gates skip those by design. - -The first full scan read 558 `type` literals across 143 pages against 661 derived keys and -found three more instances of the same shape, fixed here: `content/docs/utilities/runner.mdx` -and `content/docs/utilities/vscode-extension.mdx` taught `heading`, which nothing registers -(now `h1`, which `html-elements.tsx` registers and which renders the node's `children`), and -`content/docs/plugins/plugin-form.mdx` taught a `multi-step-form` type that appears nowhere in -the repo outside that snippet (now the `object-form` + `formType: 'wizard'` + `sections` shape -that `WizardFormSchema` itself declares). - -No published behaviour changes — repo tooling plus three documentation snippets — so this -declares "no release" rather than a bump. diff --git a/.changeset/drawer-field-group-sections-4774.md b/.changeset/drawer-field-group-sections-4774.md deleted file mode 100644 index d164d0dcbf..0000000000 --- a/.changeset/drawer-field-group-sections-4774.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -Fix: a `drawer` form with no `sections` now renders the object's declared -`fieldGroups` as sections, matching `ObjectForm` and `ModalForm`. - -`deriveFieldGroupSections` had exactly two call sites in the repo — -`ObjectForm` and `ModalForm` — so the same object, with the same metadata, -rendered one section per declared group in the modal create dialog and one -ungrouped flat list in the drawer. The author who laid the groups out in the -object designer saw them honoured on two surfaces out of three. - -`DrawerForm` now runs the same fallback the modal does: gated on "no explicit -`sections`, no `customFields`", over the same auto-layout-filtered field list -(system fields dropped, auto-generated fields dropped in create mode), with the -flat path's inferred column count carried onto the grouped layout. A curated -`sections` list from a form view still wins, and an object whose fields join no -declared group keeps its flat layout untouched. A derived group declaring -ADR-0085 `collapse` renders as a collapsible header, like an authored one. diff --git a/.changeset/drawer-flat-field-rules-4755.md b/.changeset/drawer-flat-field-rules-4755.md deleted file mode 100644 index 7f9cde5dfa..0000000000 --- a/.changeset/drawer-flat-field-rules-4755.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -Fix: a `drawer` form with no `sections` now honours the object's field-level -conditional rules (`visibleWhen` / `readonlyWhen` / `requiredWhen`) and field -`group`. - -`ModalForm` and `DrawerForm` each carried their own copy of the "object-schema -field to runtime FormField" loop for the no-sections case, and the drawer's -copy had fallen behind: it stopped at `multiple`, so the ADR-0036 predicates -never reached the runtime field and `resolveFieldRuleState` had nothing to -resolve. A hidden field rendered anyway, a frozen field stayed editable (with -the server then dropping the write), and a conditionally-required field never -blocked the submit. - -Both containers now build that list through one shared `buildFlatFields`, which -resolves each field through the same `fromObjectSchema` the sectioned path uses -— so the next field-mapping fix lands once and reaches every container. diff --git a/.changeset/drop-stale-eslint-disable-directives.md b/.changeset/drop-stale-eslint-disable-directives.md deleted file mode 100644 index afc92527c1..0000000000 --- a/.changeset/drop-stale-eslint-disable-directives.md +++ /dev/null @@ -1,23 +0,0 @@ ---- ---- - -Comment-only cleanup (objectui#4833). Removed all 49 `eslint-disable` directives that ESLint -itself reports as `Unused eslint-disable directive (no problems were reported from 'X')` — -suppressions whose underlying finding no longer exists, spread over 35 files in 17 packages. - -No published behaviour changes: the diff removes comment lines and nothing else. The single -line that is modified rather than deleted is `apps/console/src/pages/developer/PublicFormsPage.tsx:151`, -where the directive sat inline inside a statement, so only the comment was stripped and the -`useEffect` call itself is byte-identical. - -The judgement was never ours: every site was taken from ESLint's own `ruleId: null` report, -and the whole-repo counts reconcile exactly — warnings 9828 to 9779 (−49, one per directive), -errors 0 to 0, unused directives 49 to 0, and **no other rule's count moved in either -direction**. That last figure is what proves the deletions were inert: had any directive still -been load-bearing, removing it would have surfaced the rule it was suppressing. - -Twenty of the 49 sat on `no-console`, which this repo's config sets to `error` (the objectui#4029 -ratchet) — they were dead because that rule is configured `{ allow: ['warn', 'error'] }` and the -calls beneath them are `console.warn` / `console.error`, or because the file is one the config -turns `no-console` off for. A stale suppression on an error-level ratchet is the kind of comment -a later reader copies as precedent, which is why they are worth removing rather than tolerating. diff --git a/.changeset/element-text-button-i18n-arms-4970.md b/.changeset/element-text-button-i18n-arms-4970.md deleted file mode 100644 index 7fadd87f5c..0000000000 --- a/.changeset/element-text-button-i18n-arms-4970.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@object-ui/components': minor -'@object-ui/plugin-detail': minor ---- - -`element:text.content` and `element:button.label` declare the inline translation -map they already accept - -Two more instances of the contradiction objectui#3832 fixed the mechanism for, -measured after that ruling had fixed its scope at five specimens and filed -separately as objectui#4970. Both inputs' own `description` tells the author to -write an inline translation map (`{ en, "zh-CN", … }`), both renderers resolve one -through `pickLocalized`, and both spec props schemas accept one — while the -declaration said `type: 'string'`, so the manifest gate reported -`type-mismatch` on the exact shape the block had recommended. Both blocks are in -`PUBLIC_BLOCKS`, so this reached authors through `sdui.manifest.json` and -`sdui-intrinsics.d.ts` as well as the save gate. - -Each declaration is now `type: ['string', 'object']`, the union form -objectui#3832 introduced, and the arms are the ones the contract accepts — -re-measured on the `@objectstack/spec` 17.0.0 GA pin rather than carried over -from the issue, which was written at the 17.0.0-rc.6 pin: -`ComponentPropsMap['element:text'].content` and -`ComponentPropsMap['element:button'].label` are both -`string | Record< string, string >`, and both refuse a number, a boolean and an -array. Those three refusals are the controls in the acceptance test, which is -what keeps a widening distinguishable from a silenced check. - -Nothing else about the two blocks moves. A plain-string `content` / `label` -validates exactly as before, values matching neither arm are still reported, and -no other manifest entry changes shape — the public manifest now carries seven -array-valued input types, the five from objectui#3832 plus these two, with the -remaining 57 public blocks serializing byte for byte as they did. - -`record:alert`'s renderer-local prop type is corrected in the same pass -(`plugin-detail`): its `title` / `body` were still typed `string` while the same -file resolves both through `pickLocalized` and the block's published `inputs` -have declared `['string', 'object']` since objectui#3832, so the two slots were -narrower than both the renderer and the block's own published surface. The type -is not exported, so no consumer was misled and no published surface changes. The -CTA's `action.label` one level down is left alone on purpose (objectui#4998): -`action` is published as a bare `object` whose member shape lives in prose, so -there are no declared arms for it to be aligned against yet. diff --git a/.changeset/embeddable-form-thank-you-host-navigation-5112.md b/.changeset/embeddable-form-thank-you-host-navigation-5112.md deleted file mode 100644 index 2285e93f10..0000000000 --- a/.changeset/embeddable-form-thank-you-host-navigation-5112.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -`EmbeddableForm`'s thank-you redirect stops being mount-blind: an in-app destination now goes through the host's injected navigate. - -The redirect ended in one unconditional `window.location.href = url`. That is -right for the external destination this key deliberately admits, and wrong for -the in-app one it equally admits: a rooted path such as `/thanks` assigned to -`location.href` resolves against the ORIGIN root, so under a host mounted at a -sub-path — the framework CLI configures one for every embedded deployment, and -the console runs at basename `/_console` — the submitter landed outside the -application, usually on the host's own 404. Nothing refused either half of that -authoring, so the failure was silent. This is objectui#4989 defect 4 on the key -that card explicitly did not cover, and it is fixed here through the seam -objectui#5111 landed (`HostNavigationContext`, `@object-ui/react`). - -The destinations are now split by who can travel to them: - -- an **app-relative** destination (`/thanks`, `thanks`, `?ok=1`, `#done`) is - handed to the host's navigate when a host supplied one, so a mounted host - places it inside its mount; with no provider 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; -- an **external** destination admitted by `allowedRedirectHosts` keeps - browser-level navigation **unconditionally**. This is the seam's own declared - input contract, not a conservatism: `HostNavigationValue.navigate` documents - `to` as an application-relative path, "never an absolute URL", because a host - navigate is a client-side router transition. Since a relative reference cannot - carry an authority, the seam is now structurally incapable of being handed a - cross-origin URL. - -A same-origin **absolute** URL — the one shape those two arms do not name — also -keeps browser-level navigation. Routing it through the seam would mean rewriting -the author's full address into a path a mounted router then places at a -different address; an author who spelled out the whole address asked for that -address. - -Not changed, deliberately: `isRedirectUrlSafe` and `allowedRedirectHosts` — -WHICH destinations are followed. That acceptance set (same-origin OR the -author's allowlist) is this key's own contract, a refused destination reaches -neither the seam nor the browser, and objectstack#7496's relative-only ruling -belongs to `submitBehavior.url` and is not imported onto this key. The wait's -ownership (objectui#5049) and the thank-you panel's copy (objectui#5073) are -carried over unchanged: unmounting or pressing "Submit Another Response" still -cancels a pending redirect, seam or no seam. diff --git a/.changeset/embeddable-form-thankyou-countdown-tick-5083.md b/.changeset/embeddable-form-thankyou-countdown-tick-5083.md deleted file mode 100644 index 3db6114f8d..0000000000 --- a/.changeset/embeddable-form-thankyou-countdown-tick-5083.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -A public form's thank-you countdown ("Redirecting in {{seconds}} seconds…") now -actually counts down, instead of rendering a number once and leaving it frozen -for the whole wait. - -All ten locale packs document `publicForm.redirecting`'s `{{seconds}}` as "the -remaining seconds", but `EmbeddableForm` computed it exactly once — at the render -that first shows the thank-you panel — from `pendingRedirect.delayMs`, and never -touched it again. On the 3 second default delay, a submitter saw a fully static -"Redirecting in 3 seconds…" for the entire wait (objectui#5083). - -The number is now owned by a per-second `setInterval`, on the same ownership -model PR #5070 established for the redirect wait itself: an effect keyed on the -accepted destination (`pendingRedirect`), cancelled on unmount and on -`handleReset`'s `Submit Another Response` — the exact regression surface -objectui#5049 fixed for the navigation timer, restated here rather than -reintroduced. The interval also stops itself once it reaches 0, rather than -ticking indefinitely past a wait that has already ended. - -Nothing about WHICH destinations are followed or refused changes -(`isRedirectUrlSafe` / `allowedRedirectHosts`, objectui#4989), and neither does -the navigation wait's own ownership (objectui#5049 / PR #5070) — this is the -display only. diff --git a/.changeset/embeddable-form-thankyou-countdown-verdict-5073.md b/.changeset/embeddable-form-thankyou-countdown-verdict-5073.md deleted file mode 100644 index b192eb5d25..0000000000 --- a/.changeset/embeddable-form-thankyou-countdown-verdict-5073.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -A public form's thank-you panel no longer promises a redirect its own guard just -refused, and the `texts.redirectBlocked` string can finally reach a screen. - -`EmbeddableForm` decided whether to redirect from `isRedirectUrlSafe` / -`allowedRedirectHosts`, but the panel's copy was keyed on something else: whether -a `thankYouPage.redirectUrl` had been *authored* (objectui#5073). An author who -declared a cross-origin destination without allowlisting its host therefore got a -submitter who was told `Redirecting in 3 seconds…` and was then never redirected. -The guard did its job; the screen contradicted it. That screen is the terminal -state of a public form, so nothing came after to correct the impression. - -On the same path, the `texts.redirectBlocked` string the refusal set was -unreachable in every locale. It was recorded with `setError(...)`, whose banner -lives in the form branch — and `setSubmitted(true)` has already run one statement -earlier, so the component is showing the thank-you branch, which renders no error -at all. That was the only assignment of the key anywhere; pressing -`Submit Another Response` cleared it rather than showing it. - -Both now follow the verdict: - -- The countdown renders on `pendingRedirect` — the destination that was actually - accepted — and reads its seconds from the delay captured with it, so the - displayed wait is the wait being served. A refused destination, and the - honeypot's silent fake-success (which accepts no destination either), simply - omit the line. -- A refused destination renders `texts.redirectBlocked` in the thank-you panel - when the author declared it — the case the key exists for, in the author's own - words to the public. Undeclared means silence; the author keeps the existing - `console.warn`, which is the channel for the person who can fix the - declaration. - -Which destinations are refused is unchanged: `isRedirectUrlSafe` and -`allowedRedirectHosts` are untouched, as is the timer ownership introduced for -objectui#5049. Nothing was ever at risk in the data — the write succeeds before -any of this — the harm was a false statement on the confirmation screen and a -shipped, translated string no user could see. diff --git a/.changeset/embeddable-form-thankyou-redirect-timer-lifetime-5049.md b/.changeset/embeddable-form-thankyou-redirect-timer-lifetime-5049.md deleted file mode 100644 index 5deaf0d2e1..0000000000 --- a/.changeset/embeddable-form-thankyou-redirect-timer-lifetime-5049.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/plugin-form': patch ---- - -A public form's thank-you redirect no longer outlives the form that armed it, and -"Submit Another Response" now cancels it. - -`EmbeddableForm` armed the `thankYouPage.redirectUrl` wait with a bare -`setTimeout` inside the submit handler: the handle was not stored, nothing cleared -it, and no part of the component owned it (objectui#5049). Two consequences, and -the second needs no unmount at all: - -- For the whole of the delay a full-page navigation was pending that survived the - form being taken off screen — an embed removed by the host page, a route change, - a re-keyed subtree. With `redirectDelay` unset that window is the 3000 ms - default, so this was the normal state of every submit on this surface rather - than an edge authoring; the thank-you panel says as much out loud with - `Redirecting in {{seconds}} seconds…`. -- Under `allowMultiple`, `Submit Another Response` only flipped `submitted` back - to false while the pending navigation kept ticking. The component invited the - submitter into a fresh form and then, about three seconds later, threw the whole - page away while they were typing the next response. - -The wait now lives in an effect keyed on the accepted destination, with a -`clearTimeout` cleanup, so unmounting cancels it; and `handleReset` drops the -destination, so pressing `Submit Another Response` cancels it too. The button -offers the submitter a fresh form, and that offer cannot be honoured alongside -discarding the page a moment later. This is the same move `ObjectForm` / -`WizardForm` (objectui#5033) and `apps/console`'s `FormPage` already made for -their own copies of this defect. - -Nothing else changed. Which destinations are followed and which are refused is -still decided by the same `isRedirectUrlSafe` / `allowedRedirectHosts` guard, on -the same line as before — only who owns the wait changed. The delay is captured -together with the destination at the moment the write is accepted, so a host -re-rendering with a different `redirectDelay` mid-wait cannot restart the pause -under the submitter. The countdown copy is untouched. No data was ever at risk: -the write has already succeeded before the wait begins, so the harm was a -surprising navigation — and, on the `allowMultiple` path, the loss of what the -submitter had just re-typed. diff --git a/.changeset/engine-i18n-carveout-recorded.md b/.changeset/engine-i18n-carveout-recorded.md deleted file mode 100644 index 1784dfa100..0000000000 --- a/.changeset/engine-i18n-carveout-recorded.md +++ /dev/null @@ -1,13 +0,0 @@ ---- ---- - -Docs only (objectui#4662): records the `engine.*` carve-out where the i18n -conventions live — `packages/i18n/README.md` gains a "Scope" section stating -that the metadata-admin (Studio) namespace resolves through a module-local -`en`/`zh` table rather than the ten locale packs, why that is Phase 3f design -rather than drift (the server's `/meta/types` `label` is the primary path; the -table is the fallback), the two consequences (eight locales render English -there; the i18n gates cannot see the namespace by construction), and the -condition for revisiting it. `packages/app-shell/src/views/metadata-admin/i18n.ts` -gains a header note pointing at that section — a comment-only edit. No key was -migrated, no locale pack was touched, and no published behaviour changes. diff --git a/.changeset/field-constraints-zod-mirror-5186.md b/.changeset/field-constraints-zod-mirror-5186.md deleted file mode 100644 index 47e062bc90..0000000000 --- a/.changeset/field-constraints-zod-mirror-5186.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@object-ui/types': minor ---- - -Align `FieldConstraintsSchema` (the zod face of `FormFieldSchema.validation`) to the public TS contract `FieldValidationRules`. Behaviour change in `objectui validate`: `validation` written to the TS contract — `required: string | boolean`, `minLength`/`maxLength`/`min`/`max` as `{ value, message }` objects, `validate` function — is now accepted (it was rejected before), and the flat scalar dialect (`minLength: 3`, `pattern: '^[a-z]+$'`) that react-hook-form never runs is now rejected (it passed before, validating nothing — the objectui#5099 symptom on the zod face). `pattern.value` must be a compiled RegExp per the objectui#5099 ruling; JSON/YAML cannot express one, so a string `pattern.value` is rejected by name with guidance toward the metadata route (`FieldSchema.pattern`). No silent strip, no string-to-RegExp coercion. diff --git a/.changeset/field-selector-load-failure-5227.md b/.changeset/field-selector-load-failure-5227.md deleted file mode 100644 index 1a52e02a79..0000000000 --- a/.changeset/field-selector-load-failure-5227.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A failed field fetch in the metadata-admin `field-selector` picker now reads as a failure, not as "this object has no fields". - -`FieldSelectorWidget` was the fourth loader of the family objectui#5170 and -objectui#5169 closed, and the only one that does not go through -`MetadataClient`: a raw `fetch` to `/api/v1/objects/:name/fields`, its own -component-local `fields` / `loading` state, no `WidgetContext`. That is why the -`catalogErrors` channel added for the other pickers never reached it, and why it -kept the defect after they were fixed. - -It could reach a false empty two ways, and both are closed here: - -- the `catch` wrote `setFields([])` — the exact value a successful response with - no fields writes — and cleared the loading flag, so a dropped connection or an - expired session rendered as a completed, empty picker with a `console.error` - nobody reads; -- it never checked `res.ok`, so a 4xx/5xx whose body happens to parse as JSON - landed in the SUCCESS branch with `data.fields` undefined and `|| []` spelled - the refusal as an empty catalog. This mouth is the worse of the two: no error - was raised for the `catch` to swallow, so a union guarding only the `catch` - would have left it wide open. - -Both now leave through one door — a throw — and the loader is the four-arm -`LoadState` (`idle | loading | loaded | error`) the sibling pickers already use. -A failure renders the shared `PickerLoadFailure` block with the server's own -message, and the picker is replaced rather than decorated, so nothing on screen -can still be read as a measurement of zero. Whatever field is already stored -stays visible and removable: a failed catalog must not also block authoring. - -The "no fields" reading is deliberately kept for the case where it is true — a -load that COMPLETED and found nothing still renders the disabled picker, -unchanged and now reachable only from the `loaded` arm. No copy was added or -reworded. - -`usePickerLoad`, the shared loader hook, moves from `ResourceEditPage` into -`loadState` so this fourth loader reuses it instead of hand-rolling a fifth -union in a second file — which is exactly how this loader came to be missed. -Behaviour of the three existing callers is unchanged. diff --git a/.changeset/field-type-coverage-user-form-widget-4855.md b/.changeset/field-type-coverage-user-form-widget-4855.md deleted file mode 100644 index 771f652bd7..0000000000 --- a/.changeset/field-type-coverage-user-form-widget-4855.md +++ /dev/null @@ -1,9 +0,0 @@ ---- ---- - -Test-only: `field-type-coverage.test.ts` now pins `'user'` in `FORM_WIDGET_TYPES`, -closing a one-way blindness in the field-type → renderer coverage guard — -`CELL_RENDERER_TYPES` already pinned `'user'`, but the form half did not, so -deleting the `user: 'field:user'` alias would have silently fallen back to -`field:text` on the form path while the guard stayed green. No published -behaviour changes. diff --git a/.changeset/field-validation-rules-pattern-regexp-5099.md b/.changeset/field-validation-rules-pattern-regexp-5099.md deleted file mode 100644 index 67d8a9adb0..0000000000 --- a/.changeset/field-validation-rules-pattern-regexp-5099.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/types': minor -'@object-ui/components': minor ---- - -`FieldValidationRules.pattern.value` narrows to `RegExp`, and the form renderer reports unrecognized validation rule names loudly (objectui#5099, maintainer ruling 2026-08-18). - -**BREAKING for hand-written form schemas — deliberately declared `minor`.** This -repo's version policy reserves `major` for tracking `@objectstack` majors and is -mechanically enforced (`scripts/check-changeset-no-major.mjs`); per that policy, -objectui's own breaking changes ship as `minor` with the breaking semantics -stated plainly here: - -- **What breaks:** `validation: { pattern: { value: '^…$', message } }` with a - **string** value no longer compiles. Write a `RegExp` literal instead: - `pattern: { value: /^…$/, message }`. -- **Why red is the fix, not the damage:** react-hook-form applies `pattern` - only when `value instanceof RegExp`, and the renderer's single read point - spreads `validation` verbatim — so every string pattern accepted by the old - type ran **zero** validations, silently. Callers turning red were not - validating anything yesterday; the error converts silent non-validation into - explicit failure at authoring time. -- **Unaffected:** the metadata route. `FieldSchema.pattern` (a string in field - metadata) is still compiled by `buildValidationRules` in `@object-ui/fields` - via `new RegExp(...)` before it reaches the renderer. - -Also, per the same ruling's second limb, the form renderer now reports rule -names react-hook-form does not run (`console.error`, message doubles as the fix -instruction): a misspelled `minlength`, an invented `email`, or numeric keys -left by spreading an array into `validation` shout instead of vanishing. The -recognized set is pinned against the installed react-hook-form bundle so a -future bump cannot silently rot the diagnostic. The ruling's rejected half is -equally binding and equally pinned by test: the read point does **not** compile -string patterns — that consumer-side tolerance would harden the ambiguous -declaration into contract (AGENTS.md #0.1). diff --git a/.changeset/fields-files-drops-src-4856.md b/.changeset/fields-files-drops-src-4856.md deleted file mode 100644 index b45bfadded..0000000000 --- a/.changeset/fields-files-drops-src-4856.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -`@object-ui/fields` stops publishing its `src/` tree - -The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 173 source files — 97 of them `*.test.tsx` / `*.test.ts`. It has been that way since the file's first commit (`780a1b993`), where `files` was already `["dist", "src", "README.md"]` while `exports` named only `dist`, so the entry was never added for a consumer. objectui#4006 recorded this exact shape and did not act on it: its scope was the `*.test.d.ts` half the build program emitted into `dist`, and it noted in passing that the test sources were already in the tarball by this other route. - -Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has two entries and both target `dist` (`.` resolves `types` / `import` / `require` to `./dist/index.d.ts` / `./dist/index.js` / `./dist/index.cjs`, and `./style.css` to `./dist/index.css`); `main` / `module` / `types` are `./dist/index.cjs`, `./dist/index.js`, `./dist/index.d.ts`; no deep import into this package exists anywhere in the repo or the docs — the one that used to, `@object-ui/fields/widgets/MarkdownContent`, was ruled out by objectui#4325 precisely because a package's surface is its index, and the `../fields/src` paths in sibling `vite.config.ts` files are workspace aliases resolved through `resolve()` against the source tree, which no `files` array shapes; and the tarball holds no sourcemap that could point back at `src`. That last one is the check that had to be measured rather than assumed, because this package emits its declarations through `vite-plugin-dts` rather than the `tsup` of objectui#4847 or the bare `tsc` of `@object-ui/types`: a clean rebuild writes 78 files into `dist`, of which zero are `.d.ts.map`, zero are `.js.map`, zero contain a `sourceMappingURL` comment and zero mention `../src`. `src/index.css` is an input to `scripts/build-css.mjs`, not an output anyone resolves; the sheet the `./style.css` export names is the built `dist/index.css`, which still ships. - -`npm pack --dry-run` across the change, on the same `dist`: - -| | before | after | -| --- | --- | --- | -| entries | 255 | 82 | -| unpacked | 2265557 B | 841772 B | -| tarball | 629843 B | 252364 B | - -173 files leave, none arrives, nothing outside `src/` moves, and every surviving entry is byte-identical apart from the edited `package.json` itself. The 173 are the 97 tests plus 76 implementation modules, whose published form remains the bundled `dist/index.js` / `dist/index.cjs` and the 75 declaration files beside them. - -`@object-ui/types` keeps its `src` entry for now, and that is a different judgement rather than an omission: it builds with a bare `tsc` under a `declarationMap` / `sourceMap` config with no `inlineSources`, so its shipped `dist/*.d.ts.map` name `../src/*.ts` with no embedded content and dropping `src` there would leave published maps pointing at files the tarball no longer carries. That trade-off is filed as objectui#4851. diff --git a/.changeset/filter-builder-between-pair-and-operator-labels.md b/.changeset/filter-builder-between-pair-and-operator-labels.md deleted file mode 100644 index 4825d2c66f..0000000000 --- a/.changeset/filter-builder-between-pair-and-operator-labels.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/plugin-list': patch -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -Console list filters: a `between` range is submitted only when both bounds are filled, and six operator labels stop rendering as raw i18n keys. - -Two defects in the list-view filter panel (objectstack#8815), both in the Console -render layer, with no workaround available downstream. - -**A half-filled range no longer refuses the whole view.** Picking a date column -and 「介于」 draws two inputs — that part landed in objectui#3958 — but typing -only one bound produced `["2024-01-01", ""]`, and both write paths read "is this -row filled in?" with one shape-blind predicate (`null` / `''` / empty array). -An array of length 2 passed it, so the empty bound went to the server, which -refuses the query outright (`400 INVALID_FILTER`): the list showed -「该视图的查询被拒绝」 and the filters the user had already applied stopped -applying too. The saved-view fold persisted the same half-range, so the refusal -came back on every later read of that view, for every user of it. - -The spec cannot intercept this — `ViewFilterRuleSchema` accepts -`["2024-01-01", ""]` because it counts the two slots rather than what is in -them, while refusing a scalar or a one-element array. Authoring validation is -therefore green on exactly the shape that fails at query time, which makes not -emitting it the producer's job. `@object-ui/components` now exports -`isFilterValueComplete(operator, value)` — arity-aware, so a `pair` row needs -both bounds — and the two consumers that had each kept a copy of the old -predicate (`plugin-list`'s `convertFilterGroupToAST`, `app-shell`'s -`foldFilterGroupToSpecRules`) read it instead. A half-filled range is now -dropped exactly as a half-typed `equals` row already was: no filter, rather than -a filter the server will reject. Bounds of `0` and `false` stay real bounds. - -**Six operator labels are translated in all ten locale packs.** -`startsWith`, `endsWith`, `isNull`, `isNotNull`, `exists` and `notExists` were -missing from every pack, so i18next resolved them to the raw key and the dropdown -showed `filterBuilder.operators.isNull` beside translated entries. The -component's own defaults table could not cover it: that table serves only the -no-provider path, and the Console mounts a provider. The report named four — -a `date` column's bucket offers the four nullness operators; a `text` column -showed all six. - -Because the label key is built dynamically (`t(\`filterBuilder.operators.${op}\`)`), -no existing gate could see the gap: the call-site checker classifies a template -key as `missing-prefix` and only asks whether the prefix resolves, and -cross-pack parity is satisfied when all ten packs are missing a key together. -A new parity test pins the packs against `FILTER_BUILDER_OPERATORS` in both -directions, so an operator added to the dropdown now fails loudly until every -pack labels it. diff --git a/.changeset/filter-builder-falsy-values-and-strict-numeric-reads.md b/.changeset/filter-builder-falsy-values-and-strict-numeric-reads.md deleted file mode 100644 index 6c87ac3cfb..0000000000 --- a/.changeset/filter-builder-falsy-values-and-strict-numeric-reads.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`FilterBuilder` shows the falsy values a row actually holds — a boolean `false` and a number `0` are values, not empty boxes. - -The value controls asked `!condition.value` and `String(condition.value || "")`, -which folds `false` and `0` in with the rows nobody has filled in yet. Both rows -saved, persisted and filtered by their value the whole time; only the control -said otherwise: - -- a boolean column filtered `equals false` snapped back to the **Select value** - placeholder the moment the user clicked **False**, while the row carried - `value: false`; -- a number column filtered `equals 0` showed an empty box — and typing `0` into - one looked like the keystroke had never landed, because the row took the value - and the very next render blanked the input; -- a single-select whose option id is `0` showed the placeholder too, even though - the same control's multi-select branch already drew that option as checked. - -"No value" is now one judgement (`undefined` / `null` / `''`), read by every -value control and by the two helpers that already spelled it out correctly, so -"not picked yet" and "picked False" stay two distinguishable states rather than -trading places. - -The three keyed numeric paths — the token input's commit, a range bound, and the -single value input — no longer read with `parseFloat(raw) || 0`, which takes half -of `"42abc"` and turns `"acme"` into `0`: a filter the user never wrote. All -three now use the same strict reading a field switch uses, so this component -holds one answer to "is this string a number" instead of a strict one and a -lenient one. An unreadable entry becomes an unfilled value, except in the token -input, which declines the commit and leaves the text in the draft box to be -fixed. No behaviour a user can reach today changes: those inputs are -``, which never hands a non-numeric string to the component -in the first place — this closes the drift, before a text box, a formula or a -paste path opens it. diff --git a/.changeset/filter-builder-field-switch-resets-operator.md b/.changeset/filter-builder-field-switch-resets-operator.md deleted file mode 100644 index b2dbd1e0b2..0000000000 --- a/.changeset/filter-builder-field-switch-resets-operator.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`FilterBuilder` settles a row's operator when its **field** changes, instead of leaving an operator the new field's dropdown does not list. - -The operator buckets are per field type and they do not nest: a `select` column -offers `in` / `notIn`, a `text` column offers none of them, and only a date -column offers `between`. Changing a row's field wrote `{ field }` alone, so the -operator survived into a bucket that no longer contained it. Radix's -`SelectValue` matches against the `SelectItem`s actually mounted, so the -operator trigger rendered **blank** — while the row went on filtering by an -operator the user could neither see nor reach, and could only clear by deleting -the row. - -Changing the field is now one edit with the operator and the value's shape, the -same way objectui#3958 / PR #4762 made changing the operator one edit with the -value's shape: - -- an operator the new field's bucket still offers is **kept** — switching - `contains` from one text column to another must not silently become `equals`, - and the value it carries is left alone; -- one the new bucket cannot offer is replaced by that bucket's **first** entry, - and the row's `value` is then re-shaped for the family it lands in — a list - under `in` collapses to its first entry under `equals`, a `between` range - keeps its lower bound, an untouched `[]` becomes `''`. - -Membership is decided through the spec's own `normalizeFilterOperator`, the fold -`filterValueArity` already uses, so a stored rule that reaches the builder -spelled `not_in` is recognised as the operator the dropdown lists as `notIn` and -is not reset out from under the author. The fold is injective over this -builder's whole operator vocabulary, which is what makes comparing through it -safe; a test pins that, and fails the day an added operator would break it. diff --git a/.changeset/filter-builder-field-switch-retypes-value.md b/.changeset/filter-builder-field-switch-retypes-value.md deleted file mode 100644 index 4f31f3f964..0000000000 --- a/.changeset/filter-builder-field-switch-retypes-value.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/components': patch ---- - -`FilterBuilder` settles a row's **value** when its field changes, instead of leaving a value the new column's input cannot show. - -objectui#4768 / PR #4779 settled the row's operator on a field switch and -re-shaped the value only when the operator's family changed — scalar to scalar -has no shape question, so what the user typed was carried through on purpose. -But the field's **type** changed too, and the value input is redrawn from it: a -browser renders a non-numeric value in `` as **blank**. A -`text` row filtered `equals "acme"`, pointed at a number column, showed an empty -box while the row went on carrying `"acme"` — `foldFilterGroupToSpecRules` -persisted it and the live grid queried `amount equals "acme"`. The same -invisible-value shape as objectui#4768, one column over. - -Changing the field is now one edit with the operator, the value's shape **and** -the value's type. Convertible values are carried, the rest clear to the family's -empty shape (scalar `''`, list `[]`, range `[]`): - -- `"42"` on a number column becomes the number `42`; `"acme"`, `"42abc"` and - `"1,000"` clear. The reading is deliberately stricter than `parseFloat`, which - would turn `"acme"` into `0` — a filter the user never wrote; -- `"true"` / `"false"` convert on a boolean column, and a boolean becomes - `"true"` / `"false"` on a text column, so the round trip closes; `1` and - `"yes"` are conventions rather than readings, and clear; -- date-like columns take only what their own input can render, plus the one - truncation that loses nothing it could have shown (`"2024-03-05T14:30"` → - `"2024-03-05"` on a date column). A bare date does **not** gain a midnight to - fit a `datetime` column: `equals 2024-03-05T00:00` is a filter that looks - answered and matches almost nothing; -- a value the new column can already hold is left alone — switching between two - text columns, or two numeric ones, still keeps what the user typed, and an - unfilled row stays unfilled. - -The convertibility judgement is defined once, next to `reshapeFilterValue`, -and `getInputType` now reads the same family table it does — so the type a value -is converted **to** and the input it is edited **in** cannot drift apart. diff --git a/.changeset/filter-builder-icontains-operator.md b/.changeset/filter-builder-icontains-operator.md deleted file mode 100644 index 1725df48b9..0000000000 --- a/.changeset/filter-builder-icontains-operator.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/fields': patch -'@object-ui/i18n': patch ---- - -FilterConditionField can author the spec's `$icontains` — case-insensitive contains is reachable from the filter UI. - -`@objectstack/spec`'s `FieldOperatorsSchema` gained `$icontains` between -`17.0.0-rc.2` and `rc.5`, and every driver and evaluation face the platform -ships now executes it. `FilterConditionField` had no builder operator that could -author it, so the capability was unreachable from the sharing-rule criteria -builder and sat in that widget's parity test as an explicit `KNOWN_UNREACHABLE` -entry. - -The FilterBuilder gains a `containsCaseInsensitive` operator ("Contains (ignore -case)", translated in all ten locale packs). `condToMongo` emits -`{ field: { $icontains: value } }` and `kvToCondition` reads it back, so a saved -criteria reopens in the visual builder instead of falling into the raw-JSON -editor. Today's `contains` is unchanged and still emits the case-SENSITIVE -`$contains`; whether it should have been case-insensitive all along is a product -question that stays open, and stored filter views keep meaning what they meant. - -The fold is ASCII-only by contract — `café` does not match `CAFÉ`. - -The new operator is **opt-in per consumer**: `FilterBuilder` takes an -`extraOperators` prop, and only `FilterConditionField` passes it. The one -dropdown feeds three at-rest dialects and only the MongoDB-style criteria this -widget writes can carry the operator — the spec's `VIEW_FILTER_OPERATORS` (saved -views) and `VALID_AST_OPERATORS` (the live grid's filter AST) have no -case-insensitive contains, so offering it there would author a filter those -paths cannot execute. Every other FilterBuilder is unchanged. diff --git a/.changeset/filter-builder-like-ilike-ruling-harvest.md b/.changeset/filter-builder-like-ilike-ruling-harvest.md deleted file mode 100644 index c7e805e97c..0000000000 --- a/.changeset/filter-builder-like-ilike-ruling-harvest.md +++ /dev/null @@ -1,18 +0,0 @@ ---- ---- - -Test-comment only: harvests the maintainer ruling on objectui#4911 into the -`KNOWN_UNREACHABLE` entry for `$like` / `$ilike` in -`FilterConditionField.operators.test.ts`. The entry's justification was landed as a CITED -OPEN QUESTION ("undecided — see #4911") to unblock the queue while the authoring-surface -call was pending; it is now rewritten as the decision it became — ruled B on 2026-08-17, -the visual FilterBuilder deliberately does not offer raw pattern-matching authoring, the -constrained intents (`contains` / `containsCaseInsensitive` / `startsWith` / `endsWith`) -are the authorable surface, and the API surface is unaffected since spec goes on accepting -both operators for hand-written ObjectQL and direct JSON authors. The ruling's named -reopen condition (a real user or deployment asks to author wildcard patterns in the UI) is -recorded on the entry, because the exclusion ratchet can only check that a member is still -a spec operator, never that its reason is still the true one. - -The two `KNOWN_UNREACHABLE` members, the reachability sweep and the exclusion ratchet are -unchanged; no operator was added or removed. Declared as releasing nothing. diff --git a/.changeset/filter-builder-lookup-empty-options.md b/.changeset/filter-builder-lookup-empty-options.md deleted file mode 100644 index 015d6a9308..0000000000 --- a/.changeset/filter-builder-lookup-empty-options.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@object-ui/components": patch ---- - -fix(components): FilterBuilder 的 lookup 列不再因 `options: []` 被拒掉远程搜索 (objectui#5031) - -`renderValueInput` 里那条远程 picker 分支的条件写的是 `!field?.options`,而 `[]` -是真值 —— 于是一个带 `referenceTo`、`options` 为空数组的 lookup 列**进不到** -`LookupValuePicker`,落进按 options 画的分支:标量算子得到一个候选数为 0 的 -Select(没有搜索框),`in` / `notIn` 得到一个空的勾选框列表。用户在这一列上挑不出 -任何**新**值,而同一列若 `options` 键干脆缺席反而能拿到完整的远程搜索。可达性不需要 -任何异常状态:`@object-ui/fields` 的 `deriveFilterFields` 与 `plugin-view` 的 -`deriveFieldOptions` 都把 `options` 原样透传,对象元数据里 picklist 值尚未到位时 -就是这个形状。 - -objectui#4874(PR #5030)已经为「静态选项集是否真的在位」建了唯一判据 -`hasStaticOptionDomain(field)`(= `options` 是**非空数组**),并按「`options: []` -属于远程/未到位」这一侧裁定了值域行为。这条分支条件把同一个问题又答了一遍,两个答案 -对 `options: []` 相反:值域侧当它是远程列(保值),控件侧当它是静态列(画空 Select)。 - -按 2026-08-17 维护者裁定,分支条件改读同一个判据: - -- `referenceTo`(或 `type` 为 `user` / `owner`)且 `options` 为 `[]` 的 lookup 列 - → `LookupValuePicker`,与 `options` 键缺席时完全一致:有搜索框、有候选、能选出 - 新值;多值算子走 picker 的多选形态,仍然回吐列表(objectui#3958)。 -- `options` **非空**的列不受影响 —— 选项集就是它的全部值域,静态 Select 依旧。 -- 分支的其余条件未动:没有 `referenceTo` 又不是 `user` / `owner` 的列(无处可搜)、 - 以及 `select` 这类非 lookup 列,路由与此前逐字相同。 - -「值必须可见」这一条不变,只是由 picker 而不是临时 `SelectItem` 兑现。 diff --git a/.changeset/filter-builder-set-operator-value-shapes.md b/.changeset/filter-builder-set-operator-value-shapes.md deleted file mode 100644 index 55d1f2f5d0..0000000000 --- a/.changeset/filter-builder-set-operator-value-shapes.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/i18n': patch ---- - -`FilterBuilder` gives the set and range operators an input that matches the value shape the spec accepts, and stops minting the shape it refuses. - -Three independent paths let one filter row end up with `operator: 'in'` and a -SCALAR `value` — the shape `ViewFilterRuleSchema` refuses at save time since -objectstack#6227, and the shape the query path answered `400 INVALID_FILTER` on -before that (objectstack#5869): - -- Changing the operator dropdown wrote `{ operator }` alone, so the seed `''` - (or whatever the previous family had produced) survived the switch into - `in` / `not_in` / `between`. The operator and the shape of its value are one - edit, so they are now made together: switching families re-shapes the value — - a typed scalar becomes a one-element list, an empty one becomes `[]`, a range - keeps its first bound and leaves the second open, and a list collapsing to a - scalar keeps its first entry. -- A plain text or number column has no static `options`, so `in` fell through to - the single-value input and the user could only ever type a scalar into it. - Those columns now get a token input (type, Enter or comma commits, `×` or - Backspace removes) that always emits an array; `between` gets its two bounds - instead of one box. The lookup picker's no-DataSource fallback, which also - handed back a scalar while `multiple`, emits a list too. -- The multi-value families were decided from a local `["in", "notIn"]` literal, - already one spelling adrift: `notIn` is an alias and the canonical member is - `not_in`, so a stored view read back in canonical form got the single-value - input for a set operator. The families are now read from `@objectstack/spec`'s - exported `VIEW_FILTER_LIST_VALUE_OPERATORS` / `VIEW_FILTER_PAIR_VALUE_OPERATORS` - and folded through `normalizeFilterOperator`, so both spellings of one operator - get one answer and a family the spec widens is picked up without an edit here. - -`foldFilterGroupToSpecRules` is unchanged and needed no change: it normalizes the -operator and carries `value` through verbatim, so the shape that reaches storage -is the producer's to get right. An untouched `in` row arrives as `[]`, which the -fold's existing incomplete-row rule already drops. - -Four locale keys are added to all ten packs for the new inputs -(`filterBuilder.addValue` / `.removeValue` / `.rangeStart` / `.rangeEnd`). diff --git a/.changeset/filter-builder-value-option-domain.md b/.changeset/filter-builder-value-option-domain.md deleted file mode 100644 index 52df5a0265..0000000000 --- a/.changeset/filter-builder-value-option-domain.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@object-ui/components": patch ---- - -fix(components): FilterBuilder 的值不再落在列的选项集之外还看不见 (objectui#4874) - -带 `options` 的 select/lookup 列,其值控件是一个受控的 Radix Select,而 -`SelectValue` 只认已挂载的 `SelectItem`。于是文本列的 `equals "acme"` 指到 -picklist 列(选项 `won` / `lost`)之后,值控件显示空,行里仍是 -`value: "acme"` —— `foldFilterGroupToSpecRules` 照样持久化、实时网格照样拿 -`stage equals "acme"` 去查。这是 #4768(operator)、#4781(值的类型)之后 -「看不见的值」的第三张脸,成因是**值域**而不是类型:`select` / `lookup` 属于 -文本族,`"acme"` 在类型上装得下,只是不在该列的选项集里。 - -按 2026-08-17 维护者裁定(A + C 组合): - -- **静态选项列**(`options` 是非空数组,选项集就是该列的全部值域):切换 field - 时做成员判定,不在选项集里的值清到 #4781 的那套空形 —— 标量 `''`、列表逐项 - 过滤后 `[]`。列表是**逐项**判定,用户写对的那几项不会被一颗坏项连坐。 -- **远程/异步列**(lookup 远程搜索、`options` 缺席,或 `options: []` 尚未到位): - 值**保留**并**可见** —— 绝不因一份从未声称完整的本地选项集去删一个合法的 - lookup id。Select 把该值挂成一个临时项(标签用值本身,与 - `LookupValuePicker` 对没有 label 的 id 的做法一致),多值列表把它渲染成一行 - 已勾选、可取消的条目。 -- 可见性是**无条件**的:无论值是切列带来的、从已存视图读回来的,还是选项集晚到 - 才对不上,控件都显示行里真正带着的东西。「行带值、控件空白」这个形态不再存在, - 也不会为了显示去悄悄改写传入的 `value`。 diff --git a/.changeset/first-boot-seed-org-scope-5243.md b/.changeset/first-boot-seed-org-scope-5243.md deleted file mode 100644 index 9c3dd588bf..0000000000 --- a/.changeset/first-boot-seed-org-scope-5243.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -Fix the metadata seed cache delivering nothing on the boot right after a first login. - -On a browser that has never signed in, `ActiveOrganizationStorage` is empty at -mount, so the eager `app` fetch — one round trip — lands long before -`AuthProvider` resolves the active organization (`getSession` → -`listOrganizations` → `getActiveOrganization`). The seed entry was therefore -written under the no-org scope while every later boot computes the real -organization id, so the entry was never read again and an orphaned no-org entry -was left in `sessionStorage` until the tab closed. - -The entry is now moved onto the resolved organization's key at the moment the -organization resolves, taken from the live cache entry — no extra request and no -deferred render. This is a relabelling rather than a re-scoping: that first -request carries no `X-Tenant-ID`, and the server does not read that header for -tenant scoping (`resolveAuthzContext` derives the tenant from -`session.activeOrganizationId` alone), so the response was already computed for -exactly the organization being stamped. diff --git a/.changeset/flat-map-config-whitelist-5177.md b/.changeset/flat-map-config-whitelist-5177.md deleted file mode 100644 index ff75322cbf..0000000000 --- a/.changeset/flat-map-config-whitelist-5177.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@object-ui/plugin-view': minor -'@object-ui/plugin-list': minor -'@object-ui/plugin-map': minor ---- - -`ObjectView` and `ListView` now flatten a view's `map` block through a -whitelist instead of spreading the whole (untyped) block to the top level. - -Both `case 'map'` flatteners used to build the `object-map` schema with -`...(options.map || {})` — a raw spread of an untyped bag -(`NamedListView.options?: Record`), so any key an author wrote in -the `map` block reached the top level unfiltered. `ObjectMap`'s own -`FlatMapConfigKeys = Omit` declares `style` OUT of -this flat form (`style` is also `BaseSchema.style`, inline CSS legal on every -node), so the two disagreed about the same shape. `style` was the live -specimen: `map: { style: '' }` reached the top level as a CSS-shaped -`style` key it was never supposed to carry. - -Behavior narrowing, stated because it changes what reaches the flattened -schema: a `map` block key that is not one of `ObjectMapConfig`'s declared -flat keys (`latitudeField` / `longitudeField` / `locationField` / `titleField` -/ `descriptionField` / `zoom` / `center`) — including `style` — no longer -reaches the top level of the flattened `object-map` schema. This closes a gap -rather than removing working behavior: the pinned strict spec view schemas -accept no `map` block at all today, so no author-facing surface could reach -this path, and `ObjectMap` already stopped reading a top-level `style` as a -map style (a dev warning names the correct spelling instead). - -The whitelist is DERIVED from `ObjectMapConfigSchema` (`@object-ui/types/zod`) -rather than hand-listed, so the flatteners and the declaration cannot drift -apart again — a key added to (or removed from) the schema reaches both -flatteners without a second edit. `ObjectMap`'s own `FLAT_MAP_CONFIG_KEYS` is -derived from the same schema for the same reason. diff --git a/.changeset/flow-resume-flow-failed-terminal.md b/.changeset/flow-resume-flow-failed-terminal.md deleted file mode 100644 index 553e2d906a..0000000000 --- a/.changeset/flow-resume-flow-failed-terminal.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -A flow-run failure that arrives as `400 FLOW_FAILED` — and any `404` on the flow route — is now classified as terminal rather than retryable. - -`interpretFlowResponse` tested `!res.ok` first and returned `retryable: true` for -everything it caught, so every non-2xx was treated as a transport failure. That -flag is what `FlowRunner` reads to decide whether to keep the wizard dialog open: -a transport failure did not consume the suspension, so retrying the same run is -meaningful, while a flow failure is terminal because the engine consumes the -suspension before running downstream nodes (resume-once) and a retry can only -reach "No suspended run". - -Two classes were landing on the wrong side of that line: - -- **`400` + `FLOW_FAILED`** — the flow ran and failed. objectstack#8684 moves this - exact event off `200 {data:{success:false}}` onto a real status code, inheriting - the objectstack#3962 ruling that business failures must not ride HTTP 200 inside - a double envelope. Landing this read **first** is the maintainer's explicit - sequencing (ruling of 2026-08-15, sub-decision 3): had the backend flipped - first, a terminal node failure would have kept the wizard open offering a retry - guaranteed to fail. Forward-compatible — nothing sends that body yet, so the arm - is dormant until it does. Only this code qualifies; the route's other 400s - (`INVALID_SIGNAL`, `INVALID_SCREEN_INPUT`) are refused before the signal reaches - the engine's variable map, so the suspension survives and a corrected resubmit - stays meaningful. -- **`404`** — no such suspended run, or no such flow. This half is **not** dormant: - the route already answers `404 No such suspended run` today, and each one had - been keeping the wizard open for a retry that could only 404 again. Keyed on the - status alone, since a proxy or unmounted-route 404 carries no envelope to read a - code from. - -The flow's authorable `errorMessage` is preferred again on the failing path. The -error envelope carries no `data`, so it is read from `error.details` — the -envelope's own declared carrier for structured extras, and the only slot that -survives to the wire once `splitSemanticCode` promotes `details.code`. Absent, the -message degrades to the envelope's own `error.message`, never to silence. diff --git a/.changeset/flowcanvas-network-double.md b/.changeset/flowcanvas-network-double.md deleted file mode 100644 index 80d443c32d..0000000000 --- a/.changeset/flowcanvas-network-double.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only: stub `fetch` for the `automation/actions` endpoint in -`FlowCanvas.test.tsx` so the suite no longer escapes to the real network -(`ECONNRESET`/"socket hang up" noise from happy-dom's own `http`-backed fetch -polyfill); no published behaviour changes. diff --git a/.changeset/flowcanvas-seeds-network-double.md b/.changeset/flowcanvas-seeds-network-double.md deleted file mode 100644 index d929a26153..0000000000 --- a/.changeset/flowcanvas-seeds-network-double.md +++ /dev/null @@ -1,7 +0,0 @@ ---- ---- - -Test-only: stub `fetch` for the `automation/actions` endpoint in -`flow-canvas-seeds.spec-parse.test.tsx` so the suite no longer escapes to the -real network (`ECONNRESET`/"socket hang up" noise from happy-dom's own -`http`-backed fetch polyfill); no published behaviour changes. diff --git a/.changeset/form-data-source-wiring-reads-core-reference-family.md b/.changeset/form-data-source-wiring-reads-core-reference-family.md deleted file mode 100644 index 230dc712b8..0000000000 --- a/.changeset/form-data-source-wiring-reads-core-reference-family.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/core': patch ---- - -A `user` field in a form now receives `dataSource` / `dependentValues` / `dependsOnLabels`, like every other reference field. - -The form renderer decided which registered widget gets those three props from a -module-private `DATA_SOURCE_FIELD_TYPES` set, while `@object-ui/core` kept -`EXPANDABLE_FIELD_TYPES` for the same underlying fact — a field whose stored -value is a foreign key into another object. The core side's TSDoc claimed to -mirror the form's set, and it did for 15 days: the form's copy then gained -`capability-multiselect` (objectui#2403) and the three widget-hint pickers -`object-ref` / `filter-condition` / `recipient-picker` (objectui#2421) on the -same day, after which the two sets were not in a subset relation in either -direction — `user` only in core, the picker names only in the form — with -nothing able to report it. - -The form now derives its rule instead of restating it: the reference half is -core's set, the form-specific half is the three picker names, which are widget -hints and can never be a declarable field `type`. Adding a member to -`EXPANDABLE_FIELD_TYPES` therefore also grants it the form's data-source wiring; -that coupling is intended and is now written down on both sides. - -The user-visible half is `user`. It previously received none of the three props. -`dataSource` and `dependentValues` each have a `SchemaRendererContext` fallback -inside the widget, so the person picker limped along wherever a provider -happened to supply one; `dependsOnLabels` has no fallback, so a -dependency-gated user picker interpolated the raw API name into its -"select ... first" hint in every locale — the leak objectstack#5407 closed for -lookups and left open here. The widget contract's own `dataSource` doc has -always named `user` among the types the form renderer injects for. - -No change to what is expanded, projected or rendered anywhere else: the core -set's members are untouched. diff --git a/.changeset/form-default-branch-max-length-ceiling.md b/.changeset/form-default-branch-max-length-ceiling.md deleted file mode 100644 index 08cf9c9f75..0000000000 --- a/.changeset/form-default-branch-max-length-ceiling.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@object-ui/components': patch ---- - -The built-in form's `default` fallback branch now enforces a declared `max_length` ceiling. - -The last arm of the field switch — the one serving a `type` that is neither a -built-in field type nor resolvable from the registry — spread its props straight -onto the rendered `Input` and never read the declared ceiling. One declaration -therefore split into two outcomes depending on how it was spelled. Measured on -`main` after objectui#5201 landed: - - max_length: 50 -> attrs=["class","max_length",...] maxlength=null - maxLength: 50 -> attrs=["class","maxlength",...] maxlength="50" - -The camelCase spelling capped by coincidence — it happens to name a real DOM -attribute. The legacy `max_length` capped nothing at all and landed as a stray, -inert `max_length="50"` attribute: invalid HTML that reads like a working cap to -the next reader. Two independent defects, both now fixed. - -`max_length` is a live authoring spelling, not a fossil: the registered `field:*` -widgets have dual-read `maxLength ?? max_length` since framework#1878 §3, all -three producers of a form field normalize it (`ObjectForm`, `sectionFields`, -`EmbeddableForm.applyDefaultMaxLengths`), and `@object-ui/types` declares it on -several field types. This branch serves a hand-authored `FormSchema` handed -straight to the renderer, where there is no normalizing producer in between and -the author is the producer — so it was the one reader in the repo that dropped -the declaration. - -Same defect and same fix shape as objectui#5201 (the `input` arm) and -objectui#3439 (the `textarea` arm): the ceiling is resolved locally inside the -branch, and the legacy key is destructured off locally. The shared -renderer-only strip table is deliberately unchanged — it feeds the `checkbox`, -`switch` and `select` arms too, and widening it would alter branches this change -does not test. - -A field that declares no ceiling in either spelling renders no `maxlength` -attribute, exactly as before. diff --git a/.changeset/form-field-crossnamespace-fallback-5254.md b/.changeset/form-field-crossnamespace-fallback-5254.md deleted file mode 100644 index c9922ff1ea..0000000000 --- a/.changeset/form-field-crossnamespace-fallback-5254.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@object-ui/components': minor ---- - -Form-field type resolution no longer falls back to `ui`-namespace SDUI node renderers. - -A `FormSchema` field's `type` now resolves a `field:`-namespaced widget or takes -the builtin `default` input branch. It no longer falls back to -`ComponentRegistry.get(type)` — the bare name in whatever namespace happened to -hold it. (Maintainer ruling of 2026-08-19 on objectui#5254, option B.) - -**This is a behaviour change, and it is the point of the change, not a side -effect.** A spelling that resolved yesterday stops resolving: a form field whose -`type` names a non-field component renders the default input instead of that -component. Measured on the built-in (no-`registerAllFields()`) path, the removed -fallback answered **126** bare names — `div`, `h1`, `card`, `button`, `form`, -`alert`, `badge`, the display `text` widget — and 116 of them with the fields -package registered as well. Marked `minor` for that reason. It is released as a -behaviour change rather than a fix because callers cannot tell from their own -metadata which of the two rules answered them; anyone who deliberately pointed a -form field at an SDUI component was relying on a rule no contract stated, and -that reliance now needs a `field:`-namespaced widget instead. - -Nothing changes for the two paths that carry real traffic. With -`registerAllFields()` — the production configuration — every affected field type -already resolved its own `field:` widget (`email` to `field:email`, `password` -to `field:password`, `text` to `field:text`), and object-derived forms go -through `mapFieldTypeToFormType`, which has always emitted the `field:`-prefixed -id. Rendering one of these components as a top-level SDUI **node** is untouched: -this rule governs field resolution only. - -What the fallback was producing on the built-in path, for -`{ name: 'contact', type: 'email', max_length: 50 }`: - -``` -attrs=["class","id","max_length","field","aria-describedby", - "aria-invalid","type","value","name"] maxlength=null -``` - -`email` and `password` are registered as `ui`-namespace node renderers for -top-level `{ type: 'email' }` nodes, so reached as a *field* they received the -field-widget prop bundle they do not implement and spread it onto the element: -the raw metadata object landed as `field="[object Object]"`, `max_length` -landed as an inert attribute with no cap in effect (`maxlength` null), and the -node renderer's own `