diff --git a/.changeset/25235-missed-scheduled-run.md b/.changeset/25235-missed-scheduled-run.md deleted file mode 100644 index 579bcc7f057..00000000000 --- a/.changeset/25235-missed-scheduled-run.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/trigger-schedule': patch -'@objectstack/service-automation': patch -'@objectstack/service-job': patch ---- - -feat(trigger-schedule): a scheduled run whose due time passed while no kernel was ticking is recorded as `missed` in the flow's run history, and is not run - -Clause-②: yes (widening) -Metadata-protocol: none - -A hosted kernel that is stopped, evicted or restarting does not tick, and a scheduled flow due in that gap used to leave no trace: nothing ran and nothing said so, so an app could not tell a sweep had not happened. Now the next kernel to bind the flow records each such due time once, as a run-history entry with status `missed`: - -- **What the entry says.** `startedAt` is the due time that passed and `completedAt` the instant the next kernel detected it. No step ran, so `steps` is empty; `trigger.type` is `schedule`. The entry is listed by `listRuns` (and `GET /api/v1/automation/:name/runs`), `?status=missed` selects it, and it is durable history in `sys_automation_run`, aged out like every other terminal run. -- **Record only.** Nothing is run for a missed due time. It is never replayed or caught up; the flow's next due time runs as usual. -- **What counts as a silence.** The flow's job was still armed when its kernel stopped (its `sys_job` row still `active`: a kernel that stops ticking cancels nothing, while disabling, republishing or withdrawing a flow does), and a due time of the schedule that kernel armed falls after its last fire and before the next kernel bound the flow. A kernel that ticked through records nothing, and a flow that was deliberately unbound records nothing for the time it was unbound. -- **A refusal is not a silence.** A kernel that refuses the flow on purpose (scheduled work switched off for it, a missing acting organization, an unreadable schedule) now disarms the job by its name even when it never bound it, so the next kernel to run the flow does not read that kernel's life as missed due times. `FlowTrigger` gains an optional `release(flowName)`, which the engine calls when its scheduled-work policy refuses a flow before the trigger is reached. -- **Never a run that happened.** A due time whose tick window is claimed in the dispatch ledger (`sys_flow_dispatch`) was dispatched, landed or died in flight, and is not recorded. With no persisted ledger the check is skipped, and a ledger read that fails records nothing for that due time; the trigger says either at `warn`, naming the flow. -- **Bounded by the run history's own cap.** At most the run history's per-flow cap (`maxTerminalRunsPerFlow`, 100 by default) of the newest due times are recorded per flow per detection, since the store keeps no more terminal rows than that per flow. One `warn` line names the flow, how many earlier due times were not recorded and the earliest of them. -- **Two replicas, one row.** The entry's id is derived from the flow and the due time, so replicas that detect the same silence write one history row. - -`DbJobAdapter` gains `readArming(name)` (adapter-local, like `setReplayGuard`; not part of `IJobService`), which reads how a job's `sys_job` row was left. `AutomationEngine` gains `recordMissedRun(...)` and `missedRunLimit()`, the run stores expose their configured `maxTerminalRunsPerFlow`, and `TERMINAL_RUN_STATUSES` and the `sys_automation_run.status` options gain `missed`. diff --git a/.changeset/25579-batch-parent-summary-rule.md b/.changeset/25579-batch-parent-summary-rule.md deleted file mode 100644 index c21b4b385be..00000000000 --- a/.changeset/25579-batch-parent-summary-rule.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql)!: inside one bulk create, a child rule that reads a parent summary counts the earlier rows of the same batch its rules admitted, so a batch is judged as its rows would be one by one (#25579) - -Clause-②: no (narrowing) — a bulk create (`insert(array)`, `insertMany`, and the doors on them: `createManyData`, `insertManyData`, the import's batched creates) refuses a row whose rule reads a parent `summary` that the earlier admitted rows of the same batch move past the rule's limit, which it admitted before. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the bulk insert (`@objectstack/objectql`), served by every bulk create door. - -- **Before.** A bulk insert read the related rows its rules read through a reference once, before any row of the batch was written, and recomputed parent summaries once, after. So "each supplier has at most one primary contact" (a `count` summary filtered on `{ is_primary: true }`, and the child rule `record.is_primary == true && record.supplier.primary_contact_count > 0`) refused a second primary contact written on its own, but admitted two primary contacts for one supplier in one import: both rows read `0`. -- **Now.** Each row's rules read the parent summary as it stands after the earlier rows of the same batch that the rules admitted. The batch's admitted rows are accumulated in memory per summary and per parent; ⛔ nothing is re-read from the database per row. In the card's import, one contact is created and the other row reports the rule's error. A multi-row insert preview (`validate` with an array, mode `insert`) answers the same way. -- **Unchanged.** Which rows land and how a refusal is reported stay each door's own: `insertMany` / `insertManyData` (the import) report the refused row and write the rest; `insert(array)` / `createManyData` refuse the whole batch. A row the rules refuse is not counted. A single write reads the stored summary as before. The summary written after the batch is the same recompute as before. -- **Which summaries accumulate.** `count`, and `sum` / `min` / `max` over number values, with the summary's `filter` evaluated on the new row. An `avg` summary, a `sum` / `min` / `max` that meets a value that is not a number, and a summary whose `filter` reads a dotted path through another reference keep reading the stored value, as before. -- **Not covered.** Concurrent writes: two writes in flight at the same time each read the summary they started from (批量等于逐条,并发不保证). -- **Who sees it.** An app whose child rule reads a parent summary and that bulk-creates children: a batch that broke the rule was admitted whole before, and now its offending rows are refused. - -**The remedy.** None needed for a rule that meant what it says. A batch that must land whole has to satisfy the rule row by row, in order. diff --git a/.changeset/25582-import-row-every-cell-error.md b/.changeset/25582-import-row-every-cell-error.md deleted file mode 100644 index 630b5ada258..00000000000 --- a/.changeset/25582-import-row-every-cell-error.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): an import row with several bad cells reports every one of them in `errors` (#25582) - -Clause-②: yes (widening: a failed import row whose cells did not coerce gains the optional `errors` list, `ImportRowResultSchema.errors`; every existing key keeps its value) -Metadata-protocol: none -Ruling-ref: #25582 ruling `6377844879` letter A, proposal 4; the spec seat's fill rule `6434667735` - -A row with several bad cells used to report only the first one, so an operator fixed one cell, imported again, and only then saw the next. The row now carries `errors`, one `{ field, code, message }` element per failed cell, in column order. The first-error keys `field`, `code` and `error` are unchanged and name element 0, so a reader of those keys sees the same answer as before. - -- It reaches every path that reports rows: the dry run and the commit, on the synchronous `POST /api/v1/data/:object/import` and on the async import job's results. -- It is filled only from the import's cell-coercion errors (`invalid_date`, `invalid_number`, `invalid_option` and the rest). A row that failed for another reason omits it: the dry run's validate verdict, a refused write, a duplicate or ambiguous match key. -- `ImportRowResult` (`@objectstack/core`) gains the optional `errors?: ValidateDataIssue[]`. diff --git a/.changeset/25753-field-external-id-dead.md b/.changeset/25753-field-external-id-dead.md deleted file mode 100644 index df724c8fa1a..00000000000 --- a/.changeset/25753-field-external-id-dead.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `os lint` tells an author that a field's `externalId: true` does nothing - -Clause-②: no (a liveness-ledger re-grade: no schema, accept set, served body or error code moves; only the advisory `liveness-dead-property` warning on an authored `externalId: true` starts, and the flag's description is reworded) -Metadata-protocol: none - -The shipped liveness ledger (`liveness/field.json`) graded a field's `externalId` flag `live` on the strength of one renderer module that was deleted for having no importers. Re-measured across `packages/**` and `ui/**`, nothing reads the flag: the Studio field inspector edits it and a metadata summary copies it, and no upsert matches on it. The row is now `dead`, so `os lint` warns on a field that sets `externalId: true` (the default `false` is not warned). The flag's schema description, and the reference pages generated from it, no longer say it drives upserts. - -- **What to do.** Remove the flag. To match seed records on a field, name the field in the seed dataset's `externalId` (`name` when it is left out). For an import, name it in the mapping's `upsertKey`. -- **Unchanged.** Parsing (the key is still accepted, default `false`), seed loading, imports and every upsert. diff --git a/.changeset/25767-retire-16x-retirement-kits.md b/.changeset/25767-retire-16x-retirement-kits.md deleted file mode 100644 index ba12561c794..00000000000 --- a/.changeset/25767-retire-16x-retirement-kits.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': patch -'@objectstack/metadata-core': patch ---- - -feat(spec)!: retirement kits whose conversion retired at or below protocol 16.x leave the tree; the strict unknown-key refusal names the protocol upgrade guide - -Clause-②: no (narrowing) -Metadata-protocol: narrowed - -**BREAKING** for a source or a stored `sys_metadata` row still carrying a key whose ≤16.x kit aged out. - -- **The rule (ADR-0087, amended 2026-10-26, #25767 ruling D2 = A as scoped by ruling 6434038284).** A retirement kit — the `retiredKey()` tombstone and its D2 conversion with fixtures — lives through the protocol major after the one it retired in and is removed when the next major is cut. At 18 that removes the kits whose conversion retired at or below 16.x; 19 repeats the move one major on. The kit ages out; the migration chain's step stays. -- **Before.** Such a key met a `retiredKey()` tombstone: the parse refused it with the key's own prescription in the error, `os migrate meta --from 16` listed and applied the mechanical edit, and a stored row in that shape was rewritten at read by the ≤16.x conversions. -- **Now.** Where the tombstone sat in a strict container, the key is gone from the shape, so a `strictObject` refuses it as an unknown key. The refusal carries one more sentence, after its fix channels and ahead of its history sentence (`RETIRED_KEY_UPGRADE_GUIDE_SENTENCE`, exported with `PROTOCOL_UPGRADE_GUIDE_URL` from `@objectstack/spec`): it says that keys retired before the previous major are no longer prescribed in the error and names the protocol upgrade guide, `https://objectstack.ai/docs/protocol-upgrade`, where the prescription lives; the CHANGELOG of the release that retired the key carries it too. A tombstone that stays (its container strips unknown keys) ends its error with a sentence that says the edit left `os migrate meta` with the 16.x conversions, to be applied by hand or on a release that still carries the conversion, and links the guide. -- **The chain stays.** `MIGRATION_SUPPORT_FLOOR` stays 16; step 17 and its entry files stay, and `os migrate meta --from 16` still replays 16 → 18, listing every surviving conversion and semantic to-do. Step 17's graduated list drops the ids of the 45 conversions that aged out, so their edits are no longer applied for you. -- **Stored rows.** A `sys_metadata` row still carrying such a spelling is no longer rewritten at read; canonicalize it once with `os migrate meta --stored --apply` on 18.2 or earlier, before moving to 18.3. `app-hidden-to-unpublished` is among the removed conversions, so a pre-split `hidden: true` app row is no longer rewritten to `_unpublished` by any seam, and the artifact door's refusal of it by id is removed as moot (`@objectstack/metadata-core`). -- **What left.** 50 D2 conversions with `retiredAfter` at or below 16.x (5 at 15.0.0, 45 at 16.1.0) and their fixtures; 55 tombstones already `[RETIRED]` at `@objectstack/spec@17.0.0` whose container is strict; 61 `[RETIRED]` lines of the authorable surface and their liveness-ledger rows. 58 same-age tombstones whose container strips unknown keys stay, because deleting one would turn a refusal into a silent strip; the four inline-credential refusals are not retirements and are untouched. -- **The deletion gate.** `check:authorable-surface` proof 1 ages a tombstone one major (was two). A deleted `[RETIRED]` line is dated by an exact `RETIRED_KEYS_BY_MAJOR` row or, new, by the previous major's release tag: a line already `[RETIRED]` on the surface at `@objectstack/spec@.0.0` was retired before that cut. The tag is immutable, so neither a row a PR writes nor one it deletes moves the clock; where the tag cannot be read the gate dates by the registry alone and says so. No gate was added. -- **Who moves.** No `examples/**`, `ui/` or `hotcrm` source in this repository writes a removed key (the `ui/` hits for the removed names are the console's own same-named props and type mirrors, not readers of the retired spec keys, per the liveness ledger's measurements). A consumer source still carrying one meets the strict refusal and the guide link at `os validate`. - - diff --git a/.changeset/25769-console-queue-affordances.md b/.changeset/25769-console-queue-affordances.md deleted file mode 100644 index 7834fdd5721..00000000000 --- a/.changeset/25769-console-queue-affordances.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-list': patch -'@object-ui/i18n': patch ---- - -feat(console): ownership queues in the Console — a queue picker reading both sources, a Claim button the server decides, and an "In a queue" list filter - -Clause-②: no -Metadata-protocol: none - -The server half of ownership queues shipped without a Console face. Now: - -- **Queue pickers list both sources.** The flow designer's approver Value for a `queue` approver, and a sharing rule's `queue` recipient, list the queues an installed app declares (`GET /api/v1/meta/queue`) together with the organization's own `sys_queue` records, one list by machine name, and store the name. The approver picker follows the binding the server publishes (`registry-and-data`); with no data adapter, or a server that publishes another binding, it keeps the text box. -- **Claim.** A record sitting in a queue (no owner, a `queue` named) shows **Claim** in its header only when the explain engine's answer for a transfer of that record carries an admitting `queue_claim` rule; no answer means no button. Claim sends `{ owner_id: }` through the normal update, and a lost race (`409 CONCURRENT_UPDATE`) is reported as "Someone else claimed this record first." -- **In a queue.** A list of an object served with the injected `queue` column gains an **In a queue** toggle that narrows the fetch (and an export) to `owner_id` empty and `queue` set. Which of those rows come back is still the server's read filter. -- **Docs.** A new authoring guide, *Queues and assignment rules*, covers declaring a queue and an assignment rule, the `load` requirement, what a parked record is and who sees it, how a claim is spelled, and that a queue is not a messaging audience; the approvals, sharing-rule and RLS pages link to it. diff --git a/.changeset/25769-ownership-queue.md b/.changeset/25769-ownership-queue.md deleted file mode 100644 index 266843c5f9b..00000000000 --- a/.changeset/25769-ownership-queue.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/objectql': minor -'@objectstack/metadata-core': minor -'@objectstack/metadata': minor -'@objectstack/plugin-security': minor -'@objectstack/plugin-sharing': minor -'@objectstack/plugin-approvals': minor -'@objectstack/lint': minor -'@objectstack/runtime': minor -'@objectstack/platform-objects': minor -'@object-ui/app-shell': minor -'@objectstack/skills': minor -'@objectstack/cli': minor ---- - -feat!: ownership queues: records wait in a queue until a member claims them, and assignment rules route new records (ADR-0142) - -Clause-②: yes (narrowing) -Metadata-protocol: narrowed - - - -**BREAKING** for one shape only: an object whose records have a user owner (`ownership: 'user'`, the default) can no longer declare its own field named `queue`. The platform now injects a `queue` column on every such object, and `registerObject` refuses the declaration with `422 INVALID_METADATA` (`ObjectQueueColumnRefusedError`), naming the object. An `objectExtensions` entry that declares `queue` on such an object is not refused, because an extension is folded over its object at read time: its `queue` is ignored, the platform's column stands, and a warning names the extension. The remedy: rename the business field (for example `support_queue`) and copy its data across, or, when the object's records have no user owner, declare `ownership: 'business_unit'`, `'org'` or `'none'`. The upgrade guide entry is `object-queue-field-reserved`. - -Beside that, two enum additions: `ShareRecipientType` gains `queue` (a sharing rule can now name a queue as its recipient), and the `queue` approver type is authorable again (it leaves `NON_AUTHORABLE_APPROVER_TYPES`, so it is no longer marked deprecated). Two metadata types are new: `queue` and `assignment_rule`. - -- **`@objectstack/spec`**: new `queue` and `assignment_rule` metadata types (`QueueSchema`, `AssignmentRuleSchema`) and the stack keys `queues` and `assignmentRules`. A declared queue's members are positions. New `IOwnershipQueueService` contract, registered under `ownership-queue`. `queue` joins the injected system columns of `ownership: 'user'` objects and `PUBLIC_FORM_SERVER_MANAGED_FIELDS`. `APPROVER_VALUE_BINDINGS.queue` reads both a declared queue and an organization's `sys_queue` row, and `APPROVER_ORG_SCOPED.queue` is `true`. The explain rule kinds `queue_membership` and `queue_claim` are new. `QUEUE_NOT_FOUND` (422) is registered under plugin-security. -- **`@objectstack/objectql`**: objects with `ownership: 'user'` get a system-managed `queue` column beside `owner_id`. Declaring your own `queue` field on one is refused at registration (`INVALID_METADATA`), and an extension's own `queue` field on one is ignored with a warning. An update can carry a write precondition: when it no longer matches, the update answers `409 CONCURRENT_UPDATE`. Declared assignment rules run once for each inserted row that has no owner and no queue after the `beforeInsert` hooks. A rule can park the row in a queue, or pick a member by `round_robin` or `least_loaded`. Assignment never refuses an insert. `kernel:ready` refuses to boot a stack that declares queues or assignment rules when no `ownership-queue` service is registered. -- **`@objectstack/metadata-core`**: `queue` is classified as an injected system column. -- **`@objectstack/metadata`**: the artifact door registers a stack's `queues` and `assignmentRules`, so an artifact-only runtime serves them. -- **`@objectstack/plugin-sharing`**: three new objects, `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, and the `ownership-queue` service. The service resolves a queue name from the registry first, then from the organization's own rows, and has one member expander. A record with no owner that sits in a queue can be read and edited by the queue's members, never deleted. The `queue` sharing recipient grants matched records to the queue's members, and its grants are recomputed when membership changes or the queue is switched on or off. -- **`@objectstack/plugin-security`**: the owner gate handles `queue`. A **claim** sets `owner_id` from empty to yourself, by id. It is admitted when the sharing model allows you the record, and only one claimant wins: the loser gets `409`. A **release** gives your own record back to its home queue. A **move** to another queue needs the transfer grant, and a queue name that does not resolve is refused with `422 QUEUE_NOT_FOUND`. An insert that names a queue needs the transfer grant or membership of that queue. A guest can supply neither. Explain reports the queue reasons on the sharing layer. -- **`@objectstack/plugin-approvals`**: a `queue` approver resolves to the queue's members when the step opens. An empty queue adds nobody and logs a warning. The `queue:` placeholder is never written again. -- **`@objectstack/lint`**: a queue-only approval node is reported `approval-approvers-may-resolve-empty`, and the org-axis red line admits the queue recipient. Two new advisory rules come with `os explain` entries: `assignment-rule-user-target` (warning) and `assignment-rule-queue-unstaffed` (info). -- **`@objectstack/cli`**: the organization-ownership inventory lists `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, so `os migrate organization-ownership` plans them instead of refusing. -- **`@objectstack/runtime`**: `POST /security/_activation/:type/:name` also switches a declared `queue` or `assignment_rule` on and off. -- **`@objectstack/platform-objects`**: the two new metadata types' labels, and the queue recipient option, are translated in zh-CN, ja-JP and es-ES. -- **`@object-ui/app-shell`**: the flow designer no longer warns that a queue approver resolves to nobody. -- **`@objectstack/skills`**: the platform skill lists `queues` and `assignmentRules` among the stack's top-level keys. - -What to do: rename any `queue` field on a user-owned object, or in an extension of one, as above. Every other existing stack behaves as before. To use queues, declare `queues` and `assignmentRules` in your stack, and make sure plugin-sharing is installed: it registers the `ownership-queue` service. diff --git a/.changeset/25785-served-translations-without-metadata-forms.md b/.changeset/25785-served-translations-without-metadata-forms.md deleted file mode 100644 index 5d57b133d65..00000000000 --- a/.changeset/25785-served-translations-without-metadata-forms.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor ---- - -**BREAKING** fix(i18n)!: the translation bundle a console downloads leaves out Studio's `metadataForms` - -Clause-②: yes (narrowing) -Metadata-protocol: narrowed - -`GET /i18n/translations/:locale` served the i18n service's whole locale tree, so every console open downloaded `metadataForms`, the translations of Studio's metadata-type authoring forms: 75 KB of the 221 KB raw zh-CN bundle on `examples/app-crm`. No browser reads that group from the bundle. Studio's forms already arrive translated, because `GET /meta` (and `/meta/types`) resolves `metadataForms.` on the server, from the same i18n service, before it serves each form. - -The route now serves every group except the ones the server resolves itself. The spec names them in `SERVER_RESOLVED_TRANSLATION_GROUPS`, which today holds only `metadataForms`. `GetTranslationsResponseSchema.translations` is now `ServedTranslationDataSchema`: the platform face without those groups, and closed, so a body that still carries one fails it. The runtime copies the tree and never edits the service's own one, which is the tree `/meta` translates from. The `ETag` still hashes the bytes served, so a warm reopen still gets `304`. - -**Why this is breaking, and why `minor`.** A response body that carries `metadataForms` was accepted by the published `GetTranslationsResponseSchema` and now fails it, the exported `GetTranslationsResponse` type loses that member, and the route stops serving a group it served. The published protocol records the move: the authorable surface gains the served face's keys (`ServedTranslationData`) and `GetTranslationsResponse:requestedLocale`, and the response face a client parses against is narrower, so the kind is `narrowed` and both packages that carry it — the spec that declares the face and the runtime that serves it — ship `minor`. What an author writes into a translation bundle does not move. - -**What to do.** Nothing, unless you read `metadataForms` from this route. Read it from `GET /meta/types` instead, which serves each form already translated into the requested locale, rather than from the bundle this route returns. Authoring is unchanged: `metadataForms` stays a group on every bundle face (`TranslationData`, `PlatformTranslationData`), in `i18n:extract` and in the i18n service. - -Also on this route: `requestedLocale`, which the door has always served when it fell back to another locale (`zh` served as `zh-CN`), is now declared on `GetTranslationsResponseSchema`, optional. It was served undeclared, so no generated client could read it. - - diff --git a/.changeset/25793-dependency-range-author-check.md b/.changeset/25793-dependency-range-author-check.md deleted file mode 100644 index cba6dc98707..00000000000 --- a/.changeset/25793-dependency-range-author-check.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -feat(cli): `os validate`, `os build` and `os lint` report `manifest.dependencies` ranges at build time (ADR-0141 D2, author side) - -Clause-②: no -Metadata-protocol: none - -The pre-install dependency check refuses a package whose `dependencies` range it cannot read, and judges a dependency the same artifact delivers for presence only. Until now the author first heard of either from someone else's environment: a `422 DEPENDENCY_MISSING` on install, or, for an in-artifact mismatch, never. The three authoring commands now judge every `dependencies` entry of the packages one install would deliver (each `packages[]` body of an ADR-0130 artifact, or the top-level manifest of a single package) with the install's own predicate, `checkDeclaredDependencies` from `@objectstack/metadata-core`. Nothing re-parses the range grammar. - -- **Error**: a range outside the stated grammar on an entry the artifact does not deliver, and a `dependencies` that is not a `{ PACKAGE_ID: VERSION_RANGE }` record. The install refuses these whatever the target environment holds, so `os build` refusing them refuses nothing the platform would install. -- **Warning**: what the install admits. That is an entry naming a sibling package of the same artifact whose declared `manifest.version` the range does not admit (core bumped to `2.0.0` while a module still declares `^1.0.0`), a co-delivered entry whose range cannot be read, or a sibling whose version is not `MAJOR.MINOR.PATCH`. - -Whether an out-of-artifact dependency is installed, and at what version, is still judged only at install. No build can see the target environment. diff --git a/.changeset/25873-report-date-range.md b/.changeset/25873-report-date-range.md deleted file mode 100644 index 1e56bf9d200..00000000000 --- a/.changeset/25873-report-date-range.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/plugin-report': patch -'@object-ui/i18n': patch ---- - -feat(plugin-report): a report that declares `dateRange` draws a period chip and runs every query inside the chosen window - -Clause-②: no -Metadata-protocol: none - -`ReportSchema.dateRange` (`{ dimension?, defaultRange, allowCustomRange }`) was accepted but read by nothing, so a report showed every period whatever it declared. The report page now reads it: - -- **The chip.** A non-joined report that declares `dateRange` draws a date-range chip above its body: All time, the dashboard's presets under the dashboard's labels, and Custom… (two date inputs) unless `allowCustomRange: false`. It opens on `defaultRange`, or `this_month` when that is absent; `custom` opens the custom pick with no window until both ends are named. -- **The window.** The active window rides every dataset query the report makes (the grouped table, the matrix cross-tab and the embedded chart) as one `selection.timeDimensions` entry `{ dimension, dateRange }` with no `granularity`: a preset goes by name, a custom pick as `[start, end]`, and All time sends no entry. The server ANDs it with `runtimeFilter` and the dataset's own filter. A new pick re-queries. -- **The dimension.** A declared `dimension` is sent as written. Omitted, the renderer reads the dataset definition before any query and uses its one `type: 'date'` dimension; a dataset with none or several draws that authoring error in place of the report and queries nothing. -- **Unchanged without it.** A report that does not declare `dateRange` draws no chip, reads no dataset definition and posts exactly the selection it posted before. A `joined` report (where the spec refuses `dateRange`) is not affected. - -`@object-ui/i18n` gains `report.dateRange.from`, `report.dateRange.to`, `report.dateRange.datasetUnreadable` and `report.dateRange.noSingleDateDimension` in all ten locale packs; the preset labels reuse `dashboard.filters.*`. diff --git a/.changeset/25873-report-export.md b/.changeset/25873-report-export.md deleted file mode 100644 index ba6a4b1de5a..00000000000 --- a/.changeset/25873-report-export.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-report': patch ---- - -feat(plugin-report): the report page offers CSV / XLSX export of the table it draws (`exportOptions`) - -Clause-②: no -Metadata-protocol: none - -`ReportSchema.exportOptions` (`{ formats?, includeHeaders?, fileNamePrefix? }`) was accepted but read by nothing, so a report table could not be downloaded. The report page now reads it: - -- **The control.** A non-joined report draws an Export control above its body once its table has rows, offering the declared formats in order: absent means CSV and XLSX, `formats: []` draws no control. -- **What is exported.** The table as drawn: the grouped table or the matrix cross-tab, its header row (dimension and measure labels as shown), its rows in the order shown and the server totals it draws. The file carries whatever the table's own query carried (the report's `runtimeFilter` and, where declared, its period); nothing is re-queried or re-aggregated. `includeHeaders: false` drops the header row. -- **Who may export.** Not a report key: the control is offered only when the viewer's effective API operations on the dataset's base object include `export` (`ObjectPermission.allowExport`, the predicate the list toolbar uses). An absent operation set (no permissions provider, an unrestricted object) keeps it, as on the list. -- **The files.** Both are written in the browser. The CSV is the drawn text (UTF-8 with a byte-order mark, RFC 4180 quoting; a data cell opening with a formula character is prefixed with a tab). The XLSX writes measure cells as numbers; ExcelJS is imported only when an XLSX is asked for. The file is named `fileNamePrefix`, else the report label, plus a local timestamp (the list export's file name builder). - -`@object-ui/plugin-report` now depends on `@object-ui/permissions` and `exceljs`. diff --git a/.changeset/25883-empty-fields-remembered.md b/.changeset/25883-empty-fields-remembered.md deleted file mode 100644 index 61fcd2f6dcd..00000000000 --- a/.changeset/25883-empty-fields-remembered.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/react': patch -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -fix(plugin-detail): the record page remembers "Show empty fields" per user and object - -Clause-②: no -Metadata-protocol: none - -On a record page, each section's "Show N empty fields" row used to open that one section only, and the choice was gone after a reload or on the next record. A reader who wanted the whole record clicked once per section, every time. - -What changes: - -- In the Console, the choice is now the signed-in user's, per object. One click shows the empty fields in every section of that object's record page. The choice holds on the next record of the same object, after a reload and on another device. Other objects keep their own choice. -- It is stored in a `sys_user_preference` row per object, key `ui.record.`, with the value `{ "showEmptyFields": true }` or `false`. The row sits beside the list page's `ui.list.` rows and loads in the same single preference read. -- `@object-ui/react` adds `RecordPagePreferencesProvider`, `useRecordPagePreferences` and the `RecordPagePreference` and `RecordPagePreferencesStore` types. A host uses them to hand its store to the record page's sections. -- With no store (no signed-in user, or a host that provides none), each section keeps the toggle in its own state, as before. - -What to do: nothing. Nothing a view or object declares changes. diff --git a/.changeset/25908-chart-after-reload.md b/.changeset/25908-chart-after-reload.md deleted file mode 100644 index 9db60296a1a..00000000000 --- a/.changeset/25908-chart-after-reload.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -fix(plugin-chatbot): an assistant answer's chart is still there after a reload - -Clause-②: yes (widening) -Metadata-protocol: none - -The live stream draws a chart from a transient `data-chart` part that the server never stores, so a reloaded answer used to come back as text only. The `visualize_data` tool result is stored, and its `chart` key holds the same descriptor the part carried. The chat now reads it back: - -- **Server history.** A stored message with no `data-chart` part takes its charts from its `visualize_data` results, each placed where its tool part sat among the text. The chart has the same title, series labels and rows as the live one. The result's top-level `rows`, a preview cut for the model's narration, are never drawn. A result stored before the producer wrote `chart` gets no chart, and nothing is rebuilt from its other keys. -- **API mode.** `useObjectChat` rebuilds a hydrated message's `charts` into the store as `data-chart` parts between the text runs they sat between, so the chart keeps its place after the round trip. A live message that carries both the part and the result still draws the chart once. -- **New exports** on the package root and `@object-ui/plugin-chatbot/core`: `chartsFromParts(parts)` returns a stored message's charts with their `textOffset`, and `chartAnswerParts(content, charts)` is its inverse (text runs with `data-chart` parts between them), plus the `ChartAnswerPart` type. The console uses both to restore charts from server history and from its message cache. diff --git a/.changeset/25915-load-failures-data-error-state.md b/.changeset/25915-load-failures-data-error-state.md deleted file mode 100644 index b12f4d82088..00000000000 --- a/.changeset/25915-load-failures-data-error-state.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(app-shell): the route's object-read failure and the page's record-load failure are announced to assistive technology - -Clause-②: no -Metadata-protocol: none - -Two load-failure blocks in the Console drew an `Empty` with no `role`, so a screen reader heard nothing when the read failed: the route slot shown when the by-name read of the route's object gets no answer (`route-object-read-failed`), and the custom page's record-scope read failure (`page-record-load-failed`). Both are now drawn by the shared `DataErrorState` from `@object-ui/components`, which is `role="alert"` by construction, so a failed read is announced and reads differently from an empty one. - -Their titles, messages, i18n keys and defaults, test ids and Retry controls are unchanged; Retry rides in the component's `children` so it keeps its own test id. The visible change is the shared component's look: a small destructive icon square above the title. diff --git a/.changeset/25915-retire-view-and-shimmer-skeleton.md b/.changeset/25915-retire-view-and-shimmer-skeleton.md deleted file mode 100644 index 3692abe2405..00000000000 --- a/.changeset/25915-retire-view-and-shimmer-skeleton.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/components': patch ---- - -refactor(components)!: retire the unused `ViewSkeleton` dispatcher and `ShimmerSkeleton` - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports either component from `@object-ui/components`. Both were published from the package entry and had no importer anywhere in the workspace, so the Console and every `@object-ui/*` package are unaffected. The other skeletons in `view-skeleton.tsx` stay exported unchanged: `GridSkeleton`, `KanbanSkeleton`, `FormSkeleton`, `ListSkeleton`, `ChartSkeleton` and the `ViewSkeletonProps`, `GridSkeletonProps` and `KanbanSkeletonProps` types. - -What leaves, and what to write instead: - -- FROM `ViewSkeleton` TO the skeleton for the variant you passed: `variant: "grid"` is `GridSkeleton`, `"kanban"` is `KanbanSkeleton`, `"form"` is `FormSkeleton`, `"list"` is `ListSkeleton`, `"chart"` is `ChartSkeleton`, each taking the same remaining props. -- FROM `ViewSkeletonVariant` TO a local union of the variants you render, or no type at all once you call the skeleton components directly. -- FROM `ViewSkeletonDispatchProps` TO `GridSkeletonProps` or `KanbanSkeletonProps` (which carry `columns`; `KanbanSkeletonProps` carries `cardsPerColumn`), or `ViewSkeletonProps` for the others. -- FROM `ShimmerSkeleton` TO `Skeleton` from `@object-ui/components`, or a `div` carrying the `animate-shimmer` utility that `@object-ui/app-shell/styles.css` still declares. -- FROM `ShimmerSkeletonProps` TO `React.HTMLAttributes`, which it aliased. diff --git a/.changeset/25918-list-view-typed-schema-reads.md b/.changeset/25918-list-view-typed-schema-reads.md deleted file mode 100644 index 267dcbf59a1..00000000000 --- a/.changeset/25918-list-view-typed-schema-reads.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -refactor(plugin-list): `ListView` reads its view blocks through `ListViewSchema` instead of a `(schema as any)` cast - -Clause-②: no -Metadata-protocol: none - -The kanban, calendar, gallery, timeline, gantt, tree and chart blocks and `bulkActionDefs` are declared members of `ListViewSchema`, so `ListView` now reads them as typed members. The host-composed `rowActionDefs`, which the spec refuses on the list surface, is read through one narrow cast typed as the `object-grid` node declares it. Nothing a list view renders or fetches changes. diff --git a/.changeset/25918-renderer-props-unknown-index.md b/.changeset/25918-renderer-props-unknown-index.md deleted file mode 100644 index f300c0dba05..00000000000 --- a/.changeset/25918-renderer-props-unknown-index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@object-ui/types': patch -'@object-ui/components': patch ---- - -fix(types,components)!: a renderer's props index signature is `unknown`, not `any`, so a forwarded key must be narrowed before it is used - -Clause-②: yes (narrowing) -Metadata-protocol: none - - - -**BREAKING** type-surface narrowing, compile-time only. Nothing a renderer receives or renders at runtime changes. - -- **`@object-ui/types`.** `ComponentRendererProps` keeps its index signature, so every key the host forwards is still admitted, but the signature now maps to `unknown` instead of `any`. -- **`@object-ui/components`.** The built-in renderers' own inline props annotations move the same way. The three host keys renderers read by name and pass to their root element (`data-obj-id`, `data-obj-type` and `style`, written or forwarded by `SchemaRenderer`) are declared once in a package-local type instead. The host-supplied callbacks and values a renderer reads by name (`onPageChange` on `pagination`, `onChange` / `value` on `input-otp`, `loading` on `button`, `className` on `text`, react-hook-form's `ref` on the form's built-in controls) are declared on that renderer's props. - -**Migration.** A renderer typed with `ComponentRendererProps` that reads a forwarded key: - -```ts -// FROM — compiled, because the key was `any` -const Pager = ({ schema, ...props }: ComponentRendererProps) => -